Solidity – Incorrect Signer Address on Verifying Signature with Generated Hash

ethers.jsjavascriptkeccaksignaturesolidity

Basically on verifying the signature with the generated hash of the signed message, I'm getting the correct signer on JS, but not on Solidity.

JS Code (According to Keir Finlow-Bates' suggestion):

async function signHello() {
    // Encode parameters
    const encodedParams = ethers.utils.defaultAbiCoder.encode(
        ['string'],
        ['hello']
    );

    const hash = ethers.utils.hexlify(ethers.utils.toUtf8Bytes(encodedParams));

    const prefixedHash = ethers.utils.solidityKeccak256(
        ['string', 'bytes'],
        ['\x19Ethereum Signed Message:\n' + hash.length, hash]
    );

    console.log("Hash: ", prefixedHash)

    // Sign the message
    const signature = await wallet.signMessage(ethers.utils.arrayify(prefixedHash));
    
    console.log("Signature:", signature);

    return {'hash': prefixedHash, 'signature': signature}

}

function getSigner(hash, signature) {

    const digest = ethers.utils.keccak256(ethers.utils.solidityPack(['string', 'bytes32'], ["\x19Ethereum Signed Message:\n32", hash]));
    return ethers.utils.recoverAddress(digest, signature);
}

signHello().then((res) => {
    console.log(getSigner(res.hash, res.signature))  // getting the correct signer address here
})

JS Code:

const ethers = require('ethers');

const provider = new ethers.providers.JsonRpcProvider("https://polygon-mumbai.g.alchemy.com/v2/YOUR_ALCHEMY_API_KEY");
const privateKey = "YOUR_ACCOUNT_PRIVATE_KEY";
const wallet = new ethers.Wallet(privateKey, provider);

async function signHello() {
    // Encode parameters
    const encodedParams = ethers.utils.defaultAbiCoder.encode(
        ['string'],
        ['hello']
    );

    let hash = ethers.utils.keccak256(encodedParams);

    console.log("Hash: ", hash)
    
    // Sign the message
    const signature = await wallet.signMessage(hash);

    console.log("Signature:", signature);

    return {'hash': hash, 'signature': signature}

}


function getSigner(hash, signature) {

    return ethers.utils.verifyMessage(hash, signature)

    // Using the code below, getting the same incorrect signer address as getting from the getSigner() solidity function

    // const digest = ethers.utils.keccak256(ethers.utils.solidityPack(['string', 'bytes32'], ["\x19Ethereum Signed Message:\n32", hash]));
    // return ethers.utils.recoverAddress(digest, signature);
}

signHello().then((res) => {
    console.log(getSigner(res.hash, res.signature))  // getting the correct signer address here
})

Solidity Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract Signer {
    
    function getHash() public pure returns (bytes32) {
        return keccak256(
            abi.encode(
                "hello"
            )
        );
    }
    
    function getSigner(bytes32 _hash, bytes memory _signature)
        public
        pure
        returns (address)
    {
        bytes32 r;
        bytes32 s;
        uint8 v;
        if (_signature.length != 65) {
            return address(0);
        }
        assembly {
            r := mload(add(_signature, 32))
            s := mload(add(_signature, 64))
            v := byte(0, mload(add(_signature, 96)))
        }
        if (v < 27) {
            v += 27;
        }
        if (v != 27 && v != 28) {
            return address(0);
        } else {
            return
                ecrecover(
                    keccak256(
                        abi.encodePacked(
                            "\x19Ethereum Signed Message:\n32",
                            _hash
                        )
                    ),
                    v,
                    r,
                    s
                );

        }
    }

    function getSignerUsingOpenzeppelin(bytes32 _hash, bytes memory _signature)
        public
        pure
        returns (address)
    {

        return ECDSA.recover(_hash, _signature);

    }

}

PS, I'm getting the same hash for hello in both JS as well as Solidity codes, i.e., 0x984002fcc0ca639f96622add24c2edd2fe72c65e71ca3faa243e091e0bc7cdab.

Using the JS Code (According to Keir Finlow-Bates' suggestion), getting this hash:

0xd9f807e25c27377c0d87443b1736dfaa5c3a582d7023b696acf4dde098ee659e

Which is when used with the corresponding generated signature, returning the correct/expected signer.

But, now the question is how to generate the same hash in solidity?

As keccak256(abi.encode("hello")) is returning a different hash i.e., 0x984002fcc0ca639f96622add24c2edd2fe72c65e71ca3faa243e091e0bc7cdab.

Best Answer

The correct way to sign the message hash in JS is to arrayify it first, using ethers.utils.arrayify, so the signHello() and getSigner() functions can be rewritten like this:

async function signHello() {
    // Encode parameters
    const encodedParams = ethers.utils.defaultAbiCoder.encode(
        ['string'],
        ['hello']
    );

    let hash = ethers.utils.keccak256(encodedParams);

    console.log("Hash: ", hash)

    // Sign the message hash after arrayifying
    const signature = await wallet.signMessage(ethers.utils.arrayify(hash));
    
    console.log("Signature:", signature);

    return {'hash': hash, 'signature': signature}

}

function getSigner(hash, signature) {

    // Verify the arrayifyed message hash with the signature to get the signer

    return ethers.utils.verifyMessage(ethers.utils.arrayify(hash), signature)

}

So, now the generated hash is exactly identical in both JS and Solidity, also the signature can be verified in both, returning the correct/expected signer.