2

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)
    public
    pure 
    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)
    public
    view
    returns (bytes32)
{
    return keccak256(abi.encodePacked(
        bytes4(0xe18546ee),
        address(this),
        userNonce[user], // sequential nonce
        _operator
    ));
}

function prove_me (bytes32 message, bytes32 r, bytes32 s, uint8 v, address _operator)
    public
{
    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

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);
    console.log(signature2);
    console.log(signature1.slice(0, -2) == signature2.slice(0 ,-2));
}

test();

Got this:

0x22df4202a49cb5c33d1d815c6c95ef9bc1eca34f1343806970a20168d5c49db25cebcc3bb85faaf894101e323e5af298f96983a4a8acdbc21d50ee72d2141ceb1c
0x22df4202a49cb5c33d1d815c6c95ef9bc1eca34f1343806970a20168d5c49db25cebcc3bb85faaf894101e323e5af298f96983a4a8acdbc21d50ee72d2141ceb01
true

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
}
3

Your Answer

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

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