Why ecrecover call in my Solidity code failed to resolve the signer?

ecrecover() is a very useful Solidity function that allows the smart contract to validate that incoming data is properly signed by an expected party. The signing must done according to the technique described here: https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign. The resulting signature can then be successfully validated by Solidity with ecrecover().

A sample Solidity code to do the validation looks like the following:

  function verify(bytes32  documentHash, uint8[] memory sigV, bytes32[] memory sigR, bytes32[] memory sigS) public {
    bytes memory prefix = "\x19Ethereum Signed Message:\n32";
    bytes32 prefixedProof = keccak256(abi.encodePacked(prefix, documentHash));
    address recovered = ecrecover(prefixedProof, sigV[0], sigR[0], sigS[0]);
    emit LogSignature(documentHash, prefixedProof, recovered);
  }

The recovered value should be the account address of the Ethereum signer. However sometimes this fails and the value is returned as "0x0000000000000000000000000000000000000000".

The most likely cause of the failure to recover the signer is due to the difference in how different Ethereum implementations returns the v portion of the signature. For a simple explanation of the r, s and v portions of an Ethereum signature, visit this Solidity documentation page. Different Ethereum implementations returns different v values from the eth_sign call. This can catch developers by surprise and can result in the error above.

Basically Solidity expects the v value to be either 27 or 28, as explained here. Other values are also possible when signing transactions using EIP 155 schema. But currently the eth_sign implementations do not support EIP 155 signing.

Given the above, the client program that calls the Solidity function must supply a v value of either 27 or 28. However, when calling the target Ethereum node to sign, different v values may be returned.

testrpc and Ganache, both popular with development environments, return 00 or 01 for the V value portion of the signature. So 27 must be added in order to calculate the correct value.

Geth/Quorum, on the other hand, both return the V value according to the ecrecover technique, with 27 already added.

Protocol eth.sign() supported? To calculate V value from returned signature
Geth Yes web3.utils.toDecimal("0x" + sig.slice(130, 132))
Quorum Yes web3.utils.toDecimal("0x" + sig.slice(130, 132))
Besu No
testrpc Yes web3.utils.toDecimal("0x" + sig.slice(130, 132)) + 27
Ganache Yes web3.utils.toDecimal("0x" + sig.slice(130, 132)) + 27

A client program must compensate for this difference in order to get the proper v value. Here's a sample truffle project that demonstrates the difference and what client programs must to do always pass in the proper v value.