I have a smart contract that provides a message to sign and then validates the signer of the message. Something like:

   mapping(address => uint) public userNonce;

function getSigner(bytes32 message, bytes32 r, bytes32 s, uint8 v)
    returns (address)
    bytes32 prefixedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", message));
    address signer = ecrecover(prefixedHash,v,r,s);
    return signer;

function getMessageToSign(address user, address _operator)
    returns (bytes32)
    return keccak256(abi.encodePacked(
        userNonce[user], // sequential nonce

function prove_me (bytes32 message, bytes32 r, bytes32 s, uint8 v, address _operator)
    address signer = getSigner(message, r, s, v);
    bytes32 proof = getMessageToSign(signer, _operator);
    require(proof == message);
    userNonce[signer] = userNonce[signer] + 1;
    // do something on behalf of user
    hasUserAllowed[signer] = true;

Testing above with dummy private-key and address.

I get a message to sign from smart contract :

dataHash = "0x3c6819779a3c0a163d93e2e1e1168fb5ab424217555132ea356d491002c84e7f"

user1Addr = "0x72f27f62998bc1a2929a72bf753b6a335abd46c0"

user1PvtKey = "0xc73c6b7ad8e821a303871e8a63f8a53c9b6b0cf3977becd94032f3ff7c3055c1"

I sign it using private key belonging to user1:

web3.eth.accounts.sign(dataHash, user1PvtKey);

But when I pass corresponding r,s,v to the getSigner method, I get a different signer (0x2ad05a181F51975C1bc79b6B1eC1Ab9A86a3fE24)

I know this works if I use web3.sign but that requires an unlocking account with personal however I prefer signing using the private key.

The signature is different using web3.sign than web3.eth.sign.

PS: I know that web3 updated the signing method including chain code as well. But since I can not update my smart contract, how can I sign the message using a private key and smart contract verifies the correct owner?

1 Answer 1


Executed this:

const Web3 = require("web3");
const web3 = new Web3("http://localhost:7545");

const dataHash    = "0x3c6819779a3c0a163d93e2e1e1168fb5ab424217555132ea356d491002c84e7f";
const user1Addr   = "0x72f27f62998bc1a2929a72bf753b6a335abd46c0";
const user1PvtKey = "0xc73c6b7ad8e821a303871e8a63f8a53c9b6b0cf3977becd94032f3ff7c3055c1";

async function test() {
    const signature1 = web3.eth.accounts.sign(dataHash, user1PvtKey).signature;
    const signature2 = await web3.eth.sign(dataHash, user1Addr);
    console.log(signature1.slice(0, -2) == signature2.slice(0 ,-2));


Got this:


Conclusion: You need to exclude the last byte (two hexadecimal characters) in the output.

Details: That last byte is the v part in the message, which can be either 0 / 27 or 1 / 28.

In your case, it is 28 when calling web3.eth.accounts.sign and 1 when calling web3.eth.sign.

I believe that the reason for this difference is related to a bug in Geth implementation.

See package ethereumjs-util / function fromRpcSig for more details.

Alternatively to ignoring that last byte, you can take the same approach used in the function linked above, and add 27 to that byte if it's smaller than 27:

if (v < 27) {
    v += 27

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.