Ethers.js: Hashing that matches Solidity `keccak256(abi.encode(...))`

Created on 24 Jan 2020  路  2Comments  路  Source: ethers-io/ethers.js

I spent a long time figuring out why I could not get matching hashes between Solidity and Ethers using the solidityKeccak256 function with dynamic types (i.e. string).

Example Solidity contract:

pragma solidity ^0.5.0;

contract TestingHashes {
    function hash() public pure returns (bytes32) {
        return keccak256(abi.encode("Hello", "world!"));
    }

    function hashPacked() public pure returns (bytes32) {
        return keccak256(abi.encodePacked("Hello", "world!"));
    }
}

In Ethers, if I use the hash function:

eth.utils.solidityKeccak256(["string", "string"], ["Hello", "world!"]);

The above matches the output of the Solidity function hashPacked. However, as the Solidity docs state, the packed encoding is ambiguous. Is there a way to mimic the behavior of abi.encode in Ethers?

discussion

Most helpful comment

Right, the packed encoding is ambiguous. For example, imagine the following two cases:

// A
const a = solidityPack([ "bytes1", "bytes2" ], [ "0x12", "0x3456" ]);

//
const b = solidityPack([ "bytes2", "bytes1" ], [ "0x1234", "0x56" ]);

Both will have the same packed representation and hence the same hash. This can allow a malicious actor to trick your contract. So in general you need to use a distinguished encoding. The abi.encode in Solidity is mirrored in ethers, so they should both produce the same, non-ambiguous data; however abi encoding is not truly distinguished, as there are multiple ways to encode the same data, it's just the most popular implementations all behave the same, so this point is moot (ish).

To get the result that matches hash(), you can use:

/home/ethers> ethers
homestead> ethers.utils.defaultAbiCoder.encode([ "string", "string" ], [ "Hello", "world" ])
'0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000548656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005776f726c64000000000000000000000000000000000000000000000000000000'
homestead> ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode([ "string", "string" ], [ "Hello", "world" ]))
'0x5ac989f52ea4c399343f6c0cf5a4810fc1bdac5773de37ca0cd0a8287f75a5c6'

I am also working on an EIP for a scheme that provides an actual distinguished encoding too, but I've been quite busy and haven't had a chance to follow up on some feedback for it. I'll bump it up my todo list. :)

Make sense? Feel free to ask follow-up questions. :)

All 2 comments

Right, the packed encoding is ambiguous. For example, imagine the following two cases:

// A
const a = solidityPack([ "bytes1", "bytes2" ], [ "0x12", "0x3456" ]);

//
const b = solidityPack([ "bytes2", "bytes1" ], [ "0x1234", "0x56" ]);

Both will have the same packed representation and hence the same hash. This can allow a malicious actor to trick your contract. So in general you need to use a distinguished encoding. The abi.encode in Solidity is mirrored in ethers, so they should both produce the same, non-ambiguous data; however abi encoding is not truly distinguished, as there are multiple ways to encode the same data, it's just the most popular implementations all behave the same, so this point is moot (ish).

To get the result that matches hash(), you can use:

/home/ethers> ethers
homestead> ethers.utils.defaultAbiCoder.encode([ "string", "string" ], [ "Hello", "world" ])
'0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000548656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005776f726c64000000000000000000000000000000000000000000000000000000'
homestead> ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode([ "string", "string" ], [ "Hello", "world" ]))
'0x5ac989f52ea4c399343f6c0cf5a4810fc1bdac5773de37ca0cd0a8287f75a5c6'

I am also working on an EIP for a scheme that provides an actual distinguished encoding too, but I've been quite busy and haven't had a chance to follow up on some feedback for it. I'll bump it up my todo list. :)

Make sense? Feel free to ask follow-up questions. :)

Ah, this makes sense, thanks for clarifying @ricmoo! I'll go ahead and close this.

Was this page helpful?
0 / 5 - 0 ratings