Skip to content

Commit

Permalink
feat: invoke sequencer (#144)
Browse files Browse the repository at this point in the history
* commit trait

* execution trait

* implement commit and execution for sequencer

* test one transaction on sequencer

* fix constants

* refactor committer

* add cairo 1 contracts

* update error handling

* add cairo 1 test

* update based on comments
  • Loading branch information
greged93 authored Sep 22, 2023
1 parent b0b853b commit 0087e4e
Show file tree
Hide file tree
Showing 16 changed files with 21,706 additions and 4 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/sequencer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ starknet_api = { workspace = true }
starknet = { workspace = true }

# Other
tracing = { workspace = true }
rustc-hash = "1.1.0"

[dev-dependencies]
lazy_static = { workspace = true }
serde_json = { workspace = true }
30 changes: 30 additions & 0 deletions crates/sequencer/src/commit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use blockifier::state::{
cached_state::CachedState,
state_api::{State as BlockifierState, StateReader as BlockifierStateReader, StateResult},
};

pub trait Committer<S>
where
for<'a> &'a mut S: BlockifierState + BlockifierStateReader,
{
fn commit(cached_state: &mut CachedState<&mut S>) -> StateResult<()> {
let diff = cached_state.to_state_diff();
for (address, class_hash) in diff.address_to_class_hash {
cached_state.state.set_class_hash_at(address, class_hash)?;
}
for (address, _) in diff.address_to_nonce {
cached_state.state.increment_nonce(address)?;
}
for (address, storage_updates) in diff.storage_updates {
for (k, v) in storage_updates {
cached_state.state.set_storage_at(address, k, v);
}
}
for (class_hash, compiled_class_hash) in diff.class_hash_to_compiled_class_hash {
cached_state
.state
.set_compiled_class_hash(class_hash, compiled_class_hash)?;
}
Ok(())
}
}
1 change: 1 addition & 0 deletions crates/sequencer/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub mod test_constants {
pub static ref SEQUENCER_ADDRESS: ContractAddress = ContractAddress(TryInto::<PatriciaKey>::try_into(StarkFelt::from(1234u16)).unwrap());
pub static ref FEE_TOKEN_ADDRESS: ContractAddress = ContractAddress(TryInto::<PatriciaKey>::try_into(StarkFelt::from(12345u16)).unwrap());

pub static ref ZERO_FELT: StarkFelt = StarkFelt::from(0u8);
pub static ref ONE_FELT: StarkFelt = StarkFelt::from(1u8);
pub static ref TWO_FELT: StarkFelt = StarkFelt::from(2u8);
pub static ref ONE_PATRICIA: PatriciaKey = TryInto::<PatriciaKey>::try_into(*ONE_FELT).unwrap();
Expand Down
7 changes: 7 additions & 0 deletions crates/sequencer/src/execution.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use blockifier::transaction::{
errors::TransactionExecutionError, transaction_execution::Transaction,
};

pub trait Execution {
fn execute(&mut self, transaction: Transaction) -> Result<(), TransactionExecutionError>;
}
2 changes: 2 additions & 0 deletions crates/sequencer/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod commit;
pub mod constants;
pub mod execution;
pub mod sequencer;
pub mod state;
252 changes: 250 additions & 2 deletions crates/sequencer/src/sequencer.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
use blockifier::block_context::BlockContext;
use blockifier::state::state_api::{State, StateReader};
use crate::{commit::Committer, execution::Execution};
use blockifier::{
block_context::BlockContext,
state::{
cached_state::CachedState,
state_api::{State, StateReader},
},
transaction::{
errors::TransactionExecutionError, transaction_execution::Transaction,
transactions::ExecutableTransaction,
},
};
use tracing::{trace, warn};

/// Sequencer is the main struct of the sequencer crate.
/// Using a trait bound for the state allows for better
Expand All @@ -25,3 +36,240 @@ where
Self { context, state }
}
}

impl<S> Execution for Sequencer<S>
where
for<'a> &'a mut S: State + StateReader + Committer<S>,
{
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);

match res {
Err(err) => {
warn!("Transaction execution failed: {:?}", err);
return Err(err);
}
Ok(execution_information) => {
<&mut S>::commit(&mut cached_state)?;
match execution_information.revert_error {
Some(err) => {
warn!("Transaction execution reverted: {:?}", err)
}
None => {
trace!("Transaction execution succeeded {execution_information:?}")
}
}
}
}

Ok(())
}
}

#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::fs::File;
use std::sync::Arc;

use blockifier::abi::abi_utils::get_storage_var_address;
use blockifier::execution::contract_class::{ContractClass, ContractClassV0, ContractClassV1};
use blockifier::state::state_api::State as BlockifierState;
use blockifier::transaction::account_transaction::AccountTransaction;
use starknet::macros::selector;
use starknet_api::core::{ChainId, ClassHash, ContractAddress, Nonce};
use starknet_api::hash::StarkFelt;
use starknet_api::transaction::{
Calldata, Fee, InvokeTransaction, InvokeTransactionV1, TransactionHash,
TransactionSignature,
};

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,
};
use crate::state::State;

use super::*;

fn read_contract_class_v0(path: &str) -> ContractClass {
let reader = File::open(path).unwrap();
let contract_class: ContractClassV0 = serde_json::from_reader(reader).unwrap();

ContractClass::V0(contract_class)
}

fn read_contract_class_v1(path: &str) -> ContractClass {
let raw_contract_class = std::fs::read_to_string(path).unwrap();
let contract_class: ContractClassV1 =
ContractClassV1::try_from_json_string(&raw_contract_class).unwrap();

ContractClass::V1(contract_class)
}

fn declare_and_deploy_contract(
path: &str,
address: ContractAddress,
class_hash: ClassHash,
mut state: &mut State,
is_v0: bool,
) {
let contract_class = if is_v0 {
read_contract_class_v0(path)
} else {
read_contract_class_v1(path)
};

state
.set_contract_class(&class_hash, contract_class)
.unwrap();
state.set_class_hash_at(address, class_hash).unwrap();
}

fn fund(address: StarkFelt, mut state: &mut State) {
state.set_storage_at(
*FEE_TOKEN_ADDRESS,
get_storage_var_address("ERC20_balances", &[address]).unwrap(),
StarkFelt::from(u128::MAX),
);
}

fn block_context() -> BlockContext {
BlockContext {
chain_id: ChainId("KKRT".into()),
block_number: *ONE_BLOCK_NUMBER,
block_timestamp: *ONE_BLOCK_TIMESTAMP,
sequencer_address: *SEQUENCER_ADDRESS,
fee_token_address: *FEE_TOKEN_ADDRESS,

vm_resource_fee_cost: vm_resource_fee_cost(),
gas_price: 1,
invoke_tx_max_n_steps: 4_000_000,
validate_max_n_steps: 4_000_000,
max_recursion_depth: 1_000,
}
}

/// Maps builtins and steps to a single cost unit of reference (gas).
fn vm_resource_fee_cost() -> Arc<HashMap<String, f64>> {
Arc::new(
[
(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(),
)
}

fn test_transaction() -> Transaction {
Transaction::AccountTransaction(AccountTransaction::Invoke(InvokeTransaction::V1(
InvokeTransactionV1 {
transaction_hash: TransactionHash(*ZERO_FELT),
sender_address: *TEST_CONTRACT_ACCOUNT,
calldata: Calldata(
vec![
*TEST_ADDRESS, // destination
selector!("inc").into(),
*ZERO_FELT, // no data
]
.into(),
),
max_fee: Fee(1_000_000),
signature: TransactionSignature(vec![]),
nonce: Nonce(*ZERO_FELT),
},
)))
}

#[test]
fn test_sequencer_cairo_0() {
// Given
let mut state = State::default();
let mutable = &mut state;

declare_and_deploy_contract(
"src/test_data/cairo_0/compiled_classes/counter.json",
*TEST_CONTRACT_ADDRESS,
*ONE_CLASS_HASH,
mutable,
true,
);
declare_and_deploy_contract(
"src/test_data/cairo_0/compiled_classes/account.json",
*TEST_CONTRACT_ACCOUNT,
*TWO_CLASS_HASH,
mutable,
true,
);
fund(*TEST_ADDRESS, mutable);

let context = block_context();
let mut sequencer = Sequencer::new(context, state);

// When
let transaction = test_transaction();
sequencer.execute(transaction).unwrap();

// Then
let expected = StarkFelt::from(1u8);
let actual = (&mut sequencer.state)
.get_storage_at(
*TEST_CONTRACT_ADDRESS,
get_storage_var_address("counter", &[]).unwrap(),
)
.unwrap();
assert_eq!(expected, actual);
}

#[test]
fn test_sequencer_cairo_1() {
// Given
let mut state = State::default();
let mutable = &mut state;

declare_and_deploy_contract(
"src/test_data/cairo_1/compiled_classes/counter.json",
*TEST_CONTRACT_ADDRESS,
*ONE_CLASS_HASH,
mutable,
false,
);
declare_and_deploy_contract(
"src/test_data/cairo_1/compiled_classes/account.json",
*TEST_CONTRACT_ACCOUNT,
*TWO_CLASS_HASH,
mutable,
false,
);
fund(*TEST_ADDRESS, mutable);

let context = block_context();
let mut sequencer = Sequencer::new(context, state);

// When
let transaction = test_transaction();
sequencer.execute(transaction).unwrap();

// Then
let expected = StarkFelt::from(1u8);
let actual = (&mut sequencer.state)
.get_storage_at(
*TEST_CONTRACT_ADDRESS,
get_storage_var_address("counter", &[]).unwrap(),
)
.unwrap();
assert_eq!(expected, actual);
}
}
13 changes: 11 additions & 2 deletions crates/sequencer/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use starknet_api::{
hash::StarkFelt,
};

use crate::commit::Committer;

/// Generic state structure for the sequencer.
/// The use of FxHashMap allows for a better performance.
/// This hash map is used by rustc. It uses a non cryptographic hash function
Expand All @@ -29,6 +31,8 @@ pub struct State {
nonces: FxHashMap<ContractAddress, Nonce>,
}

impl Committer<State> for &mut State {}

/// State implementation for the sequencer. We use a mutable reference to the state
/// because this is what will be available during the implementation of the execution.
impl BlockifierState for &mut State {
Expand Down Expand Up @@ -62,8 +66,13 @@ impl BlockifierState for &mut State {
contract_address: ContractAddress,
class_hash: ClassHash,
) -> StateResult<()> {
self.contracts.insert(contract_address, class_hash);
Ok(())
match self.contracts.get(&contract_address) {
Some(_) => Err(StateError::UnavailableContractAddress(contract_address)),
None => {
self.contracts.insert(contract_address, class_hash);
Ok(())
}
}
}

fn set_contract_class(
Expand Down
Loading

0 comments on commit 0087e4e

Please sign in to comment.