21.4.2023

Keccak hash - Cairo vs. Solidity

Let’s talk about Keccak hash and how to make Cairo's Keccak hash match Solidity’s Keccak (and vice-versa).

Hello frens!👋

Today, we are going to present an interesting problem we recently had some fun resolving: How to make Cairo's Keccak hash match Solidity’s Keccak and vice-versa.

💡 To learn more about Keccak hashing in Solidity check out this post!

A small teaser:

Cairo lang’s Keccak doesn’t need to have something like encodePacked for arguments, unlike Solidity. You can directly pass the arguments to the function! Also one very important point - in Cairo every element is 32 bytes long! So sometimes you will need to do “right padding”, or move bytes to be able to reproduce the same hash as Solidity!

> You can also go and check out our repo with many more examples!

NOTE: this article was written when Cairo 0 was the only option to use! We will revisit this article once Cairo 1 is mature enough!

Simple example

The simplest case is when you have only 32-byte types on the Solidity side, a type like uint256. Then you don’t have to move any bytes or do the right pad. We can just use cairo_keccak_uint256s_bigend and that’s it!

On Solidity side we have:


hash = keccak256(abi.encodePacked(a_uint256, b_uint256));

Some of the functions for Keccak hashing (provided by Starkware here):

  • cairo_keccak_uint256s
  • cairo_keccak_uint256s_bigend
  • cairo_keccak_felts
  • ...

So, in Cairo, first you will need to create an Uint256 array to store the 2 arguments you want to pass to the function:


alloc_locals;
let (local keccak_ptr: felt*) = alloc();
let keccak_ptr_start = keccak_ptr;
let (data_uint: Uint256*) = alloc();
assert data_uint[0] = a_uint256;
assert data_uint[1] = b_uint256;

After creating the array, you can pass it to the cairo_keccak_uint256s_bigend function:


# run as many keccaks as you want (or verify_eth_signature), pass keccak_ptr implicitly 
let (hash : Uint256) = cairo_keccak_uint256s_bigend{keccak_ptr=keccak_ptr}(n_elements=2, elements=data_uint)
# call finalize once at the end
finalize_keccak(keccak_ptr_start=keccak_ptr_start, keccak_ptr_end=keccak_ptr)

Don’t forget to call finalize_keccak at the end, otherwise you’re basically getting the results from hints without verifying them.

That was pretty straightforward, wasn’t it?

Example 2

But what if we have an element which is less than 32 bytes (on the Solidity side)? Then we will have to play a bit with it on the Cairo side…

In this first example we just have one element smaller than 32 bytes and it’s the last one. So, something like this in solidity:


hash = keccak256 (abi.encodePacked(uint256, address));

The breakdown:

Again, Uint256 is 32 bytes and address is 20 bytes in Solidity. So we can say that we have a total of 52 bytes (32 + 20).

But as we already said before, in Cairo we must use an array of Uint256 - so every element of the array will be 32 bytes!

If you have a number, for example 1, the byte representation of this number will be 0000000000000000000000000000000000000000000000000000000000000001

So if we send n_bytes= 52 to our cairo_keccak_bigend function, it will take the 32 bytes of the first element of our array and then the 20 bytes of the second element.

But here, our last element is a 20-byte address, for example 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266… But on Cairo, this address will be converted implicitly to 32 bytes so it will look like this: 0x00000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266

Do you see the problem?

If we take the first 20 bytes of this address, we won’t have our entire address, just this part: 0x00000000000000000000000f39fd6e51aad88f6

That is why we need to move our bytes, the 0-s that are in front of our address to the end of it!

The result will be like this: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000

Now, if we take the first 20 bytes, our entire address is 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266

You can take a look at the code of our dear friends from Snapshot to see how to do the right padding in Cairo here!

In our case, we can do something like:


// Do right padding if address len is less than 32 
let (padded_address) = pad_right(address, 20);

Full example:


// Do right padding if address len is less than 32
let (padded_address) = pad_right(address, 20);
let (data_uint: Uint256*) = alloc();
assert data_uint[0] = value_uint256; assert data_uint[1] = padded_address; let (signable_bytes) = alloc();
let signable_bytes_start = signable_bytes;
keccak_add_uint256s{inputs=signable_bytes}(n_elements=2, elements=data_uint,    bigend=TRUE);
// Compute the hash
let (hash) = keccak_bigend{keccak_ptr=keccak_ptr}(
inputs=signable_bytes_start, n_bytes=32 + 20);

Example 3

Now, another complication!

We will take the same example with Uint256 and address in Solidity, but we will change their order:


hash = keccak256(
abi.encodePacked(address, uint256)
);

On Ethereum side, this is what we get as output of abi.encodePacked(address, uint256);

0xf39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000

0000000000000000000000000000001

And we need to match this on the Cairo side!

Remember, we need to tell the cairo_keccak_bigend that the number of bytes is 52.

In this case, it will take all 32 bytes of our first element! But our first element is 20 bytes in Solidity 🙀 Unfortunately it cannot take 20 and then 32… It will automatically take 32 bytes by 32 bytes - and that is why only the last element can be less than 32 bytes.

Our address in Cairo is 0x00000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266 and cairo_keccak_bigend will take the whole 32 bytes. If you compare it to the Ethereum result, you will see that these 0-s in front will cause us problems!

The number 1 we sent also looks like this: 0000000000000000000000000000000000000000000000000000000000000001 and when our  cairo_keccak_bigend function takes the remaining 20 bytes, it’s just 0-s!

What we will have to do here first, is move the first 12 bytes from our second element to the end of our first element.

If we do so, our first element will be

0xf39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000 and our second one will be 000000000000000000000000000000000000000000000000000000000000001

Is it done now? Well not yet!! We will take the first 20 bytes of our second element so again just 0-s!

Now we can do the right padding!

So we will move 0-s from start to the end and get this result 000000000000000000000000000000000000001000000000000000000000000

Now If you take 32 bytes from the first element and 20 bytes from the second one we will finally get the good result: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000000000

000000000000000000000000000000001

🥳

You can take a look at our repo for the code on how to achieve this! We are using some useful functions created by our friends from the Kakarot project (kudos to them!) so also be sure to take a look before moving forward!

Example 4

Now for the last example, let's use uint8 on the Solidity side with two numbers, for example a= 2 and b = 5.

The output of keccak256(abi.encodePacked(uint8,uint8)); will be 0x0205.

Remember on Cairo our values are 32 bytes long so we would have

a = 0x0000000000000000000000000000000000000000000000000000000000000002

and

b = 0x0000000000000000000000000000000000000000000000000000000000000005

And we want 0x0205.

For that we are going to do the right pad on b to make it look like this b=0x0500000000000000000000000000000000000000000000000000000000000000

Then we are going to move the whole b into a, so we will get this: 0x020500000000000000000000000000000000000000000000000000000000000

The difference between our previous example is that in this case, b is now unusable, everything is in a  so our array will contain only one element!


let (data_uint: Uint256*) = alloc();
assert data_uint[0] = a_uint8;
let (signable_bytes) = alloc();
let signable_bytes_start = signable_bytes;
keccak_add_uint256s{inputs=signable_bytes}(n_elements=1, elements=data_uint, bigend=TRUE);
// Compute the hash
let (hash) = keccak_bigend{keccak_ptr=keccak_ptr}(inputs=signable_bytes_start, n_bytes=2);
finalize_keccak(keccak_ptr_start=keccak_ptr_start, keccak_ptr_end=keccak_ptr);

And we are done!!

For more information take a look at these links:

Thanks again to our Snapshot and Kakarot frens and thank you dear anon for reading - we hope it will be useful! And until next time, keep Starknet strange!

‍‍

* This article is also available on SpaceShard Medium.

Downlaod all images