bitcoin-scripting-0.3.0: src/Language/Bitcoin/Script/Descriptors/Checksum.hs
{-# LANGUAGE ParallelListComp #-}
module Language.Bitcoin.Script.Descriptors.Checksum (
validDescriptorChecksum,
descriptorChecksum,
) where
import Data.Bifunctor (first)
import Data.Bits (Bits (shiftL, shiftR, testBit, xor, (.&.)))
import Data.Char (ord)
import Data.Foldable (foldl')
import Data.IntMap (IntMap)
import qualified Data.IntMap.Strict as IntMap
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Vector.Unboxed (Vector)
import qualified Data.Vector.Unboxed as Vector
{- | Test whether the textual representation of an output descriptor has the
given checksum.
-}
validDescriptorChecksum :: Text -> Text -> Bool
validDescriptorChecksum desc checksum =
case mapM (charsetFind checksumCharset) (Text.unpack checksum) of
Nothing -> False
Just checkSymbols ->
1 == polymodChecksum (expandChecksum desc <> checkSymbols)
{- | Compute the checksum of the textual representation of an output descriptor
if possible.
-}
descriptorChecksum :: Text -> Maybe Text
descriptorChecksum desc = Text.pack <$> sequenceA checksumChars
where
checksumChars = [checksumCharset `charsetGet` charsetIndex i | i <- [0 .. 7]]
charsetIndex i = (checksum `shiftR` (5 * (7 - i))) .&. 31
symbols = expandChecksum desc <> replicate 8 0
checksum = 1 `xor` polymodChecksum symbols
expandChecksum :: Text -> [Word]
expandChecksum =
reverse . end
. foldl'
( \(gs, s) v -> case (v `shiftR` 5 : gs, v .&. 31 : s) of
([g2, g1, g0], s') -> ([], 9 * g0 + 3 * g1 + g2 : s')
x -> x
)
mempty
. fromMaybe []
. mapM (charsetFind inputCharset)
. Text.unpack
where
end ([g0], s) = g0 : s
end ([g1, g0], s) = 3 * g0 + g1 : s
end (_, s) = s
polymodChecksum :: [Word] -> Word
polymodChecksum =
foldl'
( \chk value ->
foldl'
xor
((chk .&. 0x7ffffffff) `shiftL` 5 `xor` value)
[if chk `testBit` i then g else 0 | i <- [35 ..] | g <- generator]
)
1
where
generator =
[ 0xf5dee51989
, 0xa9fdca3312
, 0x1bab10e32d
, 0x3706b1677a
, 0x644d626ffd
]
data Charset = Charset
{ charToIndex :: IntMap Word
, indexToChar :: Vector Char
}
charsetFromString :: String -> Charset
charsetFromString s =
let xs = [(c, i) | c <- s | i <- [0 ..]]
in Charset
{ charToIndex = IntMap.fromList $ first ord <$> xs
, indexToChar = Vector.fromList $ fst <$> xs
}
charsetFind :: Charset -> Char -> Maybe Word
charsetFind set c = IntMap.lookup (ord c) $ charToIndex set
charsetGet :: Charset -> Word -> Maybe Char
charsetGet set i = indexToChar set Vector.!? fromInteger (toInteger i)
inputCharset :: Charset
inputCharset = charsetFromString "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
checksumCharset :: Charset
checksumCharset = charsetFromString "qpzry9x8gf2tvdw0s3jn54khce6mua7l"