Skip to content

Commit

Permalink
feat: deploy starknet account if CairoLib caller is not deployed (#1253)
Browse files Browse the repository at this point in the history
<!--- Please provide a general summary of your changes in the title
above -->

<!-- Give an estimate of the time you spent on this PR in terms of work
days.
Did you spend 0.5 days on this PR or rather 2 days?  -->

Time spent on this PR: 0.3d

## Pull request type

<!-- Please try to limit your pull request to one type,
submit multiple pull requests if needed. -->

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?

<!-- Please describe the current behavior that you are modifying,
or link to a relevant issue. -->

Resolves #<Issue number>

## What is the new behavior?

<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Deploys a SN account for a caller of a Cairo contract if its account
is not deployed on starknet already.
-
-

<!-- Reviewable:start -->
- - -
This change is [<img src="https://reviewable.io/review_button.svg"
height="34" align="absmiddle"
alt="Reviewable"/>](https://reviewable.io/reviews/kkrt-labs/kakarot/1253)
<!-- Reviewable:end -->
  • Loading branch information
enitrat committed Jul 4, 2024
1 parent 22cbf53 commit 59835a7
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 108 deletions.
57 changes: 23 additions & 34 deletions src/backend/starknet.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 ();
}
Expand All @@ -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 ();
}

Expand All @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion src/kakarot/interpreter.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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));
Expand Down
17 changes: 13 additions & 4 deletions src/kakarot/precompiles/kakarot_precompiles.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
44 changes: 8 additions & 36 deletions src/kakarot/precompiles/precompiles.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
36 changes: 36 additions & 0 deletions src/kakarot/precompiles/precompiles_helpers.cairo
Original file line number Diff line number Diff line change
@@ -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_
);
}
}
3 changes: 2 additions & 1 deletion tests/src/kakarot/precompiles/test_precompiles.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

Expand Down
40 changes: 8 additions & 32 deletions tests/src/kakarot/precompiles/test_precompiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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(
Expand Down Expand Up @@ -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",
[
Expand Down
56 changes: 56 additions & 0 deletions tests/utils/syscall_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 59835a7

Please sign in to comment.