I've seen many instances of verifying signed messages in which the hash to be verified gets passed as an argument. I need to first recreate the hash on-chain, before running ecrecover on it. However, when I keccak256 hash the same thing I hashed off-chain, I'm getting a non-match.

Here's my web3 code, where I create the hash off-chain:

let prepend = keccak256(ethers.utils.defaultAbiCoder.encode(
      [ 'string' ],
      [ "\x19Ethereum Signed Message:\n32" ]

    let message = ethers.utils.defaultAbiCoder.encode(
        'address', 'string', 'uint256'

        contracts.accessControls.address, 'Generate authorization code', this.state.spaceId

    let msg = keccak256(prepend, message);
    await accessControlContract.commitSignatureHash(indexOfUser, this.state.spaceId, msg);

    web3.eth.sign(msg, context.account, (err, res) => {
      } else{
        this.setState({userSecretCode: "register " + this.state.spaceId + context.account + res});

And here's my solidity code where I try to recreate the same hash on chain and then run ecrecover on the sending address:

    function commitSignatureHash(uint256 index, uint256 space, bytes32 hashToVerify) public {
        require(whitelistForSpace[space][index] == msg.sender);
        latestCommittedHashByAddressBySpace[space][msg.sender] = hashToVerify;
        timeStampByAddressBySpace[space][msg.sender] = now;

    function verifySignature(address userRegisteringSpace, uint256 space, uint8 v, bytes32 r, bytes32 s) public view returns (bool){
        bytes32 hashToVerify = keccak256(
                'Generate authorization code', 

        bytes memory prefix = "\x19Ethereum Signed Message:\n32";
        bytes32 prefixedHash = keccak256(abi.encodePacked(prefix, hashToVerify));

        //making sure it recreates a previously committed hash
        require (prefixedHash == latestCommittedHashByAddressBySpace[space][userRegisteringSpace]);

        return userRegisteringSpace == ecrecover(prefixedHash, v, r, s);

msg (created off-chain) doesn't seem to be matching prefixedHash (created on-chain)

2 Answers 2


It looks like off-chain you're doing something like this:

keccak256(keccak256(abiEncode("\x19Ethereum Signed Message:\n32")), keccak256(message))

when it should be:

keccak256("\x19Ethereum Signed Message:\n32", keccak256(message))

The prefix shouldn't be ABI encoded and shouldn't be hashed.


Signing the typed data offchain and verifying it onchain requires great amount of efforts. It is complex in its nature.The process is, any thing you define offchian you need to define a struct for it onchain, and define its type onchain as well. make sure everything on both side remains same, for example if you define type of variable as string in offchain code, so define the type as string onchain as well. A working example:

        const domain = [
            { name: "name", type: "string" },
            { name: "version", type: "string" },
            { name: "chainId", type: "uint256" },
            { name: "verifyingContract", type: "address" }
        const bid = [
            { name: "amount", type: "uint256" },
            { name: "bidder", type: "address" },

onchain code for this should be a struct and define the type of domain and bid

    struct Bid {
        uint256 amount;
        address bidder;

    string private constant EIP712_DOMAIN = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)";
    string private constant BID_TYPE = "Bid(uint256 amount,address bidder)";

After this hash these defined type using keccack256, but remember to encode them first

   bytes32 constant private EIP712_DOMAIN_TYPEHASH = keccak256(abi.encodePacked(EIP712_DOMAIN));
    bytes32 constant private BID_TYPEHASH = keccak256(abi.encodePacked(BID_TYPE));

After this get the domain separator, its the value that prevents replay attacks

    bytes32 private DOMAIN_SEPARATOR = keccak256(abi.encode(
        keccak256("your app name"),
        chainId, // replace this with your chain id

now you need to hash the message that you will be passing in verify function as tupple, and use ecrecover to recover the signer

    function hashBid(Bid memory bid) private view returns (bytes32) {
        return keccak256(abi.encodePacked(
          // In your case you will be hashing each of your type inside the keccak256 of return statement separately,

    function verify(address signer, Bid memory bid, bytes32 sigR, bytes32 sigS, uint8 sigV) public view returns (bool) {
        return signer == ecrecover(hashBid(bid), sigV, sigR, sigS);

offchain code for calling the verify function and sending message, r,s,v to contract

    var message = {
            amount: 100,
            bidder: "0xF6f8196eEE9B04a3cF5711C32A21A8fd625E5dAA"

        const data = JSON.stringify({
            types: {
                EIP712Domain: domain,
                Bid: bid,
            domain: domainData,
            primaryType: "Bid",
            message: message
       //sign the message
    let r,s,v;
    const sign = await window.ethereum.request({
         method: 'eth_signTypedData_v4',
         params: [signer, data],
    const signature = sign.substring(2);
    r = "0x" + signature.substring(0, 64);
    s = "0x" + signature.substring(64, 128);
    v = parseInt(signature.substring(128, 130), 16);
    // call the contract function
    const res = await contract.methods.verify(message,r, s, v).call();

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.