Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement 0x20 - SHA3 Opcode #281

Merged
merged 31 commits into from
Sep 25, 2023
Merged

feat: implement 0x20 - SHA3 Opcode #281

merged 31 commits into from
Sep 25, 2023

Conversation

Quentash
Copy link
Contributor

@Quentash Quentash commented Sep 5, 2023

Implementation of SHA3 opcode (0x20).
The code has been run against official ethereum tests found at https://github.com/ethereum/tests/blob/develop/src/GeneralStateTestsFiller/VMTests/vmTests/sha3Filler.yml#L52

Only a few tests couldn't be run because the actual memory.bytes_len is a u32 and some tested value are u64, It has been discussed with @Eikix to leave it as it is for now and eventually refactor it later to handle u64 memory size.

I have added comments in the code ( to eventually remove later ) to explain how the code bypasses continuous memory allocation when trying to access a memory slot beyond memory.bytes_len and will instead ensure that the memory is allocated at the end of the process, this seems way more efficient since some tests will fail otherwise.

Pull Request type

Please check the type of change your PR introduces:

  • Bugfix
  • Feature
  • Code style update (formatting, renaming)
  • Refactoring (no functional changes, no API changes)
  • Build-related changes
  • Documentation content changes
  • Other (please describe):

What is the current behavior?

Resolves: #115

What is the new behavior?

Does this introduce a breaking change?

  • Yes
  • No

@github-actions
Copy link

github-actions bot commented Sep 5, 2023

Snapshot Comparison Report:

No changes in gas consumption.

crates/evm/src/instructions/sha3.cairo Outdated Show resolved Hide resolved
crates/evm/src/instructions/sha3.cairo Outdated Show resolved Hide resolved
crates/evm/src/instructions/sha3.cairo Outdated Show resolved Hide resolved
crates/evm/src/instructions/sha3.cairo Outdated Show resolved Hide resolved
crates/evm/src/instructions/sha3.cairo Outdated Show resolved Hide resolved
crates/evm/src/instructions/sha3.cairo Outdated Show resolved Hide resolved
crates/evm/src/instructions/sha3.cairo Outdated Show resolved Hide resolved
crates/evm/src/instructions/sha3.cairo Outdated Show resolved Hide resolved
crates/utils/src/helpers.cairo Outdated Show resolved Hide resolved
crates/utils/src/helpers.cairo Outdated Show resolved Hide resolved
@Quentash
Copy link
Contributor Author

Quentash commented Sep 8, 2023

Used Default::default() as array initiator.
Corrected various comment and added some.
Added SHA3 rustdoc-like comment. ( Please tell me if this one is explicit enough ).
Added slot_to_read variable which made me realize I could dispose the counter one.
Replaced sneaky camels by snakes.

Copy link
Collaborator

@enitrat enitrat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's some modifications I would like to see:

  • Don't explicitly call memory.ensure_length in a method that computes sha3. This is extremely dangerous and bug-prone. "internal" memory functions are meant to use internally, use public methods instead, like load.
  • Instead of looping and padding in each iteration when the offset read is bigger than the (initial) memory size, we can use modulo operations to compute how many zeroes must be padded, and then create a pad_left function (similar to pad_right) to add the required padding in one place
  • Reverting a word's endianness, and storing them in the array to hash should be in a utility function that doesn't take memory as parameter
  • Handling of the last input should be in a standalone function.
  • Ideally we would first load all words, then apply transformations and hash them. In Cairo this might end up being more expensive, so we can do it progressively as we prepare the array of elements to be hashed

@Quentash
Copy link
Contributor Author

I have pushed a new version of the sha3 algorithm hoping it is clearer and better structured. ( There is a significant improvement in gas cost concerning big sized test 6993735300 -> 1703355056 for the bigger one. )
I still have tests to add but I pushed this commit to allow you to check on Memory.load().

Indeed, it appears that the memory expansion gas cost calculation makes one of my test fail with what firstly appears to be an infinite loop that ends up with a error: process did not exit successfully: signal: 9 (SIGKILL) message.
If I comment those lines, my tests appear to run normally. "Funny" thing to note, this is not even the "biggest" test that fails, see screenshots.

With comments :
image

Without comments :
image

Copy link
Collaborator

@enitrat enitrat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is still some refactoring to be done on how the function is constructed internally, otherwise it's a good work!

Here's an example of a keccak256 function taking an array of bytes as an input. As you can see it's around 12 lines long, and the intent by each line can be directly understood, even without having functions implementations available. This is ideally what you should aim for when writing code

Of course it's not applicable to every situation, but let's try to do our best

fn cairo_keccak_le_bytes(mut self: Array<u8>) -> u256 {
    let mut words64: Array<u64> = array![];
    let mut self = self.span();
    let (last_word, last_word_bytes) = loop {
        // Specifically handle last word
        if self.len() < 8 {
            let (value, n_bytes) = U64Trait::from_le_bytes(self);
            break (value, n_bytes);
        };
        let mut current_word = self.slice(0, 8);
        let (value, n_bytes) = U64Trait::from_le_bytes(current_word);
        words64.append(value);
        self = self.slice(8, self.len() - 8);
    };
    let mut hash = cairo_keccak(ref words64, last_word, last_word_bytes);
    reverse_endianness(hash)
}


fn reverse_endianness(value: u256) -> u256 {
    let new_low = integer::u128_byte_reverse(value.high);
    let new_high = integer::u128_byte_reverse(value.low);
    u256 { low: new_low, high: new_high }
}

crates/evm/src/instructions/sha3.cairo Outdated Show resolved Hide resolved
crates/evm/src/instructions/sha3.cairo Outdated Show resolved Hide resolved
crates/evm/src/instructions/sha3.cairo Outdated Show resolved Hide resolved
crates/utils/src/helpers.cairo Outdated Show resolved Hide resolved
crates/utils/src/helpers.cairo Outdated Show resolved Hide resolved
crates/utils/src/helpers.cairo Outdated Show resolved Hide resolved
crates/utils/src/helpers.cairo Outdated Show resolved Hide resolved
crates/evm/src/instructions/sha3.cairo Outdated Show resolved Hide resolved
@enitrat
Copy link
Collaborator

enitrat commented Sep 13, 2023

I have pushed a new version of the sha3 algorithm hoping it is clearer and better structured. ( There is a significant improvement in gas cost concerning big sized test 6993735300 -> 1703355056 for the bigger one. ) I still have tests to add but I pushed this commit to allow you to check on Memory.load().

Indeed, it appears that the memory expansion gas cost calculation makes one of my test fail with what firstly appears to be an infinite loop that ends up with a error: process did not exit successfully: signal: 9 (SIGKILL) message. If I comment those lines, my tests appear to run normally. "Funny" thing to note, this is not even the "biggest" test that fails, see screenshots.

With comments : image

Without comments : image

I tried locally and I didn't have this problem!

@Quentash
Copy link
Contributor Author

I have pushed a new version of the sha3 algorithm hoping it is clearer and better structured. ( There is a significant improvement in gas cost concerning big sized test 6993735300 -> 1703355056 for the bigger one. ) I still have tests to add but I pushed this commit to allow you to check on Memory.load().
Indeed, it appears that the memory expansion gas cost calculation makes one of my test fail with what firstly appears to be an infinite loop that ends up with a error: process did not exit successfully: signal: 9 (SIGKILL) message. If I comment those lines, my tests appear to run normally. "Funny" thing to note, this is not even the "biggest" test that fails, see screenshots.
With comments : image
Without comments : image

I tried locally and I didn't have this problem!

Ok that is even more weird. I tried it again and it gives me the same results.

Here is my scarb --version output :
scarb 0.7.0 (58cc88efb 2023-08-23)
cairo: 2.2.0 (https://crates.io/crates/cairo-lang-compiler/2.2.0)
sierra: 1.3.0
In case yours differ ?

Otherwise, could it be because I'm on WSL ?
I finish pushing the required corrections and I will try to run the tests on a native Linux.

Would you have any other ideas ?

@enitrat
Copy link
Collaborator

enitrat commented Sep 14, 2023

Otherwise, could it be because I'm on WSL ?

There's a chance it's because of this. I'm running on macOS

@enitrat
Copy link
Collaborator

enitrat commented Sep 18, 2023

@ClementWalter @Eikix good for a final review

Eikix
Eikix previously approved these changes Sep 21, 2023
Copy link
Member

@Eikix Eikix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as far as i'm concerned this PR is ready
did we make sure:

  • we have all the tests that geth uses?
  • all the tests that EELS uses?

truly nice display of patience and good cooperation, thank you for your hard work @Quentash @enitrat

@Quentash
Copy link
Contributor Author

as far as i'm concerned this PR is ready did we make sure:

  • we have all the tests that geth uses?
  • all the tests that EELS uses?

truly nice display of patience and good cooperation, thank you for your hard work @Quentash @enitrat

I have taken tests found here : https://github.com/ethereum/go-ethereum/blob/master/tests/solidity/test/opCodes.js
which are quite similiar to those found here : https://github.com/ethereum/legacytests/tree/97de5bce3a257a4b219666134df15b6d4d47f1f5/Constantinople/VMTests/vmSha3Test

Beside tests with size or offset > u64.maxvalue, I've taken all I could, some are even e bit repetitve and I'm not sure they are really relevant in our case but I still added them.
I'm open to suggestions !

@Quentash
Copy link
Contributor Author

Ensure memory length to be set even if we skipped reading for zeroes.

@enitrat
Copy link
Collaborator

enitrat commented Sep 22, 2023

@Quentash can you add a test that checks the memory length when we read out of bound bytes? To make sure that the length is correctly updated.

@Quentash
Copy link
Contributor Author

@Quentash can you add a test that checks the memory length when we read out of bound bytes? To make sure that the length is correctly updated.

Of course, my bad !!

@Quentash
Copy link
Contributor Author

Added memory length verification to tests.
Made me realise it was better to ensure_length outside of the "if", thank you !

Copy link
Member

@Eikix Eikix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the long awaited moment... 🔥🔥🔥🔥🔥

@Eikix Eikix added this pull request to the merge queue Sep 25, 2023
Merged via the queue into kkrt-labs:main with commit 597d753 Sep 25, 2023
2 checks passed
@enitrat
Copy link
Collaborator

enitrat commented Sep 25, 2023

🥳

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat: implement 0x20 - SHA3 Opcode
3 participants