A thought exercise on my part as I'm relatively new to Haskell. I wanted an interesting project to work on so I decided to implement the Hashcash Algorithm, which is most commonly used as the basis of Bitcoin Proof of Work scheme. I am implementing the original specification that utilizes SHA1 and the description of the algorithmic steps are described well in the above Wikipedia article.
This appears to function correctly to the best of my knowledge, however I feel it is somewhat slower than it should be. Any potential suggestions for performance improvements are welcome here. Furthermore, as I am new to writing Haskell, if I am violating common expected conventions here then please feel free to point out how I can write more readable and standard code here.
{-# LANGUAGE BangPatterns #-} module HashCash where import Data.Int import Data.List import Data.List.Split (splitOn) import Data.Char import Data.Function import System.Random import Data.Bits import Data.Either import Data.Binary.Strict.Get import System.IO as SIO import Data.Word (Word32) import Data.ByteString as B import Data.ByteString.Char8 as BC import Data.ByteString.UTF8 as BU import Data.ByteString.Base64 as B64 import Data.ByteString.Conversion as BCON import Data.ByteArray as BA import Crypto.Random import Crypto.Hash startingCounter :: Int32 startingCounter = 1 difficulty :: Int difficulty = 20 headerPrefix = "X-Hashcash: " template = "1:{:{:{::{:{" dateTemplate = "YYMMDDhhmmss" address = "a@a" -- example date because I dont want to mess with date formatting just now exampleDate = "150320112233" convertToString :: ByteString -> String convertToString b = BU.toString b convertFromString :: String -> ByteString convertFromString s = BU.fromString s convertIntToString :: Int -> String convertIntToString a = convertToString . BCON.toByteString' $ a encodeInt32 :: Int32 -> ByteString encodeInt32 a = B64.encode . BCON.toByteString' $ a mahDecoder :: Get Word32 mahDecoder = do first32Bits <- getWord32be return first32Bits firstBitsZero :: (Bits a) => a -> Bool firstBitsZero val = Data.List.foldr (\x acc -> ((not $ testBit val x) && acc)) True [0..(difficulty - 1)] formatTemplate :: String -> [String] -> String formatTemplate base [] = base formatTemplate base (x:xs) = let splix = (Data.List.Split.splitOn "{" base) :: [String] splixHead = Data.List.head splix ++ x splixTail = Data.List.tail splix concatSplitTail = Data.List.init $ Data.List.concatMap (++ "{") splixTail in formatTemplate (splixHead ++ concatSplitTail) xs get16RandomBytes :: (DRG g) => g -> IO (ByteString, g) get16RandomBytes gen = do let a = randomBytesGenerate 16 gen return $ a getBaseString :: ByteString -> Int32 -> String getBaseString bs counter = let encodedVal = B64.encode bs encodedCounter = encodeInt32 counter baseParams = [(convertIntToString difficulty), exampleDate, address, (convertToString encodedVal), (convertToString encodedCounter)] in formatTemplate template baseParams hashSHA1Encoded :: ByteString -> ByteString hashSHA1Encoded bs = let hashDigest = hash bs :: Digest SHA1 byteString = B.pack . BA.unpack $ hashDigest in byteString -- Pass a counter and if the first 20 bits are zero then return the same counter value else increment it -- signifying it is time to test the next number (NOTE: recursive style, may overflow stack) testCounter :: ByteString -> Int32 -> Int32 testCounter rb !counter = let baseString = getBaseString rb counter hashedString = hashSHA1Encoded $ convertFromString baseString !eitherFirst32 = runGet mahDecoder hashedString incCounter = counter + 1 in case eitherFirst32 of (Left first32, _) -> testCounter rb incCounter (Right first32, _) -> if (firstBitsZero first32) then counter else testCounter rb incCounter testCounterBool :: ByteString -> Int32 -> Bool testCounterBool rb counter = let baseString = getBaseString rb counter hashedString = hashSHA1Encoded $ convertFromString baseString eitherFirst32 = runGet mahDecoder hashedString in case eitherFirst32 of (Left first32, _) -> False (Right first32, _) -> firstBitsZero first32 -- Keep taking incrementing counters from an infinite list and testing them until we find a counter -- that generates a valid header findValidCounter :: ByteString -> Int32 findValidCounter ran = Data.List.last $ Data.List.takeWhile (not . testCounterBool ran) [1..] generateHeader :: IO String generateHeader = do g <- getSystemDRG (ran, _) <- get16RandomBytes g let validCounter = findValidCounter ran let validHeader = getBaseString ran validCounter return $ headerPrefix ++ validHeader main :: IO () main = do header <- generateHeader SIO.putStrLn header return ()