4

Below is a simple signature verification flow I wrote, along with a test case with Buidler (all mainly to learn how to verify signatures in a Solidity smart contract). The problem is that the address I recover from the signature and the associated hash is different from the address of the signer that signed the hash. Moreover, (weirdly) the recovered address changes if the hash being signed is changed. I'm sure that shouldn't happen. I'd really appreciate if you could point out what I'm doing wrong!

The contract:

//SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.6.0;

import "@openzeppelin/contracts/cryptography/ECDSA.sol";
import "@nomiclabs/buidler/console.sol";

contract Verification {

    bytes32 public hash;
    bytes public signature;

    constructor(string memory toHash) public {
        bytes32 temp = keccak256(abi.encode(toHash));
        hash = ECDSA.toEthSignedMessageHash(temp);
    }

    function setSignature(bytes calldata _signature) external {
        signature = _signature;
    }

    function verify(address signer) external view returns(bool) {
        
        address recoveredAddr = ECDSA.recover(hash, signature);
        //console.log(recoveredAddr);
        return recoveredAddr == signer;
    }

}

The test script. The first test passes, the second one fails:

const { expect } = require("chai");
const { ethers } = require("@nomiclabs/buidler");

describe("Verification", function() {

    let verify;
    let signer;

    before(async () => {
        const Verification = await ethers.getContractFactory("Verification");
        verify = await Verification.deploy("Hash this message");
        await verify.deployed();

        const addresses = await ethers.getSigners();
        signer = addresses[1];
    })

    // Test objective: To check that signature generated here in the script is stored, as is, in the contract.
    it("Should verify that signature in the contract and the ethers script are identical", async function() {

        const _hash = await verify.hash();
        const _signature = await signer.signMessage(_hash);

        await verify.setSignature(_signature);

        expect(await verify.signature()).to.equal(_signature);
    });

    it("Should verify that the signer signed the hash the contract was deployed with.", async function() {

        const _hash = await verify.hash();
        const _signature = await signer.signMessage(_hash);

        await verify.setSignature(_signature);
        
        signerAddr = await signer.getAddress();
        //console.log(`Signer address: ${signerAddr}`);

        expect(await verify.verify(signerAddr)).to.equal(true);
    })
})

Appreciate the help. Thanks!

4 Answers 4

2

A note in ethers documentation seems to apply in your case:

A common case is to sign a hash. In this case, if the hash is a string, it must be converted to an array first, using the arrayify utility function.

1

in the contract you need to recovery the signer

function _recoverSigner(string memory hash, bytes memory _signature)
        internal
        returns (address)
    {

        return
            ECDSA.recover(
                keccak256(
                    abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)
                ),
                _signature
            );
    }

    function verify(address current_signer, string memory hash,        bytes calldata _signature) public returns (bool) {
        address currentSigner = _recoverSigner(hash, _signature);
        return current_signer == currentSigner;
    }

but in your frontend side you can do it

const hash = web3.utils.soliditySha3(currentAddress);
 const result = web3.eth.accounts.sign(hash, privateKey);  
0

Moreover, (weirdly) the recovered address changes if the hash being signed is changed.

That is exactly how it is supposed to work: there is only one valid hash that will recover the correct address for your signature. If the hash changes, the recovered address changes.

Cryptography is pure math. Imagine changing the value of x in the following equation: x * y = z; in that case, z would also change.

The recovered public key over the prime field used by the secp256k1 elliptic curve is always a valid public key. But if you get the hash wrong, your signature verification fails because you will also get your resulting address wrong.

So, in a way you have your question backward. To debug your issue, you would first have to determine the correct hash for your message. I suspect the hash in your solidity contract is computed differently than the one in JavaScript. Once you have matching hashes in both implementations, you can proceed to verify the signature and recover the address.

0

In my case, I had to do two things:

  1. [Typescript side] Before signing the hash, use ethers.utils.arrayify() function on the hash.

  2. [Solidity side] Prefix the hash being verified with keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)). Here's the ethersjs documentation reference:

    A signed message is prefixd with "\x19Ethereum Signed Message:\n" and the length of the message [...] If recovering the address in Solidity, this prefix will be required to create a matching hash.

    https://docs.ethers.org/v5/api/signer/#Signer--signing-methods

The final code is as follows:

typescript

const data = "0x"
const hashedData = ethers.utils.keccak256(data);

const signeHashData = await signer.signMessage(ethers.utils.arrayify(hashedData))

solidity

    import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

    function checkSignature(
        address _signer,
        bytes32 _hash,
        bytes memory _hashSignature
    ) private pure returns (address) {
        address signer = ECDSA.recover(
            ECDSA.toEthSignedMessageHash(_hash),
            _hashSignature
        );

        if (_signer != signer) {
            revert Invalid signer();
        }

        return signer;
    }

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.