I have a test routine in Truffle which hashes data, signs it and passes the signature data to a contract to verify. The first test hashes a single string and its signature is successfully verified by the contract. The second hashes a number and an address. This second test fails as ecrecover() does not return msg.sender.
The contract I am testing is:
pragma solidity ^0.4.24;
contract ContractAuth {
function getPrefixedHash(bytes32 messageHash) internal pure returns (bytes32) {
bytes memory hashPrefix = "\x19Ethereum Signed Message:\n32";
return keccak256(abi.encodePacked(hashPrefix, messageHash));
}
// https://ethereum.stackexchange.com/a/15911
function verifyMessageHash(bytes32 messageHash, uint8 v, bytes32 r, bytes32 s) internal view returns (bool) {
bytes32 prefixedHash = getPrefixedHash(messageHash);
return ecrecover(prefixedHash, v, r, s) == msg.sender;
}
}
In order to keep the above functions internal, I have created a test wrapper contract:
pragma solidity ^0.4.24;
import "./ContractAuth .sol";
// TestContractAuth acts as a wrapper contract, allowing internal and
// private functions to be accessed without modifying the scope of the actual functions.
contract TestContractAuth is ContractAuth {
// Test access to ContractAuth::getPrefixedHash()
function getPrefixedHashTest(bytes32 messageHash) public pure returns (bytes32) {
return getPrefixedHash(messageHash);
}
// Test access to ContractAuth::verifyMessageHash()
function verifyMessageHashTest(bytes32 messageHash, uint8 v, bytes32 r, bytes32 s) public view returns (bool) {
return verifyMessageHash(messageHash, v, r, s);
}
function verifyMultipleInputs(uint256 inputNumber, address inputAddress, uint8 v, bytes32 r, bytes32 s) public view returns (bool) {
bytes32 messageHash = keccak256(abi.encodePacked(inputNumber, inputAddress));
return verifyMessageHash(messageHash, v, r, s);
}
}
The tests routine for verifying signed data is as follows:
it("verify signed data", async () => {
// getInstance() deploys the test contract above and returns
// an instance to it for testing
const contractAuth = getInstance("TestContractAuth");
const testAddr = await web3.eth.getCoinbase();
const msgPrefix = "\x19Ethereum Signed Message:\n32";
{
let hashedMessage = web3.utils.soliditySha3("Hello, World!");
const prefixedHash = web3.utils.soliditySha3(msgPrefix, hashedMessage);
let rawSig = await web3.eth.sign(prefixedHash, testAddr);
const sig = parseSignature(rawSig);
let validSig =
await contractAuth.methods.verifyMessageHashTest(prefixedHash, sig.v, sig.r, sig.s).call();
assert.equal(validSig, true, "Expected valid signature returned by verifyMessageHashTest()");
}
{ // Test currently fails...
// TODO: ask question on StackExchange
let price = 100000000;
let contractAddr = "0x856F6BD97c2e74F1089a9e7827586a8E3447400b";
let hashedMessage = web3.utils.soliditySha3(price, contractAddr);
const prefixedHash = web3.utils.soliditySha3(msgPrefix, hashedMessage);
const rawSig = await web3.eth.sign(prefixedHash, testAddr);
const sig = parseSignature(rawSig);
let validSig =
await contractAuth.methods.verifyMultipleInputs(price, contractAddr, sig.v, sig.r, sig.s).call();
assert.equal(validSig, true, "expected valid sig from verifyMultipleInputs()");
}
});
parseSignature() is a helper function to perform the slicing of the raw signature returned by web3.eth.sign().
The first assert.equal() call passes as the signature is successfully verified on the contract. The second fails and currently I do not know why.
My belief is that there is an issue in the format of inputNumber and inputAddress when I hash it in the client. Is there a way this data should be formatted before it is hashed?
Edit0:
In order to verify hashing, I have added the following function to my test contract:
function hashMultipleValues(uint256 inputNumber, address inputAddress) public pure returns (bytes32) {
return keccak256(abi.encodePacked(inputNumber, inputAddress));
}
Which simply returns the hashed combination of the two inputs.
I have added the following to a test in order to verify hashes side-by-side:
let hashedMessage = web3.utils.soliditySha3(price, contractAddr);
let solHashedMessage =
await contractAuth.methods.hashMultipleValues(price, contractAddr).call();
console.log("Solidity:\t" + solHashedMessage);
console.log("Web3:\t\t" + hashedMessage);
With the given input values above, I get the following output in the console during the test:
Solidity: 0xdce71017994de76c5339d7b083bcbe948e6e75786de1faeb971c2cb369913535
Web3: 0xdce71017994de76c5339d7b083bcbe948e6e75786de1faeb971c2cb369913535
This proves that there's parity between the methods of hashing that I'm using.