From 7d2b9c28c6a932babb68ae42de756c8b686c19fe Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 17 Jan 2025 14:08:27 +0100 Subject: [PATCH 1/9] feat: move_ether --- cairo/ethereum/cancun/state.cairo | 29 +++++++++++++++-- cairo/ethereum/utils/numeric.cairo | 36 +++++++++++++++++++++- cairo/tests/ethereum/cancun/test_state.py | 33 +++++++++++++++++++- cairo/tests/ethereum/utils/test_numeric.py | 28 +++++++++++++++++ 4 files changed, 122 insertions(+), 4 deletions(-) diff --git a/cairo/ethereum/cancun/state.cairo b/cairo/ethereum/cancun/state.cairo index 4295defa6..9811e5fc5 100644 --- a/cairo/ethereum/cancun/state.cairo +++ b/cairo/ethereum/cancun/state.cairo @@ -2,7 +2,8 @@ from starkware.cairo.common.cairo_builtins import PoseidonBuiltin from starkware.cairo.common.dict_access import DictAccess from starkware.cairo.common.registers import get_fp_and_pc from starkware.cairo.common.math import assert_not_zero - +from starkware.cairo.common.uint256 import Uint256 +from src.utils.uint256 import uint256_add, uint256_sub from ethereum.cancun.fork_types import ( Address, Account, @@ -32,7 +33,7 @@ from ethereum.cancun.trie import ( ) from ethereum_types.bytes import Bytes, Bytes32 from ethereum_types.numeric import U256, U256Struct, Bool, bool, Uint -from ethereum.utils.numeric import is_zero +from ethereum.utils.numeric import is_zero, U256_le, U256_sub, U256_add from src.utils.dict import hashdict_read, hashdict_write, hashdict_get, dict_new_empty @@ -184,6 +185,30 @@ func set_account{poseidon_ptr: PoseidonBuiltin*, state: State}( return (); } +func move_ether{range_check_ptr, poseidon_ptr: PoseidonBuiltin*, state: State}( + sender_address: Address, recipient_address: Address, amount: U256 +) { + alloc_locals; + let fp_and_pc = get_fp_and_pc(); + local __fp__: felt* = fp_and_pc.fp_val; + + let sender_account = get_account(sender_address); + let recipient_account = get_account(recipient_address); + let sender_balance = sender_account.value.balance; + + let is_sender_balance_sufficient = U256_le(amount, sender_balance); + if (is_sender_balance_sufficient.value == 0) { + assert 0 = 1; + } + + let new_sender_account_balance = U256_sub(sender_balance, amount); + let new_recipient_account_balance = U256_add(recipient_account.value.balance, amount); + + set_account_balance(sender_address, new_sender_account_balance); + set_account_balance(recipient_address, new_recipient_account_balance); + return (); +} + func get_storage{poseidon_ptr: PoseidonBuiltin*, state: State}( address: Address, key: Bytes32 ) -> U256 { diff --git a/cairo/ethereum/utils/numeric.cairo b/cairo/ethereum/utils/numeric.cairo index 3f4403a8f..b39297e02 100644 --- a/cairo/ethereum/utils/numeric.cairo +++ b/cairo/ethereum/utils/numeric.cairo @@ -5,7 +5,8 @@ from ethereum_types.bytes import Bytes32, Bytes32Struct, Bytes20 from starkware.cairo.common.cairo_builtins import BitwiseBuiltin from starkware.cairo.common.math import split_felt -from starkware.cairo.common.uint256 import word_reverse_endian +from starkware.cairo.common.uint256 import word_reverse_endian, Uint256, uint256_le +from src.utils.uint256 import uint256_add, uint256_sub func min{range_check_ptr}(a: felt, b: felt) -> felt { alloc_locals; @@ -165,3 +166,36 @@ func U256_from_be_bytes20{range_check_ptr, bitwise_ptr: BitwiseBuiltin*}(bytes20 tempvar res = U256(new U256Struct(low=low, high=high)); return res; } + +// @dev Panics if overflow +func U256_add{range_check_ptr}(a: U256, b: U256) -> U256 { + alloc_locals; + let (res, carry) = uint256_add(cast([a.value], Uint256), cast([b.value], Uint256)); + if (carry != 0) { + with_attr error_message("OverflowError") { + assert 0 = 1; + } + } + tempvar result = U256(new U256Struct(res.low, res.high)); + return result; +} + +// @dev Panics if underflow with OverflowError +func U256_sub{range_check_ptr}(a: U256, b: U256) -> U256 { + alloc_locals; + let is_within_bounds = U256_le(b, a); + if (is_within_bounds.value == 0) { + with_attr error_message("OverflowError") { + assert 0 = 1; + } + } + let (result) = uint256_sub(cast([a.value], Uint256), cast([b.value], Uint256)); + tempvar res = U256(new U256Struct(result.low, result.high)); + return res; +} + +func U256_le{range_check_ptr}(a: U256, b: U256) -> bool { + let (result) = uint256_le(cast([a.value], Uint256), cast([b.value], Uint256)); + tempvar res = bool(result); + return res; +} diff --git a/cairo/tests/ethereum/cancun/test_state.py b/cairo/tests/ethereum/cancun/test_state.py index 15916b934..41dc7467f 100644 --- a/cairo/tests/ethereum/cancun/test_state.py +++ b/cairo/tests/ethereum/cancun/test_state.py @@ -1,3 +1,4 @@ +from copy import deepcopy from typing import Optional import pytest @@ -6,7 +7,7 @@ from hypothesis import strategies as st from hypothesis.strategies import composite -from ethereum.cancun.fork_types import Account +from ethereum.cancun.fork_types import Account, Address from ethereum.cancun.state import ( account_exists, account_exists_and_is_empty, @@ -23,6 +24,7 @@ is_account_alive, is_account_empty, mark_account_created, + move_ether, set_account, set_account_balance, set_code, @@ -31,6 +33,7 @@ touch_account, ) from tests.utils.args_gen import State, TransientStorage +from tests.utils.errors import cairo_error from tests.utils.strategies import address, bytes32, code, state, transient_storage @@ -118,6 +121,34 @@ def test_set_account(self, cairo_run, data, account: Optional[Account]): set_account(state, address, account) assert state_cairo == state + @given( + data=state_and_address_and_optional_key(), recipient_address=address, amount=... + ) + def test_move_ether( + self, cairo_run, data, recipient_address: Address, amount: U256 + ): + state, sender_address = data + # create a deep copy of state to avoid mutating the original state + python_state = deepcopy(state) + try: + move_ether(python_state, sender_address, recipient_address, amount) + except AssertionError: + with pytest.raises(AssertionError): + cairo_run( + "move_ether", state, sender_address, recipient_address, amount + ) + return + except OverflowError: + with cairo_error("OverflowError"): + cairo_run( + "move_ether", state, sender_address, recipient_address, amount + ) + return + state_cairo = cairo_run( + "move_ether", state, sender_address, recipient_address, amount + ) + assert state_cairo == python_state + @given(data=state_and_address_and_optional_key()) def test_destroy_account(self, cairo_run, data): state, address = data diff --git a/cairo/tests/ethereum/utils/test_numeric.py b/cairo/tests/ethereum/utils/test_numeric.py index 98d8c1fd1..b21c032d0 100644 --- a/cairo/tests/ethereum/utils/test_numeric.py +++ b/cairo/tests/ethereum/utils/test_numeric.py @@ -8,6 +8,7 @@ from ethereum.cancun.fork_types import Address from ethereum.cancun.vm.gas import BLOB_GASPRICE_UPDATE_FRACTION, MIN_BLOB_GASPRICE from ethereum.utils.numeric import ceil32, taylor_exponential +from tests.utils.errors import cairo_error from tests.utils.strategies import felt, uint128 pytestmark = pytest.mark.python_vm @@ -81,3 +82,30 @@ def test_U256__eq__(self, cairo_run, a: U256, b: U256): @given(address=...) def test_U256_from_be_bytes20(self, cairo_run, address: Address): assert U256.from_be_bytes(address) == cairo_run("U256_from_be_bytes20", address) + + @given(a=..., b=...) + def test_U256_le(self, cairo_run, a: U256, b: U256): + assert (a <= b) == cairo_run("U256_le", a, b) + + @given(a=..., b=...) + def test_U256_add(self, cairo_run, a: U256, b: U256): + try: + a + b + except OverflowError: + with cairo_error("OverflowError"): + cairo_result = cairo_run("U256_add", a, b) + return + cairo_result = cairo_run("U256_add", a, b) + assert cairo_result == a + b + + @given(a=..., b=...) + def test_U256_sub(self, cairo_run, a: U256, b: U256): + try: + a - b + except OverflowError: + with cairo_error("OverflowError"): + cairo_result = cairo_run("U256_sub", a, b) + return + + cairo_result = cairo_run("U256_sub", a, b) + assert cairo_result == a - b From 1f32ade62f681b89c9a5ca9f5410b172521b572a Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 17 Jan 2025 14:16:28 +0100 Subject: [PATCH 2/9] fix state deep copy --- cairo/tests/ethereum/cancun/test_state.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/cairo/tests/ethereum/cancun/test_state.py b/cairo/tests/ethereum/cancun/test_state.py index 41dc7467f..6d56144c8 100644 --- a/cairo/tests/ethereum/cancun/test_state.py +++ b/cairo/tests/ethereum/cancun/test_state.py @@ -1,4 +1,3 @@ -from copy import deepcopy from typing import Optional import pytest @@ -32,6 +31,7 @@ set_transient_storage, touch_account, ) +from ethereum.cancun.trie import copy_trie from tests.utils.args_gen import State, TransientStorage from tests.utils.errors import cairo_error from tests.utils.strategies import address, bytes32, code, state, transient_storage @@ -128,8 +128,23 @@ def test_move_ether( self, cairo_run, data, recipient_address: Address, amount: U256 ): state, sender_address = data - # create a deep copy of state to avoid mutating the original state - python_state = deepcopy(state) + # We need to create a deep copy of the state to avoid mutating the original state + # We can refactor it into a deep_copy State util if we ever need to do this again + python_state = State( + _main_trie=copy_trie(state._main_trie), + _storage_tries={ + addr: copy_trie(trie) for addr, trie in state._storage_tries.items() + }, + _snapshots=[ + ( + copy_trie(trie_tuple[0]), + {addr: copy_trie(trie) for addr, trie in trie_tuple[1].items()}, + ) + for trie_tuple in state._snapshots + ], + created_accounts=state.created_accounts, + ) + try: move_ether(python_state, sender_address, recipient_address, amount) except AssertionError: From bb460d7afa6bc5894cc084253f7ba1f6b9c70e3d Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 17 Jan 2025 14:17:54 +0100 Subject: [PATCH 3/9] refactor deep copy into a util --- cairo/tests/ethereum/cancun/test_state.py | 32 +++++++++++++---------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/cairo/tests/ethereum/cancun/test_state.py b/cairo/tests/ethereum/cancun/test_state.py index 6d56144c8..d0dcb7e06 100644 --- a/cairo/tests/ethereum/cancun/test_state.py +++ b/cairo/tests/ethereum/cancun/test_state.py @@ -37,6 +37,23 @@ from tests.utils.strategies import address, bytes32, code, state, transient_storage +def _state_deep_copy(state: State) -> State: + return State( + _main_trie=copy_trie(state._main_trie), + _storage_tries={ + addr: copy_trie(trie) for addr, trie in state._storage_tries.items() + }, + _snapshots=[ + ( + copy_trie(trie_tuple[0]), + {addr: copy_trie(trie) for addr, trie in trie_tuple[1].items()}, + ) + for trie_tuple in state._snapshots + ], + created_accounts=state.created_accounts, + ) + + @composite def state_and_address_and_optional_key( draw, state_strategy=state, address_strategy=address, key_strategy=None @@ -130,20 +147,7 @@ def test_move_ether( state, sender_address = data # We need to create a deep copy of the state to avoid mutating the original state # We can refactor it into a deep_copy State util if we ever need to do this again - python_state = State( - _main_trie=copy_trie(state._main_trie), - _storage_tries={ - addr: copy_trie(trie) for addr, trie in state._storage_tries.items() - }, - _snapshots=[ - ( - copy_trie(trie_tuple[0]), - {addr: copy_trie(trie) for addr, trie in trie_tuple[1].items()}, - ) - for trie_tuple in state._snapshots - ], - created_accounts=state.created_accounts, - ) + python_state = _state_deep_copy(state) try: move_ether(python_state, sender_address, recipient_address, amount) From 497db709fabd90cfd7bc2ca84b5db87124473546 Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 17 Jan 2025 17:44:45 +0100 Subject: [PATCH 4/9] fix pr comments; --- cairo/tests/ethereum/cancun/test_state.py | 48 +++++------------------ 1 file changed, 9 insertions(+), 39 deletions(-) diff --git a/cairo/tests/ethereum/cancun/test_state.py b/cairo/tests/ethereum/cancun/test_state.py index d0dcb7e06..793e1a081 100644 --- a/cairo/tests/ethereum/cancun/test_state.py +++ b/cairo/tests/ethereum/cancun/test_state.py @@ -31,29 +31,11 @@ set_transient_storage, touch_account, ) -from ethereum.cancun.trie import copy_trie from tests.utils.args_gen import State, TransientStorage -from tests.utils.errors import cairo_error +from tests.utils.errors import strict_raises from tests.utils.strategies import address, bytes32, code, state, transient_storage -def _state_deep_copy(state: State) -> State: - return State( - _main_trie=copy_trie(state._main_trie), - _storage_tries={ - addr: copy_trie(trie) for addr, trie in state._storage_tries.items() - }, - _snapshots=[ - ( - copy_trie(trie_tuple[0]), - {addr: copy_trie(trie) for addr, trie in trie_tuple[1].items()}, - ) - for trie_tuple in state._snapshots - ], - created_accounts=state.created_accounts, - ) - - @composite def state_and_address_and_optional_key( draw, state_strategy=state, address_strategy=address, key_strategy=None @@ -145,28 +127,16 @@ def test_move_ether( self, cairo_run, data, recipient_address: Address, amount: U256 ): state, sender_address = data - # We need to create a deep copy of the state to avoid mutating the original state - # We can refactor it into a deep_copy State util if we ever need to do this again - python_state = _state_deep_copy(state) - try: - move_ether(python_state, sender_address, recipient_address, amount) - except AssertionError: - with pytest.raises(AssertionError): - cairo_run( - "move_ether", state, sender_address, recipient_address, amount - ) - return - except OverflowError: - with cairo_error("OverflowError"): - cairo_run( - "move_ether", state, sender_address, recipient_address, amount - ) + state_cairo = cairo_run( + "move_ether", state, sender_address, recipient_address, amount + ) + except Exception as cairo_error: + with strict_raises(type(cairo_error)): + move_ether(state, sender_address, recipient_address, amount) return - state_cairo = cairo_run( - "move_ether", state, sender_address, recipient_address, amount - ) - assert state_cairo == python_state + move_ether(state, sender_address, recipient_address, amount) + assert state_cairo == state @given(data=state_and_address_and_optional_key()) def test_destroy_account(self, cairo_run, data): From f58e1ed460e28c7feb5a7efa411253c7602844c3 Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 17 Jan 2025 17:45:39 +0100 Subject: [PATCH 5/9] fix pr comments --- cairo/ethereum/cancun/state.cairo | 3 --- 1 file changed, 3 deletions(-) diff --git a/cairo/ethereum/cancun/state.cairo b/cairo/ethereum/cancun/state.cairo index 9811e5fc5..408f188ba 100644 --- a/cairo/ethereum/cancun/state.cairo +++ b/cairo/ethereum/cancun/state.cairo @@ -189,9 +189,6 @@ func move_ether{range_check_ptr, poseidon_ptr: PoseidonBuiltin*, state: State}( sender_address: Address, recipient_address: Address, amount: U256 ) { alloc_locals; - let fp_and_pc = get_fp_and_pc(); - local __fp__: felt* = fp_and_pc.fp_val; - let sender_account = get_account(sender_address); let recipient_account = get_account(recipient_address); let sender_balance = sender_account.value.balance; From a6fb1406822b08ef9c940175cc2a00eb05b65473 Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 17 Jan 2025 18:05:28 +0100 Subject: [PATCH 6/9] fix tests --- cairo/ethereum/cancun/state.cairo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cairo/ethereum/cancun/state.cairo b/cairo/ethereum/cancun/state.cairo index 408f188ba..389b0e113 100644 --- a/cairo/ethereum/cancun/state.cairo +++ b/cairo/ethereum/cancun/state.cairo @@ -190,7 +190,6 @@ func move_ether{range_check_ptr, poseidon_ptr: PoseidonBuiltin*, state: State}( ) { alloc_locals; let sender_account = get_account(sender_address); - let recipient_account = get_account(recipient_address); let sender_balance = sender_account.value.balance; let is_sender_balance_sufficient = U256_le(amount, sender_balance); @@ -199,9 +198,10 @@ func move_ether{range_check_ptr, poseidon_ptr: PoseidonBuiltin*, state: State}( } let new_sender_account_balance = U256_sub(sender_balance, amount); - let new_recipient_account_balance = U256_add(recipient_account.value.balance, amount); - set_account_balance(sender_address, new_sender_account_balance); + + let recipient_account = get_account(recipient_address); + let new_recipient_account_balance = U256_add(recipient_account.value.balance, amount); set_account_balance(recipient_address, new_recipient_account_balance); return (); } From 203db750bceb0f0fc10d8ea6110e41ba997b8637 Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Fri, 17 Jan 2025 18:08:30 +0100 Subject: [PATCH 7/9] remove cast --- cairo/ethereum/utils/numeric.cairo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cairo/ethereum/utils/numeric.cairo b/cairo/ethereum/utils/numeric.cairo index b39297e02..380ab4c28 100644 --- a/cairo/ethereum/utils/numeric.cairo +++ b/cairo/ethereum/utils/numeric.cairo @@ -170,7 +170,7 @@ func U256_from_be_bytes20{range_check_ptr, bitwise_ptr: BitwiseBuiltin*}(bytes20 // @dev Panics if overflow func U256_add{range_check_ptr}(a: U256, b: U256) -> U256 { alloc_locals; - let (res, carry) = uint256_add(cast([a.value], Uint256), cast([b.value], Uint256)); + let (res, carry) = uint256_add([a.value], [b.value]); if (carry != 0) { with_attr error_message("OverflowError") { assert 0 = 1; @@ -189,13 +189,13 @@ func U256_sub{range_check_ptr}(a: U256, b: U256) -> U256 { assert 0 = 1; } } - let (result) = uint256_sub(cast([a.value], Uint256), cast([b.value], Uint256)); + let (result) = uint256_sub([a.value], [b.value]); tempvar res = U256(new U256Struct(result.low, result.high)); return res; } func U256_le{range_check_ptr}(a: U256, b: U256) -> bool { - let (result) = uint256_le(cast([a.value], Uint256), cast([b.value], Uint256)); + let (result) = uint256_le([a.value], [b.value]); tempvar res = bool(result); return res; } From c4140c29ee4b6009c14bfab32ead71d4ebbf3676 Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Mon, 20 Jan 2025 16:16:04 +0100 Subject: [PATCH 8/9] fix comments --- cairo/ethereum/cancun/state.cairo | 4 ++-- cairo/ethereum/utils/numeric.cairo | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/cairo/ethereum/cancun/state.cairo b/cairo/ethereum/cancun/state.cairo index 389b0e113..52f29072f 100644 --- a/cairo/ethereum/cancun/state.cairo +++ b/cairo/ethereum/cancun/state.cairo @@ -193,8 +193,8 @@ func move_ether{range_check_ptr, poseidon_ptr: PoseidonBuiltin*, state: State}( let sender_balance = sender_account.value.balance; let is_sender_balance_sufficient = U256_le(amount, sender_balance); - if (is_sender_balance_sufficient.value == 0) { - assert 0 = 1; + with_attr error_message("Sender has insufficient balance") { + assert is_sender_balance_sufficient.value = 1; } let new_sender_account_balance = U256_sub(sender_balance, amount); diff --git a/cairo/ethereum/utils/numeric.cairo b/cairo/ethereum/utils/numeric.cairo index 380ab4c28..86c5a35fd 100644 --- a/cairo/ethereum/utils/numeric.cairo +++ b/cairo/ethereum/utils/numeric.cairo @@ -171,11 +171,11 @@ func U256_from_be_bytes20{range_check_ptr, bitwise_ptr: BitwiseBuiltin*}(bytes20 func U256_add{range_check_ptr}(a: U256, b: U256) -> U256 { alloc_locals; let (res, carry) = uint256_add([a.value], [b.value]); - if (carry != 0) { - with_attr error_message("OverflowError") { - assert 0 = 1; - } + + with_attr error_message("OverflowError") { + assert carry = 0; } + tempvar result = U256(new U256Struct(res.low, res.high)); return result; } @@ -184,10 +184,8 @@ func U256_add{range_check_ptr}(a: U256, b: U256) -> U256 { func U256_sub{range_check_ptr}(a: U256, b: U256) -> U256 { alloc_locals; let is_within_bounds = U256_le(b, a); - if (is_within_bounds.value == 0) { - with_attr error_message("OverflowError") { - assert 0 = 1; - } + with_attr error_message("OverflowError") { + assert is_within_bounds.value = 1; } let (result) = uint256_sub([a.value], [b.value]); tempvar res = U256(new U256Struct(result.low, result.high)); From dc533dceaac20270daf46691c0997962d8885e85 Mon Sep 17 00:00:00 2001 From: Elias Tazartes Date: Mon, 20 Jan 2025 16:31:03 +0100 Subject: [PATCH 9/9] fix error assertion --- cairo/tests/ethereum/utils/test_numeric.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cairo/tests/ethereum/utils/test_numeric.py b/cairo/tests/ethereum/utils/test_numeric.py index b21c032d0..56af7e77b 100644 --- a/cairo/tests/ethereum/utils/test_numeric.py +++ b/cairo/tests/ethereum/utils/test_numeric.py @@ -91,8 +91,8 @@ def test_U256_le(self, cairo_run, a: U256, b: U256): def test_U256_add(self, cairo_run, a: U256, b: U256): try: a + b - except OverflowError: - with cairo_error("OverflowError"): + except Exception as e: + with cairo_error(str(e.__class__.__name__)): cairo_result = cairo_run("U256_add", a, b) return cairo_result = cairo_run("U256_add", a, b) @@ -102,8 +102,8 @@ def test_U256_add(self, cairo_run, a: U256, b: U256): def test_U256_sub(self, cairo_run, a: U256, b: U256): try: a - b - except OverflowError: - with cairo_error("OverflowError"): + except Exception as e: + with cairo_error(str(e.__class__.__name__)): cairo_result = cairo_run("U256_sub", a, b) return