diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..538d0813 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +--- +name: Workflow - CI + +on: + push: + branches: [main] + pull_request: + branches: + - main + +permissions: read-all + +jobs: + trunk: + name: Trunk + uses: ./.github/workflows/trunk-check.yml + permissions: + checks: write + contents: read + + submodules: + name: Compile Kakarot contracts + uses: ./.github/workflows/kakarot.yml + + tests: + name: Rust tests + uses: ./.github/workflows/test.yml + needs: submodules diff --git a/.github/workflows/kakarot.yml b/.github/workflows/kakarot.yml new file mode 100644 index 00000000..16198ddb --- /dev/null +++ b/.github/workflows/kakarot.yml @@ -0,0 +1,35 @@ +name: Kakarot compilation caching + +on: + workflow_call: {} + +permissions: read-all + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-submodules + cancel-in-progress: true + +jobs: + submodules: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.9 + uses: actions/setup-python@v4 + with: + python-version: 3.9 + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + - name: pull kakarot submodule and setup + run: make setup-kakarot + - name: Save compiled kakarot contracts + id: cached-contracts + uses: actions/cache/save@v3 + with: + path: ./lib/kakarot/build + key: ${{ runner.os }}-contracts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1da5e17f..c92582cb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,23 +1,27 @@ name: test on: - push: - branches: [main] - pull_request: - branches: [main] + workflow_call: {} permissions: read-all concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref }}-test cancel-in-progress: true jobs: test: - runs-on: ubuntu-latest + # trunk-ignore(actionlint/runner-label) + runs-on: ubuntu-latest-16-cores timeout-minutes: 120 steps: - uses: actions/checkout@v3 + - name: Load compiled kakarot contracts + id: cached-contracts + uses: actions/cache/restore@v3 + with: + path: ./lib/kakarot/build + key: ${{ runner.os }}-contracts - name: Setup rust env uses: actions-rs/toolchain@v1 with: @@ -26,13 +30,10 @@ jobs: override: true - name: Retrieve cached dependencies uses: Swatinem/rust-cache@v2 - # fetch ef tests - name: fetch ef tests run: make setup - # fetch dump - name: fetch dump run: GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} make fetch-dump - uses: taiki-e/install-action@nextest - # run tests - name: run tests run: make ef-tests diff --git a/.github/workflows/trunk-check.yaml b/.github/workflows/trunk-check.yml similarity index 79% rename from .github/workflows/trunk-check.yaml rename to .github/workflows/trunk-check.yml index 327024da..556d70e3 100644 --- a/.github/workflows/trunk-check.yaml +++ b/.github/workflows/trunk-check.yml @@ -1,21 +1,17 @@ name: Trunk on: - push: - branches: [main] - pull_request: - branches: [main] + workflow_call: {} concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref }}-trunk cancel-in-progress: true -permissions: read-all - jobs: trunk_check: name: Trunk Check Runner runs-on: ubuntu-latest + # trunk-ignore(checkov/CKV2_GHA_1) permissions: checks: write # For trunk to post annotations contents: read # For repo checkout diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..629d95e3 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "kakarot"] + path = lib/kakarot + url = https://github.com/kkrt-labs/kakarot.git diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 5ab7ff9b..7f19224c 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -20,6 +20,7 @@ lint: cargo clippy --message-format json --locked --workspace --all-features --all-targets -- --cap-lints=warn --no-deps -D warnings + run_timeout: 20m enabled: - actionlint@1.6.26 - checkov@2.4.9 diff --git a/Cargo.lock b/Cargo.lock index 3ad1c882..2e4d0a4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2450,7 +2450,9 @@ name = "ef-testing" version = "0.1.0" dependencies = [ "async-trait", + "blockifier", "bytes", + "cairo-vm", "chrono", "ctor", "dojo-test-utils", @@ -2467,6 +2469,7 @@ dependencies = [ "reth-rlp", "revm-primitives", "rstest", + "sequencer", "serde", "serde_json", "serde_yaml", @@ -7903,6 +7906,7 @@ name = "sequencer" version = "0.1.0" dependencies = [ "blockifier", + "eyre", "lazy_static", "rustc-hash", "serde_json", @@ -9131,9 +9135,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e862a1c4128df0112ab625f55cd5c934bcb4312ba80b39ae4b4835a3fd58e649" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" dependencies = [ "byteorder", "bytes", diff --git a/Cargo.toml b/Cargo.toml index f362a662..293571e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,8 @@ kakarot-rpc-core = { git = "https://github.com/kkrt-labs/kakarot-rpc.git", rev = kakarot-test-utils = { git = "https://github.com/kkrt-labs/kakarot-rpc.git", rev = "ae5e220" } # Starknet deps +cairo-vm = "0.8.2" +blockifier = { package = "blockifier", git = "https://github.com/starkware-libs/blockifier.git", tag = "v0.3.0-rc0" } katana-core = { git = 'https://github.com/dojoengine/dojo', rev = "b924dac" } dojo-test-utils = { git = 'https://github.com/dojoengine/dojo', rev = "b924dac" } diff --git a/Makefile b/Makefile index 12e9c387..60bb6b6f 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,13 @@ KAKAROT_COMMIT := .katana/remote_kakarot_sha # Ensures the commands for $(EF_TESTS_DIR) always run on `make setup`, regardless if the directory exists .PHONY: $(EF_TESTS_DIR) -setup: $(EF_TESTS_DIR) +setup: $(EF_TESTS_DIR) + +setup-kakarot: pull-kakarot + cd lib/kakarot && make setup && make build + +pull-kakarot: + git submodule update --init --recursive fetch-dump: fetch-kakarot-submodule-commit cargo run --features dump --bin fetch-dump-katana diff --git a/crates/ef-testing/Cargo.toml b/crates/ef-testing/Cargo.toml index 9f898b93..e2d509f9 100644 --- a/crates/ef-testing/Cargo.toml +++ b/crates/ef-testing/Cargo.toml @@ -20,8 +20,11 @@ reth-rlp = { workspace = true } # Kakarot deps kakarot-rpc-core = { workspace = true } kakarot-test-utils = { workspace = true } +sequencer = { path = "../sequencer" } # Starknet deps +blockifier = { workspace = true } +cairo-vm = { workspace = true } dojo-test-utils = { workspace = true } katana-core = { workspace = true } starknet_api = { workspace = true } diff --git a/crates/ef-testing/src/evm_sequencer/constants.rs b/crates/ef-testing/src/evm_sequencer/constants.rs new file mode 100644 index 00000000..d720d36d --- /dev/null +++ b/crates/ef-testing/src/evm_sequencer/constants.rs @@ -0,0 +1,100 @@ +use std::{collections::HashMap, sync::Arc}; + +use blockifier::block_context::BlockContext; +use lazy_static::lazy_static; +use starknet::core::types::{contract::legacy::LegacyContractClass, FieldElement}; +use starknet_api::{ + block::{BlockNumber, BlockTimestamp}, + core::{ChainId, ClassHash, ContractAddress, PatriciaKey}, + hash::StarkFelt, +}; + +lazy_static! { + // Chain params + pub static ref CHAIN_ID: u64 = 0x4b4b5254; + + // Vm resources: maps resource name to fee cost. + pub static ref VM_RESOURCES: HashMap = [ + (String::from("n_steps"), 1_f64), + ("pedersen_builtin".to_string(), 1_f64), + ("range_check_builtin".to_string(), 1_f64), + ("ecdsa_builtin".to_string(), 1_f64), + ("bitwise_builtin".to_string(), 1_f64), + ("poseidon_builtin".to_string(), 1_f64), + ("output_builtin".to_string(), 1_f64), + ("ec_op_builtin".to_string(), 1_f64), + ("keccak_builtin".to_string(), 1_f64), + ("segment_arena_builtin".to_string(), 1_f64), + ] + .into_iter() + .collect(); + + // Block context + pub static ref BLOCK_CONTEXT: BlockContext = BlockContext { + chain_id: ChainId(String::from_utf8(CHAIN_ID.to_be_bytes().to_vec()).unwrap()), + block_number: BlockNumber(0), + block_timestamp: BlockTimestamp(0), + sequencer_address: *SEQUENCER_ADDRESS, + fee_token_address: *FEE_TOKEN_ADDRESS, + vm_resource_fee_cost: Arc::new(VM_RESOURCES.clone()), + gas_price: 1, + invoke_tx_max_n_steps: 2_u32.pow(24), + validate_max_n_steps: 2_u32.pow(24), + max_recursion_depth: 1024, + }; + + // Main addresses + pub static ref SEQUENCER_ADDRESS: ContractAddress = ContractAddress( + TryInto::::try_into(StarkFelt::from( + FieldElement::from_hex_be( + "0x01176a1bd84444c89232ec27754698e5d2e7e1a7f1539f12027f28b23ec9f3d8" + ) + .unwrap() + )) + .unwrap() + ); + pub static ref FEE_TOKEN_ADDRESS: ContractAddress = ContractAddress( + TryInto::::try_into(StarkFelt::from( + FieldElement::from_hex_be( + "0x049D36570D4e46f48e99674bd3fcc84644DdD6b96F7C741B1562B82f9e004dC7" + ) + .unwrap() + )) + .unwrap() + ); + pub static ref KAKAROT_ADDRESS: ContractAddress = + ContractAddress(TryInto::::try_into(StarkFelt::from(1_u8)).unwrap()); + pub static ref KAKAROT_OWNER_ADDRESS: ContractAddress = + ContractAddress(TryInto::::try_into(StarkFelt::from(2_u8)).unwrap()); + + // Main contract classes + pub static ref KAKAROT_CLASS: LegacyContractClass = serde_json::from_reader::<_, LegacyContractClass>(std::fs::File::open("../../lib/kakarot/build/kakarot.json").unwrap()).unwrap(); + pub static ref CONTRACT_ACCOUNT_CLASS: LegacyContractClass = serde_json::from_reader::<_, LegacyContractClass>(std::fs::File::open("../../lib/kakarot/build/contract_account.json").unwrap()).unwrap(); + pub static ref EOA_CLASS: LegacyContractClass = serde_json::from_reader::<_, LegacyContractClass>(std::fs::File::open("../../lib/kakarot/build/externally_owned_account.json").unwrap()).unwrap(); + pub static ref PROXY_CLASS: LegacyContractClass = serde_json::from_reader::<_, LegacyContractClass>(std::fs::File::open("../../lib/kakarot/build/proxy.json").unwrap()).unwrap(); + + // Main class hashes + pub static ref KAKAROT_CLASS_HASH: ClassHash = ClassHash(KAKAROT_CLASS.class_hash().unwrap().into()); + pub static ref CONTRACT_ACCOUNT_CLASS_HASH: ClassHash = ClassHash(CONTRACT_ACCOUNT_CLASS.class_hash().unwrap().into()); + pub static ref EOA_CLASS_HASH: ClassHash = ClassHash(EOA_CLASS.class_hash().unwrap().into()); + pub static ref PROXY_CLASS_HASH: ClassHash = ClassHash(PROXY_CLASS.class_hash().unwrap().into()); + +} + +#[cfg(test)] +pub mod tests { + use std::str::FromStr; + + use lazy_static::lazy_static; + use reth_primitives::Address; + use revm_primitives::B256; + + lazy_static! { + pub static ref PRIVATE_KEY: B256 = + B256::from_str("0x6ae82d865482a203603ecbf25c865e082396d7705a6bbce92c1ff1d6ab9b503c") + .unwrap(); + pub static ref PUBLIC_KEY: Address = + Address::from_str("0x7513A12F74fFF533ee12F20EE524e4883CBd1945").unwrap(); + pub static ref TEST_CONTRACT_ADDRESS: Address = Address::from_low_u64_be(10); + } +} diff --git a/crates/ef-testing/src/evm_sequencer/mod.rs b/crates/ef-testing/src/evm_sequencer/mod.rs new file mode 100644 index 00000000..afd75410 --- /dev/null +++ b/crates/ef-testing/src/evm_sequencer/mod.rs @@ -0,0 +1,100 @@ +pub mod constants; +pub mod setup; +pub mod types; +pub mod utils; + +use blockifier::abi::abi_utils::get_storage_var_address; +use blockifier::execution::contract_class::{ContractClass, ContractClassV0}; +use blockifier::state::errors::StateError; +use blockifier::state::state_api::{State as BlockifierState, StateResult}; +use cairo_vm::types::errors::program_errors::ProgramError; +use sequencer::sequencer::Sequencer; +use sequencer::state::State; + +use self::constants::{ + BLOCK_CONTEXT, CONTRACT_ACCOUNT_CLASS, CONTRACT_ACCOUNT_CLASS_HASH, EOA_CLASS, EOA_CLASS_HASH, + FEE_TOKEN_ADDRESS, KAKAROT_ADDRESS, KAKAROT_CLASS, KAKAROT_CLASS_HASH, KAKAROT_OWNER_ADDRESS, + PROXY_CLASS, PROXY_CLASS_HASH, +}; + +pub(crate) struct KakarotSequencer(Sequencer); + +#[allow(dead_code)] +impl KakarotSequencer { + pub fn new(state: State) -> Self { + let sequencer = Sequencer::new(BLOCK_CONTEXT.clone(), state); + Self(sequencer) + } + + pub fn initialize(mut self) -> StateResult { + let storage = vec![ + ("Ownable_owner", *KAKAROT_OWNER_ADDRESS.0.key()), + ("native_token_address", *FEE_TOKEN_ADDRESS.0.key()), + ("contract_account_class_hash", CONTRACT_ACCOUNT_CLASS_HASH.0), + ("externally_owned_account_class_hash", EOA_CLASS_HASH.0), + ("account_proxy_class_hash", PROXY_CLASS_HASH.0), + ]; + + // Write all the storage vars to the sequencer state. + for (k, v) in storage { + (&mut self.0.state).set_storage_at( + *KAKAROT_ADDRESS, + get_storage_var_address(k, &[]).unwrap(), // safe unwrap: all vars are ASCII + v, + ); + } + + // Write the kakarot class and class hash. + (&mut self.0.state).set_class_hash_at(*KAKAROT_ADDRESS, *KAKAROT_CLASS_HASH)?; + (&mut self.0.state).set_contract_class( + &KAKAROT_CLASS_HASH, + ContractClass::V0(ContractClassV0::try_from_json_string( + &serde_json::to_string(&*KAKAROT_CLASS) + .map_err(|err| StateError::ProgramError(ProgramError::Parse(err)))?, + )?), + )?; + + // Write proxy, eoa and contract account classes and class hashes. + (&mut self.0.state).set_contract_class( + &PROXY_CLASS_HASH, + ContractClass::V0(ContractClassV0::try_from_json_string( + &serde_json::to_string(&*PROXY_CLASS) + .map_err(|err| StateError::ProgramError(ProgramError::Parse(err)))?, + )?), + )?; + (&mut self.0.state).set_contract_class( + &CONTRACT_ACCOUNT_CLASS_HASH, + ContractClass::V0(ContractClassV0::try_from_json_string( + &serde_json::to_string(&*CONTRACT_ACCOUNT_CLASS) + .map_err(|err| StateError::ProgramError(ProgramError::Parse(err)))?, + )?), + )?; + (&mut self.0.state).set_contract_class( + &EOA_CLASS_HASH, + ContractClass::V0(ContractClassV0::try_from_json_string( + &serde_json::to_string(&*EOA_CLASS) + .map_err(|err| StateError::ProgramError(ProgramError::Parse(err)))?, + )?), + )?; + + Ok(self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_initialize() { + // Given + let state = State::default(); + let sequencer = KakarotSequencer::new(state); + + // When + let result = sequencer.initialize(); + + // Then + assert!(result.is_ok()); + } +} diff --git a/crates/ef-testing/src/evm_sequencer/setup.rs b/crates/ef-testing/src/evm_sequencer/setup.rs new file mode 100644 index 00000000..b85d261e --- /dev/null +++ b/crates/ef-testing/src/evm_sequencer/setup.rs @@ -0,0 +1,229 @@ +use blockifier::abi::abi_utils::{ + get_erc20_balance_var_addresses, get_storage_var_address, get_uint256_storage_var_addresses, +}; +use blockifier::state::state_api::{State, StateResult}; +use reth_primitives::{Address, Bytes}; +use revm_primitives::U256; +use starknet_api::core::Nonce; +use starknet_api::hash::StarkFelt; +use starknet_api::StarknetApiError; + +use super::constants::{ + CONTRACT_ACCOUNT_CLASS_HASH, EOA_CLASS_HASH, FEE_TOKEN_ADDRESS, KAKAROT_ADDRESS, + PROXY_CLASS_HASH, +}; +use super::types::FeltSequencer; +use super::utils::{compute_starknet_address, split_bytecode_to_starkfelt, split_u256}; +use super::KakarotSequencer; + +pub trait EvmState { + fn setup_account( + &mut self, + evm_address: &Address, + bytecode: &Bytes, + nonce: U256, + storage: Vec<(U256, U256)>, + ) -> StateResult<()>; + + fn fund(&mut self, evm_address: &Address, balance: U256) -> StateResult<()>; +} + +impl EvmState for KakarotSequencer { + fn setup_account( + &mut self, + evm_address: &Address, + bytecode: &Bytes, + nonce: U256, + evm_storage: Vec<(U256, U256)>, + ) -> StateResult<()> { + let nonce = StarkFelt::from(TryInto::::try_into(nonce).map_err(|err| { + StarknetApiError::OutOfRange { + string: err.to_string(), + } + })?); + let starknet_address = compute_starknet_address(evm_address); + let evm_address = Into::::into(*evm_address).into(); + + let mut storage = vec![ + (("evm_address", vec![]), evm_address), + (("is_initialized_", vec![]), StarkFelt::from(1u8)), + (("Ownable_owner", vec![]), *KAKAROT_ADDRESS.0.key()), + ( + ("bytecode_len_", vec![]), + StarkFelt::from(bytecode.len() as u32), + ), + (("kakarot_address", vec![]), *KAKAROT_ADDRESS.0.key()), + ]; + + let starknet_address = starknet_address.try_into()?; + // Initialize the implementation and nonce based on account type. + if bytecode.is_empty() && evm_storage.is_empty() { + storage.push((("_implementation", vec![]), EOA_CLASS_HASH.0)); + self.0.state.set_nonce(starknet_address, Nonce(nonce)); + } else { + storage.append(&mut vec![ + (("nonce", vec![]), nonce), + (("_implementation", vec![]), CONTRACT_ACCOUNT_CLASS_HASH.0), + ]); + } + + // Initialize the bytecode storage var. + let bytecode_storage = &mut split_bytecode_to_starkfelt(bytecode) + .into_iter() + .enumerate() + .map(|(i, bytes)| (("bytecode_", vec![StarkFelt::from(i as u32)]), bytes)) + .collect(); + storage.append(bytecode_storage); + + // Initialize the storage vars. + let evm_storage_storage = &mut evm_storage + .iter() + .flat_map(|(k, v)| { + let keys = split_u256(*k).map(Into::into); + let values = split_u256(*v).map(Into::into); + vec![ + (("storage_", vec![keys[0]]), values[0]), + (("storage_", vec![keys[1]]), values[1]), + ] + }) + .collect(); + storage.append(evm_storage_storage); + + // Write all the storage vars to the sequencer state. + for ((var, keys), v) in storage { + (&mut self.0.state).set_storage_at( + starknet_address, + get_storage_var_address(var, &keys).unwrap(), // safe unwrap: all vars are ASCII + v, + ); + } + + // Set up the contract class hash. + (&mut self.0.state).set_class_hash_at(starknet_address, *PROXY_CLASS_HASH)?; + + // Add the address to the Kakarot evm to starknet mapping + let evm_starknet_address_mapping_storage = ( + get_storage_var_address("evm_to_starknet_address", &[evm_address]).unwrap(), // safe unwrap: var is ASCII + *starknet_address.0.key(), + ); + (&mut self.0.state).set_storage_at( + *KAKAROT_ADDRESS, + evm_starknet_address_mapping_storage.0, + evm_starknet_address_mapping_storage.1, + ); + Ok(()) + } + + fn fund(&mut self, evm_address: &Address, balance: U256) -> StateResult<()> { + let starknet_address = compute_starknet_address(evm_address); + let balance_values = split_u256(balance); + let mut storage = vec![]; + + // Initialize the balance storage var. + let balance_keys = get_erc20_balance_var_addresses(&FEE_TOKEN_ADDRESS)?; + let balance_keys = [balance_keys.0, balance_keys.1]; + let balance_storage = &mut balance_keys + .into_iter() + .zip(balance_values) + .map(|(k, v)| (k, StarkFelt::from(v))) + .collect(); + storage.append(balance_storage); + + // Initialize the allowance storage var. + let allowance_keys = get_uint256_storage_var_addresses( + "ERC20_allowances", + &[*FEE_TOKEN_ADDRESS.0.key(), starknet_address.into()], + )?; + let allowance_keys = [allowance_keys.0, allowance_keys.1]; + let allowance_storage = &mut allowance_keys + .into_iter() + .map(|k| (k, StarkFelt::from(u128::MAX))) + .collect(); + storage.append(allowance_storage); + + // Write all the storage vars to the sequencer state. + let starknet_address = starknet_address.try_into()?; + for (k, v) in storage { + (&mut self.0.state).set_storage_at(starknet_address, k, v); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::evm_sequencer::{ + constants::{ + tests::{PRIVATE_KEY, PUBLIC_KEY, TEST_CONTRACT_ADDRESS}, + CHAIN_ID, + }, + utils::to_broadcasted_starknet_transaction, + }; + + use super::*; + use blockifier::state::state_api::StateReader; + use bytes::BytesMut; + use reth_primitives::{sign_message, AccessList, Signature, TransactionSigned, TxEip1559}; + use revm_primitives::B256; + use sequencer::{ + execution::Execution, state::State as SequencerState, transaction::StarknetTransaction, + }; + use starknet::core::types::{BroadcastedTransaction, FieldElement}; + + #[test] + fn test_execute_simple_contract() { + // Given + let sequencer = KakarotSequencer::new(SequencerState::default()); + let mut sequencer = sequencer.initialize().unwrap(); + + let transaction = TransactionSigned { + hash: B256::default(), + signature: Signature::default(), + transaction: reth_primitives::Transaction::Eip1559(TxEip1559 { + chain_id: *CHAIN_ID, + nonce: 0, + gas_limit: 0, + max_fee_per_gas: 0, + max_priority_fee_per_gas: 0, + to: reth_primitives::TransactionKind::Call(*TEST_CONTRACT_ADDRESS), + value: 0, + access_list: AccessList::default(), + input: Bytes::default(), + }), + }; + let signature = + sign_message(*PRIVATE_KEY, transaction.transaction.signature_hash()).unwrap(); + let mut output = BytesMut::new(); + transaction.encode_with_signature(&signature, &mut output, false); + let transaction = BroadcastedTransaction::Invoke( + to_broadcasted_starknet_transaction(&output.to_vec().into()).unwrap(), + ); + let transaction = StarknetTransaction::new(transaction) + .try_into_execution_transaction(FieldElement::from(*CHAIN_ID)) + .unwrap(); + + // When + let bytecode = Bytes::from(vec![96, 1, 96, 0, 85]); // PUSH 01 PUSH 00 SSTORE + let nonce = U256::from(0); + sequencer + .setup_account(&TEST_CONTRACT_ADDRESS, &bytecode, nonce, vec![]) + .unwrap(); + sequencer + .setup_account(&PUBLIC_KEY, &Bytes::default(), U256::from(0), vec![]) + .unwrap(); + sequencer.0.execute(transaction).unwrap(); + + // Then + let contract_starknet_address = compute_starknet_address(&TEST_CONTRACT_ADDRESS) + .try_into() + .unwrap(); + let storage = (&mut sequencer.0.state) + .get_storage_at( + contract_starknet_address, + get_storage_var_address("storage_", &[StarkFelt::from(0u8), StarkFelt::from(0u8)]) + .unwrap(), + ) + .unwrap(); + assert_eq!(storage, StarkFelt::from(1u8)); + } +} diff --git a/crates/ef-testing/src/evm_sequencer/types.rs b/crates/ef-testing/src/evm_sequencer/types.rs new file mode 100644 index 00000000..19091ad1 --- /dev/null +++ b/crates/ef-testing/src/evm_sequencer/types.rs @@ -0,0 +1,45 @@ +use reth_primitives::Address; +use starknet::core::types::FieldElement; +use starknet_api::{ + core::{ContractAddress, PatriciaKey}, + hash::StarkFelt, + StarknetApiError, +}; + +#[derive(Debug, Clone, Copy)] +pub struct FeltSequencer(FieldElement); + +impl From for FeltSequencer { + fn from(felt: FieldElement) -> Self { + Self(felt) + } +} + +impl From for FieldElement { + fn from(felt: FeltSequencer) -> Self { + felt.0 + } +} + +impl From
for FeltSequencer { + fn from(address: Address) -> Self { + let address = FieldElement::from_byte_slice_be(&address.0[..]).unwrap(); // safe unwrap since Address is 20 bytes + Self(address) + } +} + +impl From for StarkFelt { + fn from(felt: FeltSequencer) -> Self { + StarkFelt::from(felt.0) + } +} + +impl TryFrom for ContractAddress { + type Error = StarknetApiError; + + fn try_from(felt: FeltSequencer) -> Result { + let felt: StarkFelt = felt.into(); + let contract_address = ContractAddress(TryInto::::try_into(felt)?); + Ok(contract_address) + } +} diff --git a/crates/ef-testing/src/evm_sequencer/utils.rs b/crates/ef-testing/src/evm_sequencer/utils.rs new file mode 100644 index 00000000..193e6121 --- /dev/null +++ b/crates/ef-testing/src/evm_sequencer/utils.rs @@ -0,0 +1,87 @@ +use super::{ + constants::{KAKAROT_ADDRESS, PROXY_CLASS_HASH}, + types::FeltSequencer, +}; +use reth_primitives::{Address, Bytes, TransactionSigned}; +use reth_rlp::Decodable; +use revm_primitives::U256; +use starknet::{ + core::{ + types::{BroadcastedInvokeTransaction, FieldElement}, + utils::get_contract_address, + }, + macros::selector, +}; +use starknet_api::hash::StarkFelt; + +pub fn compute_starknet_address(evm_address: &Address) -> FeltSequencer { + let evm_address: FeltSequencer = (*evm_address).into(); + let starknet_address = get_contract_address( + evm_address.into(), + PROXY_CLASS_HASH.0.into(), + &[], + (*KAKAROT_ADDRESS.0.key()).into(), + ); + starknet_address.into() +} + +pub(crate) fn split_bytecode_to_starkfelt(bytecode: &Bytes) -> Vec { + bytecode + .chunks(16) + .map(|x| { + let mut storage_value = [0u8; 16]; + storage_value[..x.len()].copy_from_slice(x); + StarkFelt::from(u128::from_be_bytes(storage_value)) + }) + .collect() +} + +pub(crate) fn split_u256(value: U256) -> [u128; 2] { + [ + (value & U256::from(u128::MAX)).try_into().unwrap(), // safe unwrap <= U128::MAX. + (value >> 128).try_into().unwrap(), // safe unwrap <= U128::MAX. + ] +} + +pub fn bytes_to_felt_vec(bytes: &Bytes) -> Vec { + bytes.to_vec().into_iter().map(FieldElement::from).collect() +} + +#[allow(dead_code)] +pub(crate) fn to_broadcasted_starknet_transaction( + bytes: &Bytes, +) -> Result { + let transaction = TransactionSigned::decode(&mut bytes.as_ref())?; + + let evm_address = transaction + .recover_signer() + .ok_or_else(|| eyre::eyre!("Missing signer in signed transaction"))?; + + let nonce = FieldElement::from(transaction.nonce()); + let starknet_address = compute_starknet_address(&evm_address); + + let mut calldata = bytes_to_felt_vec(bytes); + + let mut execute_calldata: Vec = vec![ + FieldElement::ONE, // call array length + (*KAKAROT_ADDRESS.0.key()).into(), // contract address + selector!("eth_send_transaction"), // selector + FieldElement::ZERO, // data offset + FieldElement::from(calldata.len()), // data length + FieldElement::from(calldata.len()), // calldata length + ]; + execute_calldata.append(&mut calldata); + + let signature = vec![]; + + let request = BroadcastedInvokeTransaction { + max_fee: FieldElement::from(0u8), + signature, + nonce, + sender_address: starknet_address.into(), + calldata: execute_calldata, + is_query: false, + }; + + Ok(request) +} diff --git a/crates/ef-testing/src/lib.rs b/crates/ef-testing/src/lib.rs index 6c04c317..d5949b34 100644 --- a/crates/ef-testing/src/lib.rs +++ b/crates/ef-testing/src/lib.rs @@ -1,3 +1,4 @@ +pub mod evm_sequencer; pub mod models; pub mod storage; pub mod traits; diff --git a/crates/sequencer/Cargo.toml b/crates/sequencer/Cargo.toml index 94d64421..c72c2b3f 100644 --- a/crates/sequencer/Cargo.toml +++ b/crates/sequencer/Cargo.toml @@ -14,11 +14,12 @@ license.workspace = true [dependencies] # Starknet # TODO: remove the blockifier patch on the workspace once we can remove Katana. -blockifier = { package = "blockifier", git = "https://github.com/starkware-libs/blockifier.git", tag = "v0.3.0-rc0" } +blockifier = { workspace = true } starknet_api = { workspace = true } starknet = { workspace = true } # Other +eyre = { workspace = true } tracing = { workspace = true } rustc-hash = "1.1.0" diff --git a/crates/sequencer/src/lib.rs b/crates/sequencer/src/lib.rs index a82f65f7..ebd30d80 100644 --- a/crates/sequencer/src/lib.rs +++ b/crates/sequencer/src/lib.rs @@ -3,3 +3,4 @@ pub mod constants; pub mod execution; pub mod sequencer; pub mod state; +pub mod transaction; diff --git a/crates/sequencer/src/sequencer.rs b/crates/sequencer/src/sequencer.rs index 4a11d48a..6fa25a26 100644 --- a/crates/sequencer/src/sequencer.rs +++ b/crates/sequencer/src/sequencer.rs @@ -23,7 +23,7 @@ pub struct Sequencer where for<'a> &'a mut S: State + StateReader, { - pub context: BlockContext, + pub block_context: BlockContext, pub state: S, } @@ -32,8 +32,11 @@ where for<'a> &'a mut S: State + StateReader, { /// Creates a new Sequencer instance. - pub fn new(context: BlockContext, state: S) -> Self { - Self { context, state } + pub fn new(block_context: BlockContext, state: S) -> Self { + Self { + block_context, + state, + } } } @@ -44,7 +47,7 @@ where fn execute(&mut self, transaction: Transaction) -> Result<(), TransactionExecutionError> { let mut cached_state = CachedState::new(&mut self.state); let charge_fee = false; - let res = transaction.execute(&mut cached_state, &self.context, charge_fee); + let res = transaction.execute(&mut cached_state, &self.block_context, charge_fee); match res { Err(err) => { @@ -86,10 +89,10 @@ mod tests { TransactionSignature, }; + use crate::constants::test_constants::{FEE_TOKEN_ADDRESS, SEQUENCER_ADDRESS}; use crate::constants::test_constants::{ - FEE_TOKEN_ADDRESS, ONE_BLOCK_NUMBER, ONE_BLOCK_TIMESTAMP, ONE_CLASS_HASH, - SEQUENCER_ADDRESS, TEST_ADDRESS, TEST_CONTRACT_ACCOUNT, TEST_CONTRACT_ADDRESS, - TWO_CLASS_HASH, ZERO_FELT, + ONE_BLOCK_NUMBER, ONE_BLOCK_TIMESTAMP, ONE_CLASS_HASH, TEST_ADDRESS, TEST_CONTRACT_ACCOUNT, + TEST_CONTRACT_ADDRESS, TWO_CLASS_HASH, ZERO_FELT, }; use crate::state::State; diff --git a/crates/sequencer/src/state.rs b/crates/sequencer/src/state.rs index 9734e6da..87ac5d54 100644 --- a/crates/sequencer/src/state.rs +++ b/crates/sequencer/src/state.rs @@ -31,6 +31,12 @@ pub struct State { nonces: FxHashMap, } +impl State { + pub fn set_nonce(&mut self, contract_address: ContractAddress, nonce: Nonce) { + self.nonces.insert(contract_address, nonce); + } +} + impl Committer for &mut State {} /// State implementation for the sequencer. We use a mutable reference to the state diff --git a/crates/sequencer/src/transaction.rs b/crates/sequencer/src/transaction.rs new file mode 100644 index 00000000..316d0aad --- /dev/null +++ b/crates/sequencer/src/transaction.rs @@ -0,0 +1,86 @@ +use std::sync::Arc; + +use blockifier::transaction::{ + account_transaction::AccountTransaction, + transaction_execution::Transaction as ExecutionTransaction, +}; +use starknet::core::crypto::compute_hash_on_elements; +use starknet::core::types::{BroadcastedTransaction, FieldElement}; +use starknet_api::core::{ContractAddress, Nonce, PatriciaKey}; +use starknet_api::hash::{StarkFelt, StarkHash}; +use starknet_api::transaction::InvokeTransaction; +use starknet_api::transaction::{ + Calldata, Fee, InvokeTransactionV1, TransactionHash, TransactionSignature, +}; + +#[derive(Debug)] +pub struct StarknetTransaction(BroadcastedTransaction); + +impl StarknetTransaction { + pub fn new(transaction: BroadcastedTransaction) -> Self { + Self(transaction) + } + + pub fn try_into_execution_transaction( + self, + chain_id: FieldElement, + ) -> Result { + match self.0 { + BroadcastedTransaction::Invoke(invoke) => Ok(ExecutionTransaction::AccountTransaction( + AccountTransaction::Invoke(InvokeTransaction::V1(InvokeTransactionV1 { + transaction_hash: TransactionHash(Into::::into( + Into::::into(compute_transaction_hash( + invoke.sender_address, + &invoke.calldata, + invoke.max_fee, + chain_id, + invoke.nonce, + )), + )), + max_fee: Fee(invoke.max_fee.try_into()?), + signature: TransactionSignature( + invoke + .signature + .into_iter() + .map(Into::::into) + .collect(), + ), + nonce: Nonce(invoke.nonce.try_into()?), + sender_address: ContractAddress(TryInto::::try_into(Into::< + StarkHash, + >::into( + Into::::into(invoke.sender_address), + ))?), + calldata: Calldata(Arc::new( + invoke + .calldata + .into_iter() + .map(Into::::into) + .collect(), + )), + })), + )), + // TODO: Add support for other transaction types. + _ => Err(eyre::eyre!("Unsupported transaction type")), + } + } +} + +fn compute_transaction_hash( + sender_address: FieldElement, + calldata: &[FieldElement], + max_fee: FieldElement, + chain_id: FieldElement, + nonce: FieldElement, +) -> FieldElement { + compute_hash_on_elements(&[ + FieldElement::from_byte_slice_be(b"invoke").unwrap(), + FieldElement::ONE, + sender_address, + FieldElement::ZERO, // entry_point_selector + compute_hash_on_elements(calldata), + max_fee, + chain_id, + nonce, + ]) +} diff --git a/lib/kakarot b/lib/kakarot new file mode 160000 index 00000000..93fb7369 --- /dev/null +++ b/lib/kakarot @@ -0,0 +1 @@ +Subproject commit 93fb736954b29802d9e49d82cd2b0aa7b9b29b69