diff --git a/src/backend/starknet.cairo b/src/backend/starknet.cairo index 27cdd35fd..787db152a 100644 --- a/src/backend/starknet.cairo +++ b/src/backend/starknet.cairo @@ -19,7 +19,7 @@ from starkware.starknet.common.syscalls import ( ) from kakarot.account import Account -from kakarot.precompiles.precompiles import Precompiles +from kakarot.precompiles.precompiles_helpers import PrecompilesHelpers from kakarot.constants import Constants from kakarot.interfaces.interfaces import IERC20, IAccount @@ -172,7 +172,7 @@ namespace Internals { }(self: model.Account*, native_token_address) { alloc_locals; - let is_precompile = Precompiles.is_precompile(self.address.evm); + let is_precompile = PrecompilesHelpers.is_precompile(self.address.evm); if (is_precompile != FALSE) { return (); } @@ -183,42 +183,31 @@ namespace Internals { if (starknet_account_exists == 0) { // Deploy account Starknet.deploy(self.address.evm); - - let has_code_or_nonce = Account.has_code_or_nonce(self); - if (has_code_or_nonce == FALSE) { - // Nothing to commit - return (); - } - - // If SELFDESTRUCT, leave the account empty after deploying it - including - // burning any leftover balance. - if (self.selfdestruct != 0) { - let starknet_address = Account.compute_starknet_address(Constants.BURN_ADDRESS); - tempvar burn_address = new model.Address( - starknet=starknet_address, evm=Constants.BURN_ADDRESS - ); - let transfer = model.Transfer(self.address, burn_address, [self.balance]); - State.add_transfer(transfer); - return (); - } - - // Write bytecode - IAccount.write_bytecode(starknet_address, self.code_len, self.code); - // Set nonce - IAccount.set_nonce(starknet_address, self.nonce); - // Save storages - _save_storage(starknet_address, self.storage_start, self.storage); - - // Save valid jumpdests - Internals._save_valid_jumpdests( - starknet_address, self.valid_jumpdests_start, self.valid_jumpdests - ); - return (); + tempvar syscall_ptr = syscall_ptr; + tempvar pedersen_ptr = pedersen_ptr; + tempvar range_check_ptr = range_check_ptr; + } else { + tempvar syscall_ptr = syscall_ptr; + tempvar pedersen_ptr = pedersen_ptr; + tempvar range_check_ptr = range_check_ptr; } // @dev: EIP-6780 - If selfdestruct on an account created, dont commit data + // and burn any leftover balance. let is_created_selfdestructed = self.created * self.selfdestruct; if (is_created_selfdestructed != 0) { + let starknet_address = Account.compute_starknet_address(Constants.BURN_ADDRESS); + tempvar burn_address = new model.Address( + starknet=starknet_address, evm=Constants.BURN_ADDRESS + ); + let transfer = model.Transfer(self.address, burn_address, [self.balance]); + State.add_transfer(transfer); + return (); + } + + let has_code_or_nonce = Account.has_code_or_nonce(self); + if (has_code_or_nonce == FALSE) { + // Nothing to commit return (); } @@ -227,7 +216,7 @@ namespace Internals { // Save storages Internals._save_storage(starknet_address, self.storage_start, self.storage); - // Update bytecode and jumpdests if required (SELFDESTRUCTed contract, redeployed) + // Update bytecode and jumpdests if required (newly created account) if (self.created != FALSE) { IAccount.write_bytecode(starknet_address, self.code_len, self.code); Internals._save_valid_jumpdests( diff --git a/src/kakarot/interpreter.cairo b/src/kakarot/interpreter.cairo index f2a58601d..b2910f662 100644 --- a/src/kakarot/interpreter.cairo +++ b/src/kakarot/interpreter.cairo @@ -33,6 +33,7 @@ from kakarot.instructions.system_operations import CallHelper, CreateHelper, Sys from kakarot.memory import Memory from kakarot.model import model from kakarot.precompiles.precompiles import Precompiles +from kakarot.precompiles.precompiles_helpers import PrecompilesHelpers from kakarot.stack import Stack from kakarot.state import State from kakarot.gas import Gas @@ -64,7 +65,7 @@ namespace Interpreter { let pc = evm.program_counter; let is_pc_ge_code_len = is_le(evm.message.bytecode_len, pc); if (is_pc_ge_code_len != FALSE) { - let is_precompile = Precompiles.is_precompile(evm.message.code_address.evm); + let is_precompile = PrecompilesHelpers.is_precompile(evm.message.code_address.evm); if (is_precompile != FALSE) { let parent_context = evm.message.parent; let is_parent_zero = Helpers.is_zero(cast(parent_context, felt)); diff --git a/src/kakarot/precompiles/kakarot_precompiles.cairo b/src/kakarot/precompiles/kakarot_precompiles.cairo index 163571fb6..744d981dd 100644 --- a/src/kakarot/precompiles/kakarot_precompiles.cairo +++ b/src/kakarot/precompiles/kakarot_precompiles.cairo @@ -13,6 +13,7 @@ from kakarot.interfaces.interfaces import IAccount from kakarot.account import Account from kakarot.storages import Kakarot_authorized_cairo_precompiles_callers from utils.utils import Helpers +from backend.starknet import Starknet const CALL_CONTRACT_SOLIDITY_SELECTOR = 0xb3eb2c1b; const LIBRARY_CALL_SOLIDITY_SELECTOR = 0x5a9af197; @@ -82,11 +83,19 @@ namespace KakarotPrecompiles { let is_not_deployed = Helpers.is_zero(caller_starknet_address); if (is_not_deployed != FALSE) { - let (revert_reason_len, revert_reason) = Errors.accountNotDeployed(); - return ( - revert_reason_len, revert_reason, CAIRO_PRECOMPILE_GAS, Errors.EXCEPTIONAL_HALT - ); + // Deploy account - + // order of returned values in memory matches the explicit ones in the other branch + Starknet.deploy(caller_address); + } else { + tempvar syscall_ptr = syscall_ptr; + tempvar pedersen_ptr = pedersen_ptr; + tempvar range_check_ptr = range_check_ptr; + tempvar caller_starknet_address = caller_starknet_address; } + let syscall_ptr = cast([ap - 4], felt*); + let pedersen_ptr = cast([ap - 3], HashBuiltin*); + let range_check_ptr = [ap - 2]; + let caller_starknet_address = [ap - 1]; let (retdata_len, retdata, success) = IAccount.execute_starknet_call( caller_starknet_address, to_starknet_address, starknet_selector, data_len, data diff --git a/src/kakarot/precompiles/precompiles.cairo b/src/kakarot/precompiles/precompiles.cairo index 3a6c9ee58..c06466ccb 100644 --- a/src/kakarot/precompiles/precompiles.cairo +++ b/src/kakarot/precompiles/precompiles.cairo @@ -17,44 +17,16 @@ from kakarot.precompiles.ec_recover import PrecompileEcRecover from kakarot.precompiles.p256verify import PrecompileP256Verify from kakarot.precompiles.ripemd160 import PrecompileRIPEMD160 from kakarot.precompiles.sha256 import PrecompileSHA256 +from kakarot.precompiles.precompiles_helpers import ( + PrecompilesHelpers, + LAST_ETHEREUM_PRECOMPILE_ADDRESS, + FIRST_ROLLUP_PRECOMPILE_ADDRESS, + FIRST_KAKAROT_PRECOMPILE_ADDRESS, +) from utils.utils import Helpers -const LAST_ETHEREUM_PRECOMPILE_ADDRESS = 0x0a; -const FIRST_ROLLUP_PRECOMPILE_ADDRESS = 0x100; -const LAST_ROLLUP_PRECOMPILE_ADDRESS = 0x100; -const EXEC_PRECOMPILE_SELECTOR = 0x01e3e7ac032066525c37d0791c3c0f5fbb1c17f1cb6fe00afc206faa3fbd18e1; -const FIRST_KAKAROT_PRECOMPILE_ADDRESS = 0x75001; -const LAST_KAKAROT_PRECOMPILE_ADDRESS = 0x75002; - // @title Precompile related functions. namespace Precompiles { - // @notice Return whether the address is a precompile address. - // @dev Ethereum precompiles start at address 0x01. - // @dev RIP precompiles start at address FIRST_ROLLUP_PRECOMPILE_ADDRESS. - // @dev Kakarot precompiles start at address FIRST_KAKAROT_PRECOMPILE_ADDRESS. - func is_precompile{range_check_ptr}(address: felt) -> felt { - alloc_locals; - let is_rollup_precompile_ = is_rollup_precompile(address); - let is_kakarot_precompile_ = is_kakarot_precompile(address); - return is_not_zero(address) * ( - is_le(address, LAST_ETHEREUM_PRECOMPILE_ADDRESS) + - is_rollup_precompile_ + - is_kakarot_precompile_ - ); - } - - func is_rollup_precompile{range_check_ptr}(address: felt) -> felt { - return is_in_range( - address, FIRST_ROLLUP_PRECOMPILE_ADDRESS, LAST_ROLLUP_PRECOMPILE_ADDRESS + 1 - ); - } - - func is_kakarot_precompile{range_check_ptr}(address: felt) -> felt { - return is_in_range( - address, FIRST_KAKAROT_PRECOMPILE_ADDRESS, LAST_KAKAROT_PRECOMPILE_ADDRESS + 1 - ); - } - // @notice Executes associated function of precompiled evm_address. // @dev This function uses an internal jump table to execute the corresponding precompile impmentation. // @param precompile_address The precompile evm_address. @@ -84,13 +56,13 @@ namespace Precompiles { tempvar range_check_ptr = range_check_ptr; jmp eth_precompile if is_eth_precompile != 0; - let is_rollup_precompile_ = is_rollup_precompile(precompile_address); + let is_rollup_precompile_ = PrecompilesHelpers.is_rollup_precompile(precompile_address); tempvar syscall_ptr = syscall_ptr; tempvar pedersen_ptr = pedersen_ptr; tempvar range_check_ptr = range_check_ptr; jmp rollup_precompile if is_rollup_precompile_ != 0; - let is_kakarot_precompile_ = is_kakarot_precompile(precompile_address); + let is_kakarot_precompile_ = PrecompilesHelpers.is_kakarot_precompile(precompile_address); tempvar syscall_ptr = syscall_ptr; tempvar pedersen_ptr = pedersen_ptr; tempvar range_check_ptr = range_check_ptr; diff --git a/src/kakarot/precompiles/precompiles_helpers.cairo b/src/kakarot/precompiles/precompiles_helpers.cairo new file mode 100644 index 000000000..c653996ed --- /dev/null +++ b/src/kakarot/precompiles/precompiles_helpers.cairo @@ -0,0 +1,36 @@ +from starkware.cairo.common.math_cmp import is_le, is_not_zero, is_in_range + +const LAST_ETHEREUM_PRECOMPILE_ADDRESS = 0x0a; +const FIRST_ROLLUP_PRECOMPILE_ADDRESS = 0x100; +const LAST_ROLLUP_PRECOMPILE_ADDRESS = 0x100; +const EXEC_PRECOMPILE_SELECTOR = 0x01e3e7ac032066525c37d0791c3c0f5fbb1c17f1cb6fe00afc206faa3fbd18e1; +const FIRST_KAKAROT_PRECOMPILE_ADDRESS = 0x75001; +const LAST_KAKAROT_PRECOMPILE_ADDRESS = 0x75002; + +namespace PrecompilesHelpers { + func is_rollup_precompile{range_check_ptr}(address: felt) -> felt { + return is_in_range( + address, FIRST_ROLLUP_PRECOMPILE_ADDRESS, LAST_ROLLUP_PRECOMPILE_ADDRESS + 1 + ); + } + + func is_kakarot_precompile{range_check_ptr}(address: felt) -> felt { + return is_in_range( + address, FIRST_KAKAROT_PRECOMPILE_ADDRESS, LAST_KAKAROT_PRECOMPILE_ADDRESS + 1 + ); + } + // @notice Return whether the address is a precompile address. + // @dev Ethereum precompiles start at address 0x01. + // @dev RIP precompiles start at address FIRST_ROLLUP_PRECOMPILE_ADDRESS. + // @dev Kakarot precompiles start at address FIRST_KAKAROT_PRECOMPILE_ADDRESS. + func is_precompile{range_check_ptr}(address: felt) -> felt { + alloc_locals; + let is_rollup_precompile_ = is_rollup_precompile(address); + let is_kakarot_precompile_ = is_kakarot_precompile(address); + return is_not_zero(address) * ( + is_le(address, LAST_ETHEREUM_PRECOMPILE_ADDRESS) + + is_rollup_precompile_ + + is_kakarot_precompile_ + ); + } +} diff --git a/tests/src/kakarot/precompiles/test_precompiles.cairo b/tests/src/kakarot/precompiles/test_precompiles.cairo index eadb8a338..c01dcf1f5 100644 --- a/tests/src/kakarot/precompiles/test_precompiles.cairo +++ b/tests/src/kakarot/precompiles/test_precompiles.cairo @@ -5,6 +5,7 @@ from starkware.cairo.common.memcpy import memcpy from starkware.cairo.common.alloc import alloc from kakarot.precompiles.precompiles import Precompiles +from kakarot.precompiles.precompiles_helpers import PrecompilesHelpers func test__is_precompile{range_check_ptr}() -> felt { alloc_locals; @@ -13,7 +14,7 @@ func test__is_precompile{range_check_ptr}() -> felt { %{ ids.address = program_input["address"] %} // When - let is_precompile = Precompiles.is_precompile(address); + let is_precompile = PrecompilesHelpers.is_precompile(address); return is_precompile; } diff --git a/tests/src/kakarot/precompiles/test_precompiles.py b/tests/src/kakarot/precompiles/test_precompiles.py index 3f3976c10..f10a9aaff 100644 --- a/tests/src/kakarot/precompiles/test_precompiles.py +++ b/tests/src/kakarot/precompiles/test_precompiles.py @@ -80,14 +80,16 @@ class TestKakarotPrecompiles: AUTHORIZED_CALLER_CODE, 1, ) + @SyscallHandler.patch_deploy(lambda class_hash, data: [0]) @SyscallHandler.patch("Kakarot_evm_to_starknet_address", CALLER_ADDRESS, 0) - def test_should_fail_when_sender_starknet_address_zero( + @SyscallHandler.patch("ICairo.inc", lambda addr, data: []) + def test_should_deploy_account_when_sender_starknet_address_zero( self, cairo_run, ): """ Tests the behavior when the `msg.sender` in the contract that calls the precompile resolves - to a zero starknet address (meaning - it's not deployed yet, and will be at the end of the transaction). + to a zero starknet address (meaning - it's not deployed yet). """ return_data, reverted, gas_used = cairo_run( "test__precompiles_run", @@ -102,9 +104,11 @@ def test_should_fail_when_sender_starknet_address_zero( caller_code_address=AUTHORIZED_CALLER_CODE, caller_address=CALLER_ADDRESS, ) - assert bool(reverted) - assert bytes(return_data) == b"Kakarot: accountNotDeployed" + assert not bool(reverted) + assert bytes(return_data) == b"" assert gas_used == CAIRO_PRECOMPILE_GAS + + SyscallHandler.mock_deploy.assert_called_once() return @SyscallHandler.patch( @@ -219,34 +223,6 @@ def test__cairo_precompiles( ) return - @SyscallHandler.patch( - "Kakarot_authorized_cairo_precompiles_callers", - AUTHORIZED_CALLER_CODE, - 1, - ) - def test__should_fail_if_undeployed_starknet_account( - self, - cairo_run, - ): - # The expected returndata is a list of 32-byte words where each word is a felt returned by the precompile. - return_data, reverted, gas_used = cairo_run( - "test__precompiles_run", - address=0x75001, - input=bytes.fromhex( - CALL_CONTRACT_SOLIDITY_SELECTOR - + f"{0xc0de:064x}" - + f"{get_selector_from_name('get'):064x}" - + f"{0x60:064x}" # data_offset - + f"{0x01:064x}" # data_len - + f"{0x01:064x}" # data - ), - caller_code_address=AUTHORIZED_CALLER_CODE, - ) - assert bool(reverted) - assert bytes(return_data) == b"Kakarot: accountNotDeployed" - assert gas_used == CAIRO_PRECOMPILE_GAS - return - @pytest.mark.parametrize( "address, input_data, to_address, payload, expected_reverted", [ diff --git a/tests/utils/syscall_handler.py b/tests/utils/syscall_handler.py index b530e3541..8190206b3 100644 --- a/tests/utils/syscall_handler.py +++ b/tests/utils/syscall_handler.py @@ -165,6 +165,7 @@ class SyscallHandler: mock_event = mock.MagicMock() mock_replace_class = mock.MagicMock() mock_send_message_to_l1 = mock.MagicMock() + mock_deploy = mock.MagicMock() # Patch the keccak library call to return the keccak of the input data. # We need to reconstruct the raw bytes from the Cairo-style keccak calldata. @@ -510,6 +511,61 @@ def send_message_to_l1(self, segments, syscall_ptr): payload = [segments.memory[payload_ptr + i] for i in range(payload_size)] self.mock_send_message_to_l1(to_address=to_address, payload=payload) + def deploy(self, segments, syscall_ptr): + """ + Record the deploy call in the internal mock object. + + Syscall structure is: + struct DeployRequest { + selector: felt, + class_hash: felt, + contract_address_salt: felt, + constructor_calldata_size: felt, + constructor_calldata: felt*, + deploy_from_zero: felt, + } + + struct DeployResponse { + contract_address: felt, + constructor_retdata_size: felt, + constructor_retdata: felt*, + } + + """ + class_hash = segments.memory[syscall_ptr + 1] + contract_address_salt = segments.memory[syscall_ptr + 2] + constructor_calldata_size = segments.memory[syscall_ptr + 3] + constructor_calldata_ptr = segments.memory[syscall_ptr + 4] + constructor_calldata = [ + segments.memory[constructor_calldata_ptr + i] + for i in range(constructor_calldata_size) + ] + deploy_from_zero = segments.memory[syscall_ptr + 5] + self.mock_deploy( + class_hash=class_hash, + contract_address_salt=contract_address_salt, + constructor_calldata=constructor_calldata, + deploy_from_zero=deploy_from_zero, + ) + + retdata = self.patches.get("deploy")(class_hash, constructor_calldata) + retdata_segment = segments.add() + segments.write_arg(retdata_segment, retdata) + segments.write_arg(syscall_ptr + 6, [len(retdata), retdata_segment]) + + @classmethod + @contextmanager + def patch_deploy(cls, value: callable): + """ + Patch the deploy syscall with the value. + + :param value: The value to patch with, a callable that will be called with the class hash, + the contract address salt, the constructor calldata and the deploy from zero flag. + """ + cls.patches["deploy"] = value + yield + del cls.patches["deploy"] + @classmethod @contextmanager def patch(