Help with ecrecover() and/or signing/verify

Hello,
I try to verify a message signer. Everything seems to be working well (hashing values, sign message, vrs values … are correct) But when I call the ecrecover function it gives back a "wrong" address...

Clean code:

contract SignTest {

address owner = msg.sender;

mapping(uint256 => bool) usedNonces;

function test(uint256 amount, uint256 nonce, bytes memory sig, uint tV, bytes32 tR, bytes32 tS, bytes32 tMsg) public view returns(address) {

    bytes32 message = prefixed(keccak256(abi.encodePacked(amount, nonce))); 
    bytes32 messageWithoutPrefix = keccak256(abi.encodePacked(amount, nonce)); 

   
    address signer = recoverSigner(messageWithoutPrefix, sig, tV, tR,tS);

    return signer;
}

// Signature methods

function splitSignature(bytes memory sig)
    internal
    view
    returns (uint8, bytes32, bytes32)
{
    require(sig.length == 65, "B");

    bytes32 r;
    bytes32 s;
    uint8 v;

    assembly {
        // first 32 bytes, after the length prefix
        r := mload(add(sig, 32))
        // second 32 bytes
        s := mload(add(sig, 64))
        // final byte (first byte of the next 32 bytes)
        v := byte(0, mload(add(sig, 96)))
    }

    return (v, r, s);
}

function recoverSigner(bytes32 message, bytes memory sig, uint tV, bytes32 tR, bytes32 tS)
    internal
    view
    returns (address)
{
    uint8 v;
    bytes32 r;
    bytes32 s;

    (v, r, s) = splitSignature(sig);

    require(v==tV, "V is not correct");
    require(r==tR, "R is not correct");
    require(s==tS, "S is not correct");

    return ecrecover(message, v, r, s);
}

// Builds a prefixed hash to mimic the behavior of eth_sign.
function prefixed(bytes32 inputHash) internal pure returns (bytes32) {
    return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", inputHash));
}

Code with logs:

contract SignTest {

address owner = msg.sender;

mapping(uint256 => bool) usedNonces;

function test(uint256 amount, uint256 nonce, bytes memory sig, uint tV, bytes32 tR, bytes32 tS, bytes32 tMsg) public view returns(address) {

    console.log("\ncontract_sig: ");
    console.logBytes(sig);
    console.log("contract_amount: ", amount);
    console.log("contract_nonce: ", nonce);

    bytes32 message = prefixed(keccak256(abi.encodePacked(amount, nonce))); 
    bytes32 messageWithoutPrefix = keccak256(abi.encodePacked(amount, nonce)); 

    console.log("\ncontract_message: ");
    console.logBytes32(message);

    console.log("contract_messageWithoutPrefix: ");
    console.logBytes32(messageWithoutPrefix);

    require(tMsg == messageWithoutPrefix, "Wrong message");
    address signer = recoverSigner(messageWithoutPrefix, sig, tV, tR,tS);

    console.log("\ncontract_signer: ", signer);


    return signer;
}

// Signature methods

function splitSignature(bytes memory sig)
    internal
    view
    returns (uint8, bytes32, bytes32)
{
    require(sig.length == 65, "B");

    bytes32 r;
    bytes32 s;
    uint8 v;

    assembly {
        // first 32 bytes, after the length prefix
        r := mload(add(sig, 32))
        // second 32 bytes
        s := mload(add(sig, 64))
        // final byte (first byte of the next 32 bytes)
        v := byte(0, mload(add(sig, 96)))
    }

    console.log("contract_V: ", v);
    console.log("contract_R: ");
    console.logBytes32(r);
    console.log("contract_S: ");
    console.logBytes32(s);
    return (v, r, s);
}

function recoverSigner(bytes32 message, bytes memory sig, uint tV, bytes32 tR, bytes32 tS)
    internal
    view
    returns (address)
{
    uint8 v;
    bytes32 r;
    bytes32 s;

    (v, r, s) = splitSignature(sig);

    require(v==tV, "V is not correct");
    require(r==tR, "R is not correct");
    require(s==tS, "S is not correct");

    return ecrecover(message, v, r, s);
}

// Builds a prefixed hash to mimic the behavior of eth_sign.
function prefixed(bytes32 inputHash) internal pure returns (bytes32) {
    return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", inputHash));
}

The Test:

it("Test Sign", async function () {

    accounts = await hre.ethers.getSigners();
    deployer = accounts[1] 

    const SignTest = await ethers.getContractFactory("SignTest");
    const iSignTest = await SignTest.connect(deployer).deploy();
    await iSignTest.deployed();


    //**Start */

    const nonce = await deployer.getTransactionCount()
    console.log("test_nonce: ", nonce)

    const amount =  ethers.BigNumber.from("1")
    console.log("test_amount: ", amount)

    const message =  utils.keccak256(
      utils.defaultAbiCoder.encode([ "uint256", "uint256" ], [amount, nonce])
      )
      console.log("test_message: ", message)
    


    var sig = await deployer.signMessage(message);

    console.log("test_sig: ", sig)
    const breakdown = utils.splitSignature(sig)
    console.log("Sig breakdown: ", utils.splitSignature(sig))

    

    var toProve = await iSignTest.test(amount, nonce,  sig, breakdown.v, breakdown.r, breakdown.s, message)

    console.log("Result-Contract_Signer: ", toProve)
    console.log("Signer: ", deployer.address)

  });

The output:

SignTest
    test_nonce:  237
    test_amount:  BigNumber { value: "1" }
    test_message:  0x3047222747ab93d34559a4c10f40bf67fa1172f7f6a5b026d5516ba8dbc55df6
    test_sig:          0x9f741d8eba8bd7b3ff1238f7e68b9dc3657d19d827888565810aa2e823bfc1ab2887d6b5c1e720c1b0af2b06a0ba787b8a60348bd493a37b7337c7127f58d4b11c
    Sig breakdown:  {
      r: '0x9f741d8eba8bd7b3ff1238f7e68b9dc3657d19d827888565810aa2e823bfc1ab',
      s: '0x2887d6b5c1e720c1b0af2b06a0ba787b8a60348bd493a37b7337c7127f58d4b1',
      _vs: '0xa887d6b5c1e720c1b0af2b06a0ba787b8a60348bd493a37b7337c7127f58d4b1',
      recoveryParam: 1,
      v: 28,
      yParityAndS: '0xa887d6b5c1e720c1b0af2b06a0ba787b8a60348bd493a37b7337c7127f58d4b1',
      compact:         '0x9f741d8eba8bd7b3ff1238f7e68b9dc3657d19d827888565810aa2e823bfc1aba887d6b5c1e720c1b0af2b06a0ba787        b8a60348bd493a37b7337c7127f58d4b1'
    }

    contract_sig:
    0x9f741d8eba8bd7b3ff1238f7e68b9dc3657d19d827888565810aa2e823bfc1ab2887d6b5c1e720c1b0af2b06a0ba787b8a60348bd493a37b7337c7127f58d4b11c
    contract_amount:  1
    contract_nonce:  237

    contract_message:
    0x2d1a26d11c988a125ec07cbffcafd8fc0c7c4ab3ed9e1479dd26c48e4e5af9f4
    contract_messageWithoutPrefix:
    0x3047222747ab93d34559a4c10f40bf67fa1172f7f6a5b026d5516ba8dbc55df6
    contract_V:  28
    contract_R:
    0x9f741d8eba8bd7b3ff1238f7e68b9dc3657d19d827888565810aa2e823bfc1ab
    contract_S:
    0x2887d6b5c1e720c1b0af2b06a0ba787b8a60348bd493a37b7337c7127f58d4b1

    contract_signer:  0xfac47f5b7eb4db4bf57e46e341fff26150d7766c
    Result-Contract_Signer:  0xFAc47f5b7eB4db4Bf57e46e341fff26150D7766c
    Signer:  0x70997970C51812dc3A010C7d01b50e0d17dc79C8
  1. What could be the reason?
  2. Also I am wondering that, everywhere is said that you need to prefix the message (like here: https://docs.ethers.io/v4/cookbook-signing.html) But as you can see I give out the prefixed and not prefixed message and it looks like the message without the prefix is the same as ethers creates... that means ethers do not prefixing? (why does the doc say something else or what do I over see?)

(I tried both prefixed message and not prefixed)

thats a tricky one, do this:

js test:

const message =  "Hello World"
const hash = ethers.utils.hashMessage(message)
var sig = await deployer.signMessage(message);
...
var toProve = await iSignTest.test(amount, nonce,  sig, breakdown.v, breakdown.r, breakdown.s, hash)

sol contract:

address signer = recoverSigner(tMsg, sig, tV, tR, tS);

the utils.hashMessage() method adds the "\x19Ethereum Signed Message:..." prefix to the message. ethers signMessage() method uses this method internally so you have to pass it the raw message.

Edit:

Thank you:)

It worked so that I can verify the signer correctly. But now I face the problem of getting the variables from the hash in the contract. Maybe I should describe it a bit more what my intentions are:

-Signer (Server/Wallet) creates a hash/message
-Contract encode the hash and get the variables from it back
-Contract check if the message verify also the msg/hash to make sure get the correct variables

If I understand ethers.utils.hashMessage correctly then it prefixes the message. But the return value of my prefixed(keccak256(abi.encodePacked(amount, nonce)));
line gives back another hash than tMsg is.

Any idea how to achieve what I want to do?

yes, thats because you are appending a fixed "32" after the "/x19...", but that has to be the length the unhashed message

not sure what you are trying to do here tbh

1 Like

Thank you help me alot with understanding!

But still I it does not work.

not sure what you are trying to do here tbh

Sorry for my bad english.

I aim for a onchain prove if the function is called with the same parameter as the messages was created from ... To prove if the user uses the same data as the server signed. In the first test it was a uint. But the target is to have one uint and one dynamic string (if that even possible).

I also test araound a bit but I just do not find the problem....

Code .sol:

function test(uint256 amount, string memory nonce, bytes memory sig0, bytes memory sig1) public returns(address) {
        
        bytes32 msg0 = keccak256(abi.encode(nonce));
        bytes32 msg1 = keccak256(abi.encode(amount, nonce));

        bytes32 preMsg0 = prefixed(msg0);
        bytes32 preMsg1 = prefixed(msg1);

        address signer0 = recoverSigner(msg0, sig0);
        address signer1 = recoverSigner(msg1, sig1);
        address signer2 = recoverSigner(preMsg0, sig0);
        address signer3 = recoverSigner(preMsg1, sig1);

        
        console.log("Msg0");
        console.logBytes32(msg0);
        console.log("PreFixedMsg0");
        console.logBytes32(preMsg0);
        console.log("Msg1");
        console.logBytes32(msg1);
        console.log("PreFixedMsg1");
        console.logBytes32(preMsg1);
        console.log("Signer msg0:", signer0);
        console.log("Signer msg1:", signer1);
        console.log("Signer preMsg0:", signer2);
        console.log("Signer preMsg1:", signer3);



        return signer0;
    }   

    // Signature methods

    function splitSignature(bytes memory sig)
        internal
        view
        returns (uint8, bytes32, bytes32)
    {
        require(sig.length == 65, "B");

        bytes32 r;
        bytes32 s;
        uint8 v;

        assembly {
            // first 32 bytes, after the length prefix
            r := mload(add(sig, 32))
            // second 32 bytes
            s := mload(add(sig, 64))
            // final byte (first byte of the next 32 bytes)
            v := byte(0, mload(add(sig, 96)))
        }

        return (v, r, s);
    }

    // , uint tV, bytes32 tR, bytes32 tS
    function recoverSigner(bytes32 message, bytes memory sig) 
        internal
        view
        returns (address) 
    {
        uint8 v;
        bytes32 r;
        bytes32 s;

        (v, r, s) = splitSignature(sig);

        return ecrecover(message, v, r, s);
    }

    // Builds a prefixed hash to mimic the behavior of eth_sign.
    function prefixed(bytes32 inputHash) internal returns (bytes32) {
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message\n", inputHash));
    }

Test:

it("Test Sign", async function () {

    accounts = await hre.ethers.getSigners();
    deployer = accounts[1]

    const SignTest = await ethers.getContractFactory("SignTest");
    const iSignTest = await SignTest.connect(deployer).deploy();
    await iSignTest.deployed();


    const strInput = "Test"// 
    console.log("test_String: ", strInput)

    const amount = ethers.BigNumber.from("1")
    console.log("test_amount: ", amount)



    const message0 = utils.keccak256(
      utils.defaultAbiCoder.encode(["string"], [strInput])
    )

    const message1 = utils.keccak256(
      utils.defaultAbiCoder.encode(["uint256","string"], [amount,strInput])//  
    )


    const hash0 = ethers.utils.hashMessage(message0)
    const hash1 = ethers.utils.hashMessage(message1)


    var sig0 = await deployer.signMessage(message0);
    var sig1 = await deployer.signMessage(message1);

    console.log("test_message0: ", message0)
    console.log("test_message1: ", message1)
    console.log("test_premessage0: ", hash0)
    console.log("test_premessage1: ", hash1)
    console.log("test_sig0: ", sig0)
    console.log("test_sig1: ", sig1)


    var toProve = await iSignTest.test(amount, strInput, sig0, sig1)
    console.log("Signer: ", deployer.address)

    const breakdown = utils.splitSignature(sig0)
    // console.log("Sig breakdown: ", utils.splitSignature(sig))

  });

Output:

SignTest
test_String:  Test
test_amount:  BigNumber { value: "1" }
test_message0:  0x931dbaf3028ef6a59401824972e5ff2185985e313cf0a22def98b9627cbfb737
test_message1:  0x452d3a6616d06de775708a4503bc10b18b7478b228bb15ddda285a0d131a62ea
test_premessage0:  0x22495575f150afda8eee7b84540f45019bd0e4841fc8811bf012de6412f85a74
test_premessage1:  0x9c6a8c3810528f90169ed322e95a259a0a7d547799d9b387f64c2775ffd06456
test_sig0:  0x367a5cc42b39ef60fc4e0b71f1b009dea336c63a576c242d3266209fbcf0ea4c10d454abb8f2ddb0c591d6ee48b3a1e6774c235b936b12432edb60d5aaa6e8ba1b
test_sig1:  0xcf732e58bea22fde99220c44ce9c10a4aac47113c41308a37cfa9ea3e8f23c85091bf458a4e03fe701eca2f28038c7d2c8e05aaf5c6742fe69cf8ac6387d31091b
Msg0
0x931dbaf3028ef6a59401824972e5ff2185985e313cf0a22def98b9627cbfb737
PreFixedMsg0
0x42ccb043c0aa4b5dbd919af6b1b1bc673c7df4f072182bea7b65dbc6178409c9
Msg1
0x452d3a6616d06de775708a4503bc10b18b7478b228bb15ddda285a0d131a62ea
PreFixedMsg1
0x35037c9187aece56b893d0645c453dd2b87ae7c8293bbda0936ee7b06f374477
Signer msg0: 0x3851b9a83cdcf505acdca8432396ffdd9e843d2e
Signer msg1: 0x8697f0619e5f52d2f65f9446f90cd8d5f9c4d64c
Signer preMsg0: 0x2157884ba535951b3f4f9887059bf44b3496681d
Signer preMsg1: 0x4696442edb3f437ced855090726e645a5011e86d
Signer:  0x70997970C51812dc3A010C7d01b50e0d17dc79C8

I also tryed different prefixes like (+length):
"\x19Ethereum Signed Message:"
"\x19Ethereum Signed Message\n"
"\x19Ethereum Signed Message:\n"

Nothing works... as you can see the message is correct created and identicaly off and on chain... but when it comes to prefixing I get different values.

Any ideas?

push

Does no one have any cloue what I do wrong?

I made a little bit more readable code and more outputs... maybe that helps to understand my problem...

Test:

it("Test Sign", async function () {

    accounts = await hre.ethers.getSigners();
    deployer = accounts[1]

    const SignTest = await ethers.getContractFactory("SignTest");
    const iSignTest = await SignTest.connect(deployer).deploy();
    await iSignTest.deployed();


    const strInput = "Test"
    console.log("test_String: ", strInput)

    const amount = ethers.BigNumber.from("1")
    console.log("test_amount: ", amount)

    const message0 = utils.keccak256(
      utils.defaultAbiCoder.encode(["uint256"], [amount])
    )

    const hash0 = ethers.utils.hashMessage(message0)

    var sig0 = await deployer.signMessage(message0);

    console.log("test_message0: ", message0)
    console.log("test_hash0: ", hash0)

    var toProve = await iSignTest.test(amount, strInput, message0, sig0, hash0)

  });

Contract:

contract SignTest {

    address owner = msg.sender;

    mapping(uint256 => bool) usedNonces;

    function test(uint256 amount, string memory inpStr, bytes32 checkMsg, bytes memory sig0, bytes32 inpHash) public returns(address) {
        
        bytes32 msg0 = keccak256(abi.encodePacked(amount));

        console.log("msg");
        console.logBytes32(msg0);

        require(msg0 == checkMsg, "Wrong msg0");

        bytes32 prefixed = perfixTest(msg0);
        require(inpHash == prefixed, "Wrong prefix");

        // address signer = recoverSigner(msg0,sig0);
        // console.log("Signer: ", signer);

        return address(0);

    }   

    function perfixTest(bytes32 inputHash) internal view returns(bytes32) {
        bytes32 shouldBe = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", inputHash));

        bytes32 t0 = keccak256(abi.encodePacked("\x19Ethereum Signed Message:", inputHash));
        bytes32 t1 = keccak256(abi.encodePacked("\x19Ethereum Signed Message:32", inputHash));
        bytes32 t2 = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", inputHash));
        bytes32 t3 = keccak256(abi.encodePacked("\x19Ethereum Signed Message:32\n", inputHash));
        bytes32 t4 = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", inputHash));

        bytes32 t5 = keccak256(abi.encodePacked("\x19Ethereum Signed Message", inputHash));
        bytes32 t6 = keccak256(abi.encodePacked("\x19Ethereum Signed Message32", inputHash));
        bytes32 t7 = keccak256(abi.encodePacked("\x19Ethereum Signed Message\n32", inputHash));
        bytes32 t8 = keccak256(abi.encodePacked("\x19Ethereum Signed Message32\n", inputHash));
        bytes32 t9 = keccak256(abi.encodePacked("\x19Ethereum Signed Message\n", inputHash));

        bytes32 t10 = keccak256(abi.encodePacked("\x19", inputHash));

        console.log("shouldBe");
        console.logBytes32(shouldBe);

        console.log("t0");
        console.logBytes32(t0);

        console.log("t1");
        console.logBytes32(t1);

        console.log("t2");
        console.logBytes32(t2);

        console.log("t3");
        console.logBytes32(t3);

        console.log("t4");
        console.logBytes32(t4);

        console.log("t5");
        console.logBytes32(t5);

        console.log("t6");
        console.logBytes32(t6);

        console.log("t7");
        console.logBytes32(t7);

        console.log("t8");
        console.logBytes32(t8);

        console.log("t9");
        console.logBytes32(t9);

        console.log("t10");
        console.logBytes32(t10);

        return shouldBe;

    }
}

Output:

 SignTest
test_String:  Test
test_amount:  BigNumber { value: "1" }
test_message0:  0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
test_hash0:  0x83e1e609f6e437e4b1b0b8f55f889d630f6e370c7822706b6f01b2021b80023e
msg
0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
shouldBe
0x3774a77bfb45a273febe88cfa6a2e064f33bda3d273ffd1789c83de39d861cda
t0
0x824943dde35e6711106f370b125e24dd0575b7515156591fe2826580dadfb545
t1
0x6010d0d77e4ef38b121f73450f88d9ead35733c6466906167d9db82fd47a5a1c
t2
0x3774a77bfb45a273febe88cfa6a2e064f33bda3d273ffd1789c83de39d861cda
t3
0x8636027d0098a1d8bffb9850456a6843561d8f999eb32b6504a06e43de270826
t4
0xe4a070576f45c577b0bc5bfba5cc0f7a5e58eefb087b5ff0ca8573443f95d4ef
t5
0x96d81a91ffdf8855859535927e389489daea3b0d1a4ad194aa8bc5f48ed05570
t6
0x6f032e9e6e063263446d4850fc00d5181dadbd2ed827d7a0555b8828e52f2766
t7
0x3c1c58823ced9e929caecc6af5926d61f1cf399c55b92d4a144dfad849a67a12
t8
0x040ebab1408207f4ab4c4a17f93314e4173f2e7ceac8ec3e5817f4ec1fbc8b9a
t9
0xb6d16314cd28df003f74eb15434c464faaf56a631011e69de3b0d228f9575784
t10
0x92260e8161838b56f23e51ce7147f3b76f4865d18708f7c0dc0dd6e6f000f96b
    1) Test Sign


  0 passing (4s)
  1 failing

  1) SignTest
       Test Sign:
     Error: VM Exception while processing transaction: reverted with reason string 'Wrong prefix'

Also another goal description:

  1. A Server should create a parameter list and a sign. (I try it here with just one uint256 parameter)
  2. The Contract should check if the sign fits with the parameter list (Thats why I need to create the message in the contract again - it is not enough to check if the signer is correct for the given signiture)

Hope some one can help

Just found out by myself... Here the answer if someone stuck at the same problem:

ethers.utils.hashMessage() 

Can take a string or binary datas... from Doc(https://docs.ethers.io/v5/api/utils/hashing/):

NOTE: It is important to understand how strings and binary
data is handled differently. A string is ALWAYS processed
as the bytes of the string, so a hexstring MUST be
converted to an ArrayLike object first.

So ethers can use a string and solidtiy (as far as I unserstand) use a bytes32 array.

utils.keccak() returns a string.
So I had to cast that string to an bytes (bytes 32 in my case) with:

const messageHashBinary = ethers.utils.arrayify(message0);
const hash0 = ethers.utils.hashMessage(messageHashBinary);

In that way I get the same hash in solidity an js.

1 Like