0

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.

1

OK, so I appear to have stumbled upon the answer. The issue I had was that I was signing the prefixed message, rather than just the hash without the prefix.

The modified test, which calls verifyMultipleInputs() on my test contract, appears as follows:

const testAddr = await web3.eth.getCoinbase();
let price = 100000000;
let contractAddr = "0x856F6BD97c2e74F1089a9e7827586a8E3447400b";

let hashedMessage = web3.utils.soliditySha3(price, contractAddr);

// Sign the hashedMessage, not a prefixedHash
const rawSig = await web3.eth.sign(hashedMessage, testAddr);
const sig = parseSignature(rawSig);

let validSig =
    await CentraDEX.methods.verifyMultipleInputs(price, contractAddr, sig.v, sig.r, sig.s).call({from: testAddr});

assert.equal(validSig, true, "expected valid sig from verifyMultipleInputs()");

The function on the contract is now returning true and the test passes. Can someone explain why I shouldn't be signing prefixed data before calling the contract?

Edit0:

geth prepends the Ethereum Signed Message internally when sign is called.

https://github.com/ethereum/go-ethereum/commit/b59c8399fbe42390a3d41e945d03b1f21c1a9b8d

  • btw, you should edit your question other than put the content in the answer – Ha ĐANG Sep 14 '18 at 4:23
0

Can you try this to make sure that the msg.sender when calling smart contract function is expected

change:

let validSig =
    await contractAuth.methods.verifyMultipleInputs(price, contractAddr, sig.v, sig.r, sig.s).call();

to:

let validSig =
    await contractAuth.methods.verifyMultipleInputs(price, contractAddr, sig.v, sig.r, sig.s).call({from: testAddr});

And the problem may come from abi.encodePacked; so these two below are difference

keccak256(abi.encodePacked(inputNumber, inputAddress)

and

web3.utils.soliditySha3(price, contractAddr);

It may give warning, but you can try to use keccak256(inputNumber, inputAddress) to confirm the problem.

Hope this will help!

  • Thank you for the quick reply, but that addition has not fixed the issue for me. It is an addition that I will keep in the tests to ensure I'm sending from that test address. – Craig Sep 13 '18 at 9:30
  • okay, note that keccak256 and sha3 is not the same hash algorithm – Ha ĐANG Sep 13 '18 at 9:42
  • I thought the point of soliditySha3() was to mimic the behaviour of keccak256()? – Craig Sep 13 '18 at 10:04
  • oh, okay! my bad. So the problem may come from the abi.encodePacked – Ha ĐANG Sep 13 '18 at 10:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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