web3-tools-0.1.0.0: src/Eth/Address.hs
module Eth.Address
( Address,
Signature,
verifySignature,
isEIP55Valid,
)
where
import Crypto.Hash.Keccak (hashKeccak256, hashKeccak256Str)
import Crypto.Secp256k1.Recovery (recoverPubKey)
import Data.ByteArray (convert)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import Data.Char (digitToInt, isDigit, isLower, isUpper)
-- | Checks if the address is valid according to EIP-55
isEIP55Valid :: String -> Bool
isEIP55Valid addr = and $ zipWith (\addrChar digestChar -> if digitToInt digestChar >= 8 then isDigit addrChar || isUpper addrChar else isDigit addrChar || isLower addrChar) addr addrDigest
where
addrDigest = hashKeccak256Str addr
-- | Ethereum address (20 bytes)
type Address = ByteString
-- | Ethereum signature (65 bytes: r ++ s ++ v)
type Signature = ByteString
-- | Verify an Ethereum signature
-- Given a message, signature, and address, returns True if the signature is valid
-- Signature should be 65 bytes (r ++ s ++ v) and address should be 20 bytes
-- This function handles the appending of the message prefix.
verifySignature :: ByteString -> Signature -> Address -> IO Bool
verifySignature message sig addr
| BS.length sig /= 65 = return False
| BS.length addr /= 20 = return False
| otherwise = do
result <- recoverAddress message sig
return $ case result of
Just recoveredAddr -> recoveredAddr == addr
Nothing -> False
-- | Recover the Ethereum address from a message and signature
-- Uses Ethereum's personal_sign message format: "\x19Ethereum Signed Message:\n" + length + message
recoverAddress :: ByteString -> Signature -> IO (Maybe Address)
recoverAddress message sig = do
-- Create the Ethereum signed message with prefix
let ethMessage = createEthereumSignedMessage message
messageHash32 = convert (hashKeccak256 ethMessage) :: ByteString
-- Recover the public key using our FFI bindings
maybePubKeyBytes <- recoverPubKey messageHash32 sig
-- Convert public key to address
return $ publicKeyToAddress <$> maybePubKeyBytes
-- | Create an Ethereum signed message with the standard prefix
-- Format: "\x19Ethereum Signed Message:\n" + length + message
createEthereumSignedMessage :: ByteString -> ByteString
createEthereumSignedMessage message =
let prefix = BS8.pack $ "\x19" <> "Ethereum Signed Message:\n"
lengthStr = BS8.pack (show (BS.length message))
in BS.concat [prefix, lengthStr, message]
-- | Convert a public key to an Ethereum address
-- Address is the last 20 bytes of Keccak256(publicKey)
-- Public key should be 65 bytes (0x04 prefix + 64 bytes)
publicKeyToAddress :: ByteString -> Address
publicKeyToAddress pubKeyBytes =
let -- Remove the 0x04 prefix (first byte) from uncompressed public key
pubKeyWithoutPrefix = BS.drop 1 pubKeyBytes
-- Hash with Keccak256 and take last 20 bytes
hashedPubKey = convert (hashKeccak256 pubKeyWithoutPrefix) :: ByteString
in BS.drop 12 hashedPubKey -- Keccak256 produces 32 bytes, we take last 20