Skip to content

Commit

Permalink
feat: starknet backend (#766)
Browse files Browse the repository at this point in the history
  • Loading branch information
enitrat authored Apr 13, 2024
1 parent efb0b9f commit ea2fea8
Show file tree
Hide file tree
Showing 14 changed files with 277 additions and 241 deletions.
30 changes: 5 additions & 25 deletions crates/contracts/src/kakarot_core/kakarot.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ pub mod KakarotCore {
use core::num::traits::zero::Zero;
use core::starknet::SyscallResultTrait;
use core::starknet::event::EventEmitter;
use evm::backend::starknet_backend;

use evm::errors::{EVMError, ensure, EVMErrorTrait,};
use evm::gas;
use evm::interpreter::{EVMTrait};
use evm::model::account::{Account, AccountType, AccountTrait};
use evm::model::contract_account::{ContractAccountTrait};
use evm::model::eoa::{EOATrait};
use evm::model::{
Transfer, Message, Environment, TransactionResult, TransactionResultTrait, ExecutionSummary,
ExecutionSummaryTrait, Address, AddressTrait
Expand Down Expand Up @@ -218,7 +218,7 @@ pub mod KakarotCore {
}

fn deploy_eoa(ref self: ContractState, evm_address: EthAddress) -> ContractAddress {
EOATrait::deploy(evm_address).expect('EOA Deployment failed').starknet
starknet_backend::deploy(evm_address).expect('EOA Deployment failed').starknet
}

fn eth_call(
Expand Down Expand Up @@ -258,7 +258,7 @@ pub mod KakarotCore {

let TransactionResult{success, return_data, gas_used, mut state } = self
.process_transaction(origin, tx);
state.commit_state().expect('Committing state failed');
starknet_backend::commit(ref state).expect('Committing state failed');
(success, return_data, gas_used)
}

Expand Down Expand Up @@ -331,30 +331,10 @@ pub mod KakarotCore {
fn process_transaction(
self: @ContractState, origin: Address, tx: EthereumTransaction
) -> TransactionResult {
let block_info = starknet::get_block_info().unbox();
//TODO(optimization): the coinbase account is deployed from a specific `evm_address` which is specified upon deployment
// and specific to the chain. Rather than reading from a contract, we could directly use this constant.
let coinbase = IAccountDispatcher { contract_address: block_info.sequencer_address }
.get_evm_address();

//TODO(gas) handle FeeMarketTransaction
let gas_price = tx.gas_price();
let gas_limit = tx.gas_limit();

// tx.gas_price and env.gas_price have the same values here
// - this is not always true in EVM transactions
let mut env = Environment {
origin: origin.evm,
gas_price,
chain_id: get_tx_info().unbox().chain_id.try_into().unwrap(),
prevrandao: 0,
block_number: block_info.block_number,
block_timestamp: block_info.block_timestamp,
block_gas_limit: constants::BLOCK_GAS_LIMIT,
coinbase,
state: Default::default(),
};

//TODO(gas) handle FeeMarketTransaction
let mut env = starknet_backend::get_env(origin.evm, gas_price);

// TX Gas
let gas_fee = gas_limit * gas_price;
Expand Down
1 change: 1 addition & 0 deletions crates/evm/src/backend.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod starknet_backend;
240 changes: 240 additions & 0 deletions crates/evm/src/backend/starknet_backend.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
use contracts::account_contract::{IAccountDispatcher, IAccountDispatcherTrait};
use contracts::kakarot_core::{KakarotCore, KakarotCore::KakarotCoreImpl};
use evm::errors::{ensure, EVMError, EOA_EXISTS};
use evm::model::{Address, AddressTrait, Environment};
use evm::state::{State, StateTrait};
use starknet::{
EthAddress, get_contract_address, deploy_syscall, get_tx_info, get_block_info,
SyscallResultTrait
};
use utils::constants;


fn commit(ref state: State) -> Result<(), EVMError> {
internals::commit_accounts(ref state)?;
internals::transfer_native_token(ref state)?;
internals::emit_events(ref state)?;
internals::commit_storage(ref state)
}


/// Deploys a new EOA contract.
///
/// # Arguments
///
/// * `evm_address` - The EVM address of the EOA to deploy.
fn deploy(evm_address: EthAddress) -> Result<Address, EVMError> {
// Unlike CAs, there is not check for the existence of an EOA prealably to calling `EOATrait::deploy` - therefore, we need to check that there is no collision.
let mut is_deployed = evm_address.is_deployed();
ensure(!is_deployed, EVMError::DeployError(EOA_EXISTS))?;

let mut kakarot_state = KakarotCore::unsafe_new_contract_state();
let account_class_hash = kakarot_state.account_class_hash();
let kakarot_address = get_contract_address();
let calldata: Span<felt252> = array![kakarot_address.into(), evm_address.into()].span();

let (starknet_address, _) = deploy_syscall(
account_class_hash, evm_address.into(), calldata, true
)
.unwrap_syscall();

Result::Ok(Address { evm: evm_address, starknet: starknet_address })
}

fn get_bytecode(evm_address: EthAddress) -> Span<u8> {
let kakarot_state = KakarotCore::unsafe_new_contract_state();
match kakarot_state.address_registry(evm_address) {
Option::Some((
acc_type, starknet_address
)) => {
let account = IAccountDispatcher { contract_address: starknet_address };
account.bytecode()
},
Option::None => { array![].span() }
}
}

fn get_env(origin: EthAddress, gas_price: u128) -> Environment {
let block_info = get_block_info().unbox();
//TODO(optimization): the coinbase account is deployed from a specific `evm_address` which is specified upon deployment
// and specific to the chain. Rather than reading from a contract, we could directly use this constant.
let coinbase = IAccountDispatcher { contract_address: block_info.sequencer_address }
.get_evm_address();

// tx.gas_price and env.gas_price have the same values here
// - this is not always true in EVM transactions
Environment {
origin: origin,
gas_price,
chain_id: get_tx_info().unbox().chain_id.try_into().unwrap(),
prevrandao: 0,
block_number: block_info.block_number,
block_timestamp: block_info.block_timestamp,
block_gas_limit: constants::BLOCK_GAS_LIMIT,
coinbase,
state: Default::default(),
}
}

mod internals {
use contracts::account_contract::{IAccountDispatcher, IAccountDispatcherTrait};
use contracts::kakarot_core::{KakarotCore, KakarotCore::KakarotCoreImpl};
use evm::errors::EVMError;
use evm::model::account::{Account, AccountTrait};
use evm::model::{Address, AddressTrait, Transfer};
use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait};
use starknet::SyscallResultTrait;
use starknet::syscalls::{emit_event_syscall};
use super::{State, StateTrait};
use utils::constants::BURN_ADDRESS;
use utils::set::{Set, SetTrait};


fn commit_accounts(ref state: State) -> Result<(), EVMError> {
let mut account_keys = state.accounts.keyset.to_span();
loop {
match account_keys.pop_front() {
Option::Some(evm_address) => {
let account = state.accounts.changes.get(*evm_address).deref();
commit(@account, ref state);
},
Option::None => { break Result::Ok(()); }
};
}
}

/// Commits the account to Starknet by updating the account state if it
/// exists, or deploying a new account if it doesn't.
///
/// Only Contract Accounts can be modified.
///
/// # Arguments
/// * `self` - The account to commit
///
/// # Returns
///
/// `Ok(())` if the commit was successful, otherwise an `EVMError`.
//TODO: move state out in starknet backend
fn commit(self: @Account, ref state: State) {
let is_deployed = self.evm_address().is_deployed();

if self.is_precompile() {
return;
}

// Case new account
if !is_deployed {
// If SELFDESTRUCT, deploy empty SN account
self.deploy();

let has_code_or_nonce = self.has_code_or_nonce();
if !has_code_or_nonce {
// Nothing to commit
return;
}

// If SELFDESTRUCT, leave the account empty after deploying it - including
// burning any leftover balance.
if (self.is_selfdestruct()) {
let kakarot_state = KakarotCore::unsafe_new_contract_state();
let starknet_address = kakarot_state
.compute_starknet_address(BURN_ADDRESS.try_into().unwrap());
let burn_address = Address {
starknet: starknet_address, evm: BURN_ADDRESS.try_into().unwrap()
};
state
.add_transfer(
Transfer {
sender: self.address(), recipient: burn_address, amount: self.balance()
}
)
.expect('Failed to burn on selfdestruct');
return;
}

let account = IAccountDispatcher { contract_address: self.starknet_address() };
account.write_bytecode(self.bytecode());
account.set_nonce(*self.nonce);

//TODO: storage commits are done in the State commitment
//Storage is handled outside of the account and must be committed after all accounts are committed.
return;
};

// If the account was not scheduled for deployment - then update it if it's deployed.

let is_created_selfdestructed = self.is_selfdestruct() && self.is_created();
if is_created_selfdestructed {
// If the account was created and selfdestructed in the same transaction, we don't need to commit it.
return;
}

let account = IAccountDispatcher { contract_address: self.starknet_address() };

account.set_nonce(*self.nonce);
//TODO: handle storage commitment

// Update bytecode if required (SELFDESTRUCTed contract committed and redeployed)
//TODO: add bytecode_len entrypoint for optimization
let bytecode_len = account.bytecode().len();
if bytecode_len != self.bytecode().len() {
account.write_bytecode(self.bytecode());
}
}

/// Iterates through the list of pending transfer and triggers them
fn transfer_native_token(ref self: State) -> Result<(), EVMError> {
let kakarot_state = KakarotCore::unsafe_new_contract_state();
let native_token = kakarot_state.native_token();
loop {
match self.transfers.pop_front() {
Option::Some(transfer) => {
IERC20CamelDispatcher { contract_address: native_token }
.transferFrom(
transfer.sender.starknet, transfer.recipient.starknet, transfer.amount
);
},
Option::None => { break; }
}
};
Result::Ok(())
}

/// Iterates through the list of events and emits them.
fn emit_events(ref self: State) -> Result<(), EVMError> {
loop {
match self.events.pop_front() {
Option::Some(event) => {
let mut keys = Default::default();
let mut data = Default::default();
Serde::<Array<u256>>::serialize(@event.keys, ref keys);
Serde::<Array<u8>>::serialize(@event.data, ref data);
emit_event_syscall(keys.span(), data.span()).unwrap_syscall();
},
Option::None => { break Result::Ok(()); }
}
}
}

/// Commits storage changes to the KakarotCore contract by writing pending
/// state changes to Starknet Storage.
/// commit_storage MUST be called after commit_accounts.
fn commit_storage(ref self: State) -> Result<(), EVMError> {
let mut storage_keys = self.accounts_storage.keyset.to_span();
let result = loop {
match storage_keys.pop_front() {
Option::Some(state_key) => {
let (evm_address, key, value) = self
.accounts_storage
.changes
.get(*state_key)
.deref();
let mut account = self.get_account(evm_address);
account.commit_storage(key, value);
},
Option::None => { break Result::Ok(()); }
}
};
result
}
}
1 change: 1 addition & 0 deletions crates/evm/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod backend;
// Call opcodes helpers
mod call_helpers;

Expand Down
2 changes: 0 additions & 2 deletions crates/evm/src/model.cairo
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
mod account;
mod contract_account;
mod eoa;
mod vm;

use contracts::kakarot_core::{KakarotCore, IKakarotCore};
use evm::errors::{EVMError, CONTRACT_SYSCALL_FAILED};
use evm::model::account::{Account, AccountTrait};
use evm::model::eoa::EOATrait;
use evm::state::State;
use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait};
use starknet::{EthAddress, get_contract_address, ContractAddress};
Expand Down
Loading

0 comments on commit ea2fea8

Please sign in to comment.