use std::num::NonZeroU128;
use std::sync::Arc;
use blockifier::block::BlockInfo;
use blockifier::context::{BlockContext, ChainInfo, TransactionContext};
use blockifier::execution::entry_point::CallEntryPoint;
use blockifier::state::state_api::StateReader;
use blockifier::transaction::errors::TransactionPreValidationError;
use blockifier::transaction::objects::TransactionExecutionInfo;
use blockifier::transaction::transactions::ExecutableTransaction;
use starknet_api::block::{BlockNumber, BlockStatus, BlockTimestamp, GasPrice, GasPricePerToken};
use starknet_api::core::SequencerContractAddress;
use starknet_api::transaction::Fee;
use starknet_rs_core::types::{
BlockId, MsgFromL1, TransactionExecutionStatus, TransactionFinalityStatus,
};
use starknet_rs_core::utils::get_selector_from_name;
use starknet_rs_ff::FieldElement;
use starknet_rs_signers::Signer;
use starknet_types::chain_id::ChainId;
use starknet_types::contract_address::ContractAddress;
use starknet_types::contract_class::ContractClass;
use starknet_types::contract_storage_key::ContractStorageKey;
use starknet_types::emitted_event::EmittedEvent;
use starknet_types::felt::{ClassHash, Felt, TransactionHash};
use starknet_types::patricia_key::PatriciaKey;
use starknet_types::rpc::block::{Block, BlockHeader};
use starknet_types::rpc::estimate_message_fee::FeeEstimateWrapper;
use starknet_types::rpc::state::ThinStateDiff;
use starknet_types::rpc::transaction_receipt::{
DeployTransactionReceipt, L1HandlerTransactionReceipt, TransactionReceipt,
};
use starknet_types::rpc::transactions::broadcasted_declare_transaction_v1::BroadcastedDeclareTransactionV1;
use starknet_types::rpc::transactions::broadcasted_declare_transaction_v2::BroadcastedDeclareTransactionV2;
use starknet_types::rpc::transactions::broadcasted_declare_transaction_v3::BroadcastedDeclareTransactionV3;
use starknet_types::rpc::transactions::broadcasted_deploy_account_transaction_v1::BroadcastedDeployAccountTransactionV1;
use starknet_types::rpc::transactions::broadcasted_deploy_account_transaction_v3::BroadcastedDeployAccountTransactionV3;
use starknet_types::rpc::transactions::broadcasted_invoke_transaction_v1::BroadcastedInvokeTransactionV1;
use starknet_types::rpc::transactions::broadcasted_invoke_transaction_v3::BroadcastedInvokeTransactionV3;
use starknet_types::rpc::transactions::{
BlockTransactionTrace, BroadcastedTransaction, BroadcastedTransactionCommon,
DeclareTransaction, L1HandlerTransaction, SimulatedTransaction, SimulationFlag, Transaction,
TransactionTrace, TransactionWithReceipt, Transactions,
};
use starknet_types::traits::HashProducer;
use tracing::{error, info};
use self::dump::DumpEvent;
use self::predeployed::initialize_erc20_at_address;
use self::starknet_config::{DumpOn, StarknetConfig, StateArchiveCapacity};
use self::transaction_trace::create_trace;
use crate::account::Account;
use crate::blocks::{StarknetBlock, StarknetBlocks};
use crate::constants::{
CHARGEABLE_ACCOUNT_ADDRESS, CHARGEABLE_ACCOUNT_PRIVATE_KEY, DEVNET_DEFAULT_CHAIN_ID,
DEVNET_DEFAULT_GAS_PRICE, ETH_ERC20_CONTRACT_ADDRESS, ETH_ERC20_NAME, ETH_ERC20_SYMBOL,
STRK_ERC20_CONTRACT_ADDRESS, STRK_ERC20_NAME, STRK_ERC20_SYMBOL,
};
use crate::error::{DevnetResult, Error, TransactionValidationError};
use crate::messaging::MessagingBroker;
use crate::predeployed_accounts::PredeployedAccounts;
use crate::raw_execution::{Call, RawExecution};
use crate::state::state_diff::StateDiff;
use crate::state::state_update::StateUpdate;
use crate::state::StarknetState;
use crate::traits::{
AccountGenerator, Accounted, Deployed, HashIdentified, HashIdentifiedMut, StateChanger,
StateExtractor,
};
use crate::transactions::{StarknetTransaction, StarknetTransactions};
use crate::utils::get_versioned_constants;
mod add_declare_transaction;
mod add_deploy_account_transaction;
mod add_invoke_transaction;
mod add_l1_handler_transaction;
pub mod dump;
mod estimations;
mod events;
mod get_class_impls;
mod predeployed;
pub mod starknet_config;
mod state_update;
pub(crate) mod transaction_trace;
pub struct Starknet {
pub(in crate::starknet) state: StarknetState,
predeployed_accounts: PredeployedAccounts,
pub(in crate::starknet) block_context: BlockContext,
pub(crate) blocks: StarknetBlocks,
pub transactions: StarknetTransactions,
pub config: StarknetConfig,
pub pending_block_timestamp_shift: i64,
pub next_block_timestamp: Option<u64>,
pub(crate) messaging: MessagingBroker,
pub(crate) dump_events: Vec<DumpEvent>,
}
impl Default for Starknet {
fn default() -> Self {
Self {
block_context: Self::init_block_context(
DEVNET_DEFAULT_GAS_PRICE,
ETH_ERC20_CONTRACT_ADDRESS,
STRK_ERC20_CONTRACT_ADDRESS,
DEVNET_DEFAULT_CHAIN_ID,
),
state: Default::default(),
predeployed_accounts: Default::default(),
blocks: Default::default(),
transactions: Default::default(),
config: Default::default(),
pending_block_timestamp_shift: 0,
next_block_timestamp: None,
messaging: Default::default(),
dump_events: Default::default(),
}
}
}
impl Starknet {
pub fn new(config: &StarknetConfig) -> DevnetResult<Self> {
let mut state = StarknetState::default();
let eth_erc20_fee_contract =
predeployed::create_erc20_at_address(ETH_ERC20_CONTRACT_ADDRESS)?;
let strk_erc20_fee_contract =
predeployed::create_erc20_at_address(STRK_ERC20_CONTRACT_ADDRESS)?;
let udc_contract = predeployed::create_udc()?;
udc_contract.deploy(&mut state)?;
eth_erc20_fee_contract.deploy(&mut state)?;
initialize_erc20_at_address(
&mut state,
ETH_ERC20_CONTRACT_ADDRESS,
ETH_ERC20_NAME,
ETH_ERC20_SYMBOL,
)?;
strk_erc20_fee_contract.deploy(&mut state)?;
initialize_erc20_at_address(
&mut state,
STRK_ERC20_CONTRACT_ADDRESS,
STRK_ERC20_NAME,
STRK_ERC20_SYMBOL,
)?;
let mut predeployed_accounts = PredeployedAccounts::new(
config.seed,
config.predeployed_accounts_initial_balance,
eth_erc20_fee_contract.get_address(),
strk_erc20_fee_contract.get_address(),
);
let accounts = predeployed_accounts.generate_accounts(
config.total_accounts,
config.account_contract_class_hash,
config.account_contract_class.clone(),
)?;
for account in accounts {
account.deploy(&mut state)?;
account.set_initial_balance(&mut state)?;
}
let chargeable_account = Account::new_chargeable(
eth_erc20_fee_contract.get_address(),
strk_erc20_fee_contract.get_address(),
)?;
chargeable_account.deploy(&mut state)?;
chargeable_account.set_initial_balance(&mut state)?;
state.clear_dirty_state();
let mut this = Self {
state,
predeployed_accounts,
block_context: Self::init_block_context(
config.gas_price,
ETH_ERC20_CONTRACT_ADDRESS,
STRK_ERC20_CONTRACT_ADDRESS,
config.chain_id,
),
blocks: StarknetBlocks::default(),
transactions: StarknetTransactions::default(),
config: config.clone(),
pending_block_timestamp_shift: 0,
next_block_timestamp: None,
messaging: Default::default(),
dump_events: Default::default(),
};
this.restart_pending_block()?;
if this.config.dump_path.is_some() && this.config.re_execute_on_init {
match this.load_events() {
Ok(events) => this.re_execute(events)?,
Err(Error::FileNotFound) => {}
Err(err) => return Err(err),
};
}
Ok(this)
}
pub fn restart(&mut self) -> DevnetResult<()> {
self.config.re_execute_on_init = false;
*self = Starknet::new(&self.config)?;
info!("Starknet Devnet restarted");
Ok(())
}
pub fn get_predeployed_accounts(&self) -> Vec<Account> {
self.predeployed_accounts.get_accounts().to_vec()
}
pub(crate) fn generate_pending_block(&mut self) -> DevnetResult<()> {
Self::advance_block_context_block_number(&mut self.block_context);
self.restart_pending_block()?;
Ok(())
}
fn next_block_timestamp(&mut self) -> BlockTimestamp {
match self.next_block_timestamp {
Some(timestamp) => {
self.next_block_timestamp = None;
BlockTimestamp(timestamp)
}
None => BlockTimestamp(
(Starknet::get_unix_timestamp_as_seconds() as i64
+ self.pending_block_timestamp_shift) as u64,
),
}
}
pub(crate) fn generate_new_block(
&mut self,
state_diff: StateDiff,
) -> DevnetResult<BlockNumber> {
let mut new_block = self.pending_block().clone();
new_block.set_block_hash(new_block.generate_hash()?);
new_block.status = BlockStatus::AcceptedOnL2;
let block_timestamp = self.next_block_timestamp();
new_block.set_timestamp(block_timestamp);
Self::update_block_context_block_timestamp(&mut self.block_context, block_timestamp);
let new_block_number = new_block.block_number();
new_block.get_transactions().iter().for_each(|tx_hash| {
if let Some(tx) = self.transactions.get_by_hash_mut(tx_hash) {
tx.block_hash = Some(new_block.header.block_hash.0.into());
tx.block_number = Some(new_block_number);
tx.finality_status = TransactionFinalityStatus::AcceptedOnL2;
} else {
error!("Transaction is not present in the transactions collection");
}
});
self.blocks.insert(new_block, state_diff);
if self.config.state_archive == StateArchiveCapacity::Full {
let deep_cloned_state = self.state.clone();
self.blocks.save_state_at(new_block_number, deep_cloned_state);
}
self.generate_pending_block()?;
Ok(new_block_number)
}
pub(crate) fn handle_transaction_result(
&mut self,
transaction: Transaction,
contract_class: Option<ContractClass>,
transaction_result: Result<
TransactionExecutionInfo,
blockifier::transaction::errors::TransactionExecutionError,
>,
) -> DevnetResult<()> {
let transaction_hash = *transaction.get_transaction_hash();
fn save_contract_class(
class_hash: &ClassHash,
contract_class: Option<ContractClass>,
state: &mut StarknetState,
) -> DevnetResult<()> {
state.contract_classes.insert(
*class_hash,
contract_class.ok_or(Error::UnexpectedInternalError {
msg: "contract class not provided".to_string(),
})?,
);
Ok(())
}
match transaction_result {
Ok(tx_info) => {
if !tx_info.is_reverted() {
match &transaction {
Transaction::Declare(DeclareTransaction::Version1(declare_v1)) => {
save_contract_class(
&declare_v1.class_hash,
contract_class,
&mut self.state,
)?
}
Transaction::Declare(DeclareTransaction::Version2(declare_v2)) => {
save_contract_class(
&declare_v2.class_hash,
contract_class,
&mut self.state,
)?
}
Transaction::Declare(DeclareTransaction::Version3(declare_v3)) => {
save_contract_class(
declare_v3.get_class_hash(),
contract_class,
&mut self.state,
)?
}
_ => {}
};
}
self.handle_accepted_transaction(&transaction_hash, &transaction, tx_info)
}
Err(tx_err) => {
fn match_tx_fee_error(
err: blockifier::transaction::errors::TransactionFeeError,
) -> DevnetResult<()> {
match err {
blockifier::transaction::errors::TransactionFeeError::FeeTransferError { .. }
| blockifier::transaction::errors::TransactionFeeError::MaxFeeTooLow { .. } => Err(
TransactionValidationError::InsufficientMaxFee.into()
),
blockifier::transaction::errors::TransactionFeeError::MaxFeeExceedsBalance { .. } | blockifier::transaction::errors::TransactionFeeError::L1GasBoundsExceedBalance { .. } => Err(
TransactionValidationError::InsufficientAccountBalance.into()
),
_ => Err(err.into())
}
}
match tx_err {
blockifier::transaction::errors::TransactionExecutionError::TransactionPreValidationError(
TransactionPreValidationError::InvalidNonce { .. }
) => Err(TransactionValidationError::InvalidTransactionNonce.into()),
blockifier::transaction::errors::TransactionExecutionError::FeeCheckError { .. } =>
Err(TransactionValidationError::InsufficientMaxFee.into()),
blockifier::transaction::errors::TransactionExecutionError::TransactionPreValidationError(
TransactionPreValidationError::TransactionFeeError(err)
) => match_tx_fee_error(err),
blockifier::transaction::errors::TransactionExecutionError::TransactionFeeError(err)
=> match_tx_fee_error(err),
blockifier::transaction::errors::TransactionExecutionError::ValidateTransactionError(err) => {
Err(TransactionValidationError::ValidationFailure { reason: err.to_string() }.into())
}
_ => Err(tx_err.into())
}
}
}
}
pub(crate) fn handle_accepted_transaction(
&mut self,
transaction_hash: &TransactionHash,
transaction: &Transaction,
tx_info: TransactionExecutionInfo,
) -> DevnetResult<()> {
let state_diff = self.state.extract_state_diff_from_pending_state()?;
let trace = create_trace(
&mut self.state.state,
transaction.get_type(),
&tx_info,
state_diff.clone().into(),
)?;
let transaction_to_add = StarknetTransaction::create_accepted(transaction, tx_info, trace);
self.blocks.pending_block.add_transaction(*transaction_hash);
self.transactions.insert(transaction_hash, transaction_to_add);
self.state.apply_state_difference(state_diff.clone())?;
self.state.clear_dirty_state();
self.generate_new_block(state_diff)?;
Ok(())
}
fn init_block_context(
gas_price: NonZeroU128,
eth_fee_token_address: &str,
strk_fee_token_address: &str,
chain_id: ChainId,
) -> BlockContext {
use starknet_api::core::{ContractAddress, PatriciaKey};
use starknet_api::hash::StarkHash;
use starknet_api::{contract_address, patricia_key};
let block_info = BlockInfo {
block_number: BlockNumber(0),
block_timestamp: BlockTimestamp(0),
sequencer_address: contract_address!("0x1000"),
gas_prices: blockifier::block::GasPrices {
eth_l1_gas_price: gas_price,
strk_l1_gas_price: gas_price,
eth_l1_data_gas_price: gas_price,
strk_l1_data_gas_price: gas_price,
},
use_kzg_da: true,
};
let chain_info = ChainInfo {
chain_id: chain_id.into(),
fee_token_addresses: blockifier::context::FeeTokenAddresses {
eth_fee_token_address: contract_address!(eth_fee_token_address),
strk_fee_token_address: contract_address!(strk_fee_token_address),
},
};
BlockContext::new_unchecked(&block_info, &chain_info, &get_versioned_constants())
}
fn advance_block_context_block_number(block_context: &mut BlockContext) {
let mut block_info = block_context.block_info().clone();
block_info.block_number = block_info.block_number.next();
*block_context = BlockContext::new_unchecked(
&block_info,
block_context.chain_info(),
&get_versioned_constants(),
);
}
fn update_block_context_block_timestamp(
block_context: &mut BlockContext,
block_timestamp: BlockTimestamp,
) {
let mut block_info = block_context.block_info().clone();
block_info.block_timestamp = block_timestamp;
*block_context = BlockContext::new_unchecked(
&block_info,
block_context.chain_info(),
&get_versioned_constants(),
);
}
fn pending_block(&self) -> &StarknetBlock {
&self.blocks.pending_block
}
fn restart_pending_block(&mut self) -> DevnetResult<()> {
let mut block = StarknetBlock::create_pending_block();
block.header.block_number = self.block_context.block_info().block_number;
block.header.l1_gas_price = GasPricePerToken {
price_in_fri: GasPrice(
self.block_context.block_info().gas_prices.strk_l1_gas_price.get(),
),
price_in_wei: GasPrice(
self.block_context.block_info().gas_prices.eth_l1_gas_price.get(),
),
};
block.header.sequencer =
SequencerContractAddress(self.block_context.block_info().sequencer_address);
self.blocks.pending_block = block;
Ok(())
}
fn get_state_at(&self, block_id: &BlockId) -> DevnetResult<&StarknetState> {
match block_id {
BlockId::Tag(_) => Ok(&self.state),
_ => {
if self.config.state_archive == StateArchiveCapacity::None {
return Err(Error::StateHistoryDisabled);
}
let block = self.blocks.get_by_block_id(block_id).ok_or(Error::NoBlock)?;
let state = self
.blocks
.num_to_state
.get(&block.block_number())
.ok_or(Error::NoStateAtBlock { block_number: block.block_number().0 })?;
Ok(state)
}
}
}
pub fn get_class_hash_at(
&self,
block_id: &BlockId,
contract_address: ContractAddress,
) -> DevnetResult<ClassHash> {
get_class_impls::get_class_hash_at_impl(self, block_id, contract_address)
}
pub fn get_class(
&self,
block_id: &BlockId,
class_hash: ClassHash,
) -> DevnetResult<ContractClass> {
get_class_impls::get_class_impl(self, block_id, class_hash)
}
pub fn get_class_at(
&self,
block_id: &BlockId,
contract_address: ContractAddress,
) -> DevnetResult<ContractClass> {
get_class_impls::get_class_at_impl(self, block_id, contract_address)
}
pub fn call(
&self,
block_id: &BlockId,
contract_address: Felt,
entrypoint_selector: Felt,
calldata: Vec<Felt>,
) -> DevnetResult<Vec<Felt>> {
let state = self.get_state_at(block_id)?;
if !state.is_contract_deployed(&ContractAddress::new(contract_address)?) {
return Err(Error::ContractNotFound);
}
let call = CallEntryPoint {
calldata: starknet_api::transaction::Calldata(std::sync::Arc::new(
calldata.iter().map(|f| f.into()).collect(),
)),
storage_address: starknet_api::hash::StarkFelt::from(contract_address).try_into()?,
entry_point_selector: starknet_api::core::EntryPointSelector(
entrypoint_selector.into(),
),
initial_gas: self.block_context.versioned_constants().tx_initial_gas(),
..Default::default()
};
let mut execution_context =
blockifier::execution::entry_point::EntryPointExecutionContext::new(
Arc::new(TransactionContext {
block_context: self.block_context.clone(),
tx_info: blockifier::transaction::objects::TransactionInfo::Deprecated(
blockifier::transaction::objects::DeprecatedTransactionInfo::default(),
),
}),
blockifier::execution::common_hints::ExecutionMode::Execute,
true,
)?;
let res = call
.execute(&mut state.clone().state, &mut Default::default(), &mut execution_context)
.map_err(|err| {
Error::BlockifierTransactionError(
blockifier::transaction::errors::TransactionExecutionError::ExecutionError(err),
)
})?;
Ok(res.execution.retdata.0.into_iter().map(Felt::from).collect())
}
pub fn estimate_fee(
&self,
block_id: &BlockId,
transactions: &[BroadcastedTransaction],
simulation_flags: &[SimulationFlag],
) -> DevnetResult<Vec<FeeEstimateWrapper>> {
let mut skip_validate = false;
for flag in simulation_flags.iter() {
if *flag == SimulationFlag::SkipValidate {
skip_validate = true;
}
}
estimations::estimate_fee(self, block_id, transactions, None, Some(!skip_validate))
}
pub fn estimate_message_fee(
&self,
block_id: &BlockId,
message: MsgFromL1,
) -> DevnetResult<FeeEstimateWrapper> {
estimations::estimate_message_fee(self, block_id, message)
}
pub fn add_declare_transaction_v1(
&mut self,
declare_transaction: BroadcastedDeclareTransactionV1,
) -> DevnetResult<(TransactionHash, ClassHash)> {
add_declare_transaction::add_declare_transaction_v1(self, declare_transaction)
}
pub fn add_declare_transaction_v2(
&mut self,
declare_transaction: BroadcastedDeclareTransactionV2,
) -> DevnetResult<(TransactionHash, ClassHash)> {
add_declare_transaction::add_declare_transaction_v2(self, declare_transaction)
}
pub fn add_declare_transaction_v3(
&mut self,
declare_transaction: BroadcastedDeclareTransactionV3,
) -> DevnetResult<(TransactionHash, ClassHash)> {
add_declare_transaction::add_declare_transaction_v3(self, declare_transaction)
}
pub fn chain_id(&self) -> ChainId {
self.config.chain_id
}
pub fn add_deploy_account_transaction_v1(
&mut self,
deploy_account_transaction: BroadcastedDeployAccountTransactionV1,
) -> DevnetResult<(TransactionHash, ContractAddress)> {
add_deploy_account_transaction::add_deploy_account_transaction_v1(
self,
deploy_account_transaction,
)
}
pub fn add_deploy_account_transaction_v3(
&mut self,
deploy_account_transaction: BroadcastedDeployAccountTransactionV3,
) -> DevnetResult<(TransactionHash, ContractAddress)> {
add_deploy_account_transaction::add_deploy_account_transaction_v3(
self,
deploy_account_transaction,
)
}
pub fn add_invoke_transaction_v1(
&mut self,
invoke_transaction: BroadcastedInvokeTransactionV1,
) -> DevnetResult<TransactionHash> {
add_invoke_transaction::add_invoke_transaction_v1(self, invoke_transaction)
}
pub fn add_invoke_transaction_v3(
&mut self,
invoke_transaction: BroadcastedInvokeTransactionV3,
) -> DevnetResult<TransactionHash> {
add_invoke_transaction::add_invoke_transaction_v3(self, invoke_transaction)
}
pub fn add_l1_handler_transaction(
&mut self,
l1_handler_transaction: L1HandlerTransaction,
) -> DevnetResult<TransactionHash> {
add_l1_handler_transaction::add_l1_handler_transaction(self, l1_handler_transaction)
}
pub async fn mint(
&mut self,
address: ContractAddress,
amount: u128,
erc20_address: ContractAddress,
) -> DevnetResult<Felt> {
let sufficiently_big_max_fee = self.config.gas_price.get() * 1_000_000;
let chargeable_address_felt = Felt::from_prefixed_hex_str(CHARGEABLE_ACCOUNT_ADDRESS)?;
let nonce =
self.state.state.get_nonce_at(starknet_api::core::ContractAddress::try_from(
starknet_api::hash::StarkFelt::from(chargeable_address_felt),
)?)?;
let calldata = vec![
Felt::from(address).into(),
FieldElement::from(amount), FieldElement::from(0u32), ];
let raw_execution = RawExecution {
calls: vec![Call {
to: erc20_address.into(),
selector: get_selector_from_name("mint").unwrap(),
calldata: calldata.clone(),
}],
nonce: Felt::from(nonce.0).into(),
max_fee: FieldElement::from(sufficiently_big_max_fee),
};
let chain_id_felt: Felt = self.config.chain_id.to_felt();
let msg_hash_felt =
raw_execution.transaction_hash(chain_id_felt.into(), chargeable_address_felt.into());
let signer = starknet_rs_signers::LocalWallet::from(
starknet_rs_signers::SigningKey::from_secret_scalar(
FieldElement::from_hex_be(CHARGEABLE_ACCOUNT_PRIVATE_KEY).unwrap(),
),
);
let signature = signer.sign_hash(&msg_hash_felt).await?;
let invoke_tx = BroadcastedInvokeTransactionV1 {
sender_address: ContractAddress::new(chargeable_address_felt)?,
calldata: raw_execution.raw_calldata().into_iter().map(|c| c.into()).collect(),
common: BroadcastedTransactionCommon {
max_fee: Fee(sufficiently_big_max_fee),
version: Felt::from(1),
signature: vec![signature.r.into(), signature.s.into()],
nonce: nonce.0.into(),
},
};
add_invoke_transaction::add_invoke_transaction_v1(self, invoke_tx)
}
pub fn block_state_update(&self, block_id: &BlockId) -> DevnetResult<StateUpdate> {
state_update::state_update_by_block_id(self, block_id)
}
pub fn get_block_txs_count(&self, block_id: &BlockId) -> DevnetResult<u64> {
let block = self.blocks.get_by_block_id(block_id).ok_or(Error::NoBlock)?;
Ok(block.get_transactions().len() as u64)
}
pub fn contract_nonce_at_block(
&self,
block_id: &BlockId,
contract_address: ContractAddress,
) -> DevnetResult<Felt> {
let state = self.get_state_at(block_id)?;
state.get_nonce(&contract_address)
}
pub fn contract_storage_at_block(
&self,
block_id: &BlockId,
contract_address: ContractAddress,
storage_key: PatriciaKey,
) -> DevnetResult<Felt> {
let state = self.get_state_at(block_id)?;
state.get_storage(ContractStorageKey::new(contract_address, storage_key))
}
pub fn get_block(&self, block_id: &BlockId) -> DevnetResult<StarknetBlock> {
let block = self.blocks.get_by_block_id(block_id).ok_or(Error::NoBlock)?;
Ok(block.clone())
}
pub fn get_block_with_transactions(&self, block_id: &BlockId) -> DevnetResult<Block> {
let block = self.blocks.get_by_block_id(block_id).ok_or(Error::NoBlock)?;
let transactions = block
.get_transactions()
.iter()
.map(|transaction_hash| {
self.transactions
.get_by_hash(*transaction_hash)
.ok_or(Error::NoTransaction)
.map(|transaction| transaction.inner.clone())
})
.collect::<DevnetResult<Vec<Transaction>>>()?;
Ok(Block {
status: *block.status(),
header: BlockHeader::from(block),
transactions: Transactions::Full(transactions),
})
}
pub fn get_block_with_receipts(&self, block_id: BlockId) -> DevnetResult<Block> {
let block = self.blocks.get_by_block_id(&block_id).ok_or(Error::NoBlock)?;
let mut transaction_receipts: Vec<TransactionWithReceipt> = vec![];
for transaction_hash in block.get_transactions() {
let sn_transaction =
self.transactions.get_by_hash(*transaction_hash).ok_or(Error::NoTransaction)?;
let transaction = sn_transaction.inner.clone();
let mut receipt = sn_transaction.get_receipt()?;
let common_field = match receipt {
TransactionReceipt::Deploy(DeployTransactionReceipt { ref mut common, .. })
| TransactionReceipt::L1Handler(L1HandlerTransactionReceipt {
ref mut common,
..
})
| TransactionReceipt::Common(ref mut common) => common,
};
common_field.maybe_pending_properties.block_hash = None;
common_field.maybe_pending_properties.block_number = None;
transaction_receipts.push(TransactionWithReceipt { receipt, transaction });
}
Ok(Block {
status: *block.status(),
header: BlockHeader::from(block),
transactions: Transactions::FullWithReceipts(transaction_receipts),
})
}
pub fn get_transaction_by_block_id_and_index(
&self,
block_id: &BlockId,
index: u64,
) -> DevnetResult<&Transaction> {
let block = self.get_block(block_id)?;
let transaction_hash = block
.get_transactions()
.get(index as usize)
.ok_or(Error::InvalidTransactionIndexInBlock)?;
self.get_transaction_by_hash(*transaction_hash)
}
pub fn get_latest_block(&self) -> DevnetResult<StarknetBlock> {
let block = self
.blocks
.get_by_block_id(&BlockId::Tag(starknet_rs_core::types::BlockTag::Latest))
.ok_or(crate::error::Error::NoBlock)?;
Ok(block.clone())
}
pub fn get_transaction_by_hash(&self, transaction_hash: Felt) -> DevnetResult<&Transaction> {
self.transactions
.get_by_hash(transaction_hash)
.map(|starknet_transaction| &starknet_transaction.inner)
.ok_or(Error::NoTransaction)
}
pub fn get_events(
&self,
from_block: Option<BlockId>,
to_block: Option<BlockId>,
address: Option<ContractAddress>,
keys: Option<Vec<Vec<Felt>>>,
skip: usize,
limit: Option<usize>,
) -> DevnetResult<(Vec<EmittedEvent>, bool)> {
events::get_events(self, from_block, to_block, address, keys, skip, limit)
}
pub fn get_transaction_receipt_by_hash(
&self,
transaction_hash: &TransactionHash,
) -> DevnetResult<TransactionReceipt> {
let transaction_to_map =
self.transactions.get(transaction_hash).ok_or(Error::NoTransaction)?;
transaction_to_map.get_receipt()
}
pub fn get_transaction_trace_by_hash(
&self,
transaction_hash: TransactionHash,
) -> DevnetResult<TransactionTrace> {
let tx = self.transactions.get(&transaction_hash).ok_or(Error::NoTransaction)?;
tx.get_trace().ok_or(Error::NoTransactionTrace)
}
pub fn get_transaction_traces_from_block(
&self,
block_id: &BlockId,
) -> DevnetResult<Vec<BlockTransactionTrace>> {
let transactions = self.get_block_with_transactions(block_id)?.transactions;
let mut traces = Vec::new();
if let Transactions::Full(txs) = transactions {
for tx in txs {
let tx_hash = *tx.get_transaction_hash();
let trace = self.get_transaction_trace_by_hash(tx_hash)?;
let block_trace =
BlockTransactionTrace { transaction_hash: tx_hash, trace_root: trace };
traces.push(block_trace);
}
}
Ok(traces)
}
pub fn get_transaction_execution_and_finality_status(
&self,
transaction_hash: TransactionHash,
) -> DevnetResult<(TransactionExecutionStatus, TransactionFinalityStatus)> {
let transaction = self.transactions.get(&transaction_hash).ok_or(Error::NoTransaction)?;
Ok((transaction.execution_result.status(), transaction.finality_status))
}
pub fn simulate_transactions(
&mut self,
block_id: &BlockId,
transactions: &[BroadcastedTransaction],
simulation_flags: Vec<SimulationFlag>,
) -> DevnetResult<Vec<SimulatedTransaction>> {
let mut state = self.get_state_at(block_id)?.clone();
let chain_id = self.chain_id().to_felt();
let mut skip_validate = false;
let mut skip_fee_charge = false;
for flag in simulation_flags.iter() {
match flag {
SimulationFlag::SkipValidate => {
skip_validate = true;
}
SimulationFlag::SkipFeeCharge => skip_fee_charge = true,
}
}
let mut transactions_traces: Vec<TransactionTrace> = vec![];
for broadcasted_transaction in transactions.iter() {
let blockifier_transaction =
broadcasted_transaction.to_blockifier_account_transaction(chain_id, true)?;
let tx_execution_info = blockifier_transaction.execute(
&mut state.state,
&self.block_context,
!skip_fee_charge,
!skip_validate,
)?;
let state_diff: ThinStateDiff = state.extract_state_diff_from_pending_state()?.into();
let trace = create_trace(
&mut state.state,
broadcasted_transaction.get_type(),
&tx_execution_info,
state_diff,
)?;
transactions_traces.push(trace);
}
let estimated = estimations::estimate_fee(
self,
block_id,
transactions,
Some(!skip_fee_charge),
Some(!skip_validate),
)?;
if transactions_traces.len() != estimated.len() {
return Err(Error::UnexpectedInternalError {
msg: format!(
"Non-matching number of simulations ({}) and estimations ({})",
transactions_traces.len(),
estimated.len()
),
});
}
let simulation_results = transactions_traces
.into_iter()
.zip(estimated)
.map(|(trace, fee_estimation)| SimulatedTransaction {
transaction_trace: trace,
fee_estimation,
})
.collect();
Ok(simulation_results)
}
pub fn create_block(&mut self) -> DevnetResult<(), Error> {
self.generate_new_block(StateDiff::default())?;
Ok(())
}
pub fn create_block_dump_event(
&mut self,
dump_event: Option<DumpEvent>,
) -> DevnetResult<(), Error> {
self.create_block()?;
match dump_event {
Some(event) => self.handle_dump_event(event)?,
None => self.handle_dump_event(DumpEvent::CreateBlock)?,
}
Ok(())
}
pub fn set_time(&mut self, timestamp: u64, create_block: bool) -> DevnetResult<(), Error> {
self.set_block_timestamp_shift(
timestamp as i64 - Starknet::get_unix_timestamp_as_seconds() as i64,
);
if create_block {
self.set_next_block_timestamp(timestamp);
self.create_block()?;
self.handle_dump_event(DumpEvent::SetTime(timestamp))?;
self.handle_dump_event(DumpEvent::CreateBlock)?;
} else {
self.set_next_block_timestamp(timestamp);
self.handle_dump_event(DumpEvent::SetTime(timestamp))?;
}
Ok(())
}
pub fn increase_time(&mut self, time_shift: u64) -> DevnetResult<(), Error> {
self.set_block_timestamp_shift(self.pending_block_timestamp_shift + time_shift as i64);
self.create_block_dump_event(Some(DumpEvent::IncreaseTime(time_shift)))
}
pub fn set_block_timestamp_shift(&mut self, timestamp: i64) {
self.pending_block_timestamp_shift = timestamp;
}
pub fn set_next_block_timestamp(&mut self, timestamp: u64) {
self.next_block_timestamp = Some(timestamp);
}
pub fn get_unix_timestamp_as_seconds() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("should get current UNIX timestamp")
.as_secs()
}
}
#[cfg(test)]
mod tests {
use std::thread;
use std::time::Duration;
use blockifier::state::state_api::State;
use blockifier::transaction::errors::TransactionExecutionError;
use nonzero_ext::nonzero;
use starknet_api::block::{BlockHash, BlockNumber, BlockStatus, BlockTimestamp, GasPrice};
use starknet_rs_core::types::{BlockId, BlockTag};
use starknet_types::contract_address::ContractAddress;
use starknet_types::felt::Felt;
use super::Starknet;
use crate::account::FeeToken;
use crate::blocks::StarknetBlock;
use crate::constants::{
DEVNET_DEFAULT_CHAIN_ID, DEVNET_DEFAULT_INITIAL_BALANCE, ETH_ERC20_CONTRACT_ADDRESS,
STRK_ERC20_CONTRACT_ADDRESS,
};
use crate::error::{DevnetResult, Error};
use crate::starknet::starknet_config::{StarknetConfig, StateArchiveCapacity};
use crate::state::state_diff::StateDiff;
use crate::traits::{Accounted, StateChanger, StateExtractor};
use crate::utils::test_utils::{
dummy_contract_address, dummy_declare_transaction_v1, dummy_felt,
};
#[test]
fn correct_initial_state_with_test_config() {
let config = StarknetConfig::default();
let mut starknet = Starknet::new(&config).unwrap();
let predeployed_accounts = starknet.predeployed_accounts.get_accounts();
let expected_balance = config.predeployed_accounts_initial_balance;
for account in predeployed_accounts {
let account_balance = account.get_balance(&mut starknet.state, FeeToken::ETH).unwrap();
assert_eq!(expected_balance, account_balance);
let account_balance = account.get_balance(&mut starknet.state, FeeToken::STRK).unwrap();
assert_eq!(expected_balance, account_balance);
}
}
#[test]
fn correct_block_context_creation() {
let fee_token_address =
ContractAddress::new(Felt::from_prefixed_hex_str("0xAA").unwrap()).unwrap();
let block_ctx = Starknet::init_block_context(
nonzero!(10u128),
"0xAA",
STRK_ERC20_CONTRACT_ADDRESS,
DEVNET_DEFAULT_CHAIN_ID,
);
assert_eq!(block_ctx.block_info().block_number, BlockNumber(0));
assert_eq!(block_ctx.block_info().block_timestamp, BlockTimestamp(0));
assert_eq!(block_ctx.block_info().gas_prices.eth_l1_gas_price.get(), 10);
assert_eq!(
ContractAddress::from(block_ctx.chain_info().fee_token_addresses.eth_fee_token_address),
fee_token_address
);
}
#[test]
fn pending_block_is_correct() {
let config = StarknetConfig::default();
let mut starknet = Starknet::new(&config).unwrap();
let initial_block_number = starknet.block_context.block_info().block_number;
starknet.generate_pending_block().unwrap();
assert_eq!(starknet.pending_block().header.block_number, initial_block_number.next());
}
#[test]
fn correct_new_block_creation() {
let config = StarknetConfig::default();
let mut starknet = Starknet::new(&config).unwrap();
let tx = dummy_declare_transaction_v1();
starknet.blocks.pending_block.add_transaction(tx.transaction_hash);
assert!(!starknet.pending_block().get_transactions().is_empty());
assert!(starknet.blocks.num_to_block.is_empty());
starknet.generate_new_block(StateDiff::default()).unwrap();
assert!(!starknet.blocks.num_to_block.is_empty());
let added_block = starknet.blocks.num_to_block.get(&BlockNumber(0)).unwrap();
assert!(added_block.get_transactions().len() == 1);
assert_eq!(*added_block.get_transactions().first().unwrap(), tx.transaction_hash);
}
#[test]
fn successful_emptying_of_pending_block() {
let config = StarknetConfig::default();
let mut starknet = Starknet::new(&config).unwrap();
let initial_block_number = starknet.block_context.block_info().block_number;
let initial_gas_price = starknet.block_context.block_info().gas_prices.eth_l1_gas_price;
let initial_block_timestamp = starknet.block_context.block_info().block_timestamp;
let initial_sequencer = starknet.block_context.block_info().sequencer_address;
let mut pending_block = StarknetBlock::create_pending_block();
pending_block.add_transaction(dummy_felt());
pending_block.status = BlockStatus::AcceptedOnL2;
starknet.blocks.pending_block = pending_block.clone();
assert!(*starknet.pending_block() == pending_block);
starknet.restart_pending_block().unwrap();
assert!(*starknet.pending_block() != pending_block);
assert_eq!(starknet.pending_block().status, BlockStatus::Pending);
assert!(starknet.pending_block().get_transactions().is_empty());
assert_eq!(starknet.pending_block().header.timestamp, initial_block_timestamp);
assert_eq!(starknet.pending_block().header.block_number, initial_block_number);
assert_eq!(starknet.pending_block().header.parent_hash, BlockHash::default());
assert_eq!(
starknet.pending_block().header.l1_gas_price.price_in_wei,
GasPrice(initial_gas_price.get())
);
assert_eq!(starknet.pending_block().header.sequencer.0, initial_sequencer);
}
#[test]
fn correct_block_context_update() {
let mut block_ctx = Starknet::init_block_context(
nonzero!(1u128),
ETH_ERC20_CONTRACT_ADDRESS,
STRK_ERC20_CONTRACT_ADDRESS,
DEVNET_DEFAULT_CHAIN_ID,
);
let initial_block_number = block_ctx.block_info().block_number;
Starknet::advance_block_context_block_number(&mut block_ctx);
assert_eq!(block_ctx.block_info().block_number, initial_block_number.next());
}
#[test]
fn getting_state_of_latest_block() {
let config = StarknetConfig::default();
let starknet = Starknet::new(&config).unwrap();
starknet.get_state_at(&BlockId::Tag(BlockTag::Latest)).expect("Should be OK");
}
#[test]
fn getting_state_of_pending_block() {
let config = StarknetConfig::default();
let starknet = Starknet::new(&config).unwrap();
starknet.get_state_at(&BlockId::Tag(BlockTag::Pending)).expect("Should be OK");
}
#[test]
fn getting_state_at_block_by_nonexistent_hash_with_full_state_archive() {
let config =
StarknetConfig { state_archive: StateArchiveCapacity::Full, ..Default::default() };
let mut starknet = Starknet::new(&config).unwrap();
starknet.generate_new_block(StateDiff::default()).unwrap();
match starknet.get_state_at(&BlockId::Hash(Felt::from(0).into())) {
Err(Error::NoBlock) => (),
_ => panic!("Should fail with NoBlock"),
}
}
#[test]
fn getting_nonexistent_state_at_block_by_number_with_full_state_archive() {
let config =
StarknetConfig { state_archive: StateArchiveCapacity::Full, ..Default::default() };
let mut starknet = Starknet::new(&config).unwrap();
starknet.generate_new_block(StateDiff::default()).unwrap();
starknet.blocks.num_to_state.remove(&BlockNumber(0));
match starknet.get_state_at(&BlockId::Number(0)) {
Err(Error::NoStateAtBlock { block_number: _ }) => (),
_ => panic!("Should fail with NoStateAtBlock"),
}
}
#[test]
fn getting_state_at_without_state_archive() {
let config = StarknetConfig::default();
let mut starknet = Starknet::new(&config).unwrap();
starknet.generate_new_block(StateDiff::default()).unwrap();
match starknet.get_state_at(&BlockId::Number(0)) {
Err(Error::StateHistoryDisabled) => (),
_ => panic!("Should fail with StateHistoryDisabled."),
}
}
#[test]
fn calling_method_of_undeployed_contract() {
let config = StarknetConfig::default();
let starknet = Starknet::new(&config).unwrap();
let undeployed_address_hex = "0x1234";
let undeployed_address = Felt::from_prefixed_hex_str(undeployed_address_hex).unwrap();
let entry_point_selector =
starknet_rs_core::utils::get_selector_from_name("balanceOf").unwrap();
match starknet.call(
&BlockId::Tag(BlockTag::Latest),
undeployed_address,
entry_point_selector.into(),
vec![],
) {
Err(Error::ContractNotFound) => (),
unexpected => panic!("Should have failed; got {unexpected:?}"),
}
}
#[test]
fn calling_nonexistent_contract_method() {
let config = StarknetConfig::default();
let starknet = Starknet::new(&config).unwrap();
let predeployed_account = &starknet.predeployed_accounts.get_accounts()[0];
let entry_point_selector =
starknet_rs_core::utils::get_selector_from_name("nonExistentMethod").unwrap();
match starknet.call(
&BlockId::Tag(BlockTag::Latest),
Felt::from_prefixed_hex_str(ETH_ERC20_CONTRACT_ADDRESS).unwrap(),
entry_point_selector.into(),
vec![Felt::from(predeployed_account.account_address)],
) {
Err(Error::BlockifierTransactionError(TransactionExecutionError::ExecutionError(
blockifier::execution::errors::EntryPointExecutionError::PreExecutionError(
blockifier::execution::errors::PreExecutionError::EntryPointNotFound(_),
),
))) => (),
unexpected => panic!("Should have failed; got {unexpected:?}"),
}
}
fn get_balance_at(
starknet: &Starknet,
contract_address: ContractAddress,
) -> DevnetResult<Vec<Felt>> {
let entry_point_selector =
starknet_rs_core::utils::get_selector_from_name("balanceOf").unwrap();
starknet.call(
&BlockId::Tag(BlockTag::Latest),
Felt::from_prefixed_hex_str(ETH_ERC20_CONTRACT_ADDRESS)?,
entry_point_selector.into(),
vec![Felt::from(contract_address)],
)
}
#[test]
fn getting_balance_of_predeployed_contract() {
let config = StarknetConfig::default();
let starknet = Starknet::new(&config).unwrap();
let predeployed_account = &starknet.predeployed_accounts.get_accounts()[0];
let result = get_balance_at(&starknet, predeployed_account.account_address).unwrap();
let balance_hex = format!("0x{:x}", DEVNET_DEFAULT_INITIAL_BALANCE);
let balance_felt = Felt::from_prefixed_hex_str(balance_hex.as_str()).unwrap();
let balance_uint256 = vec![balance_felt, Felt::from_prefixed_hex_str("0x0").unwrap()];
assert_eq!(result, balance_uint256);
}
#[test]
fn getting_balance_of_undeployed_contract() {
let config = StarknetConfig::default();
let starknet = Starknet::new(&config).unwrap();
let undeployed_address =
ContractAddress::new(Felt::from_prefixed_hex_str("0x1234").unwrap()).unwrap();
let result = get_balance_at(&starknet, undeployed_address).unwrap();
let zero = Felt::from_prefixed_hex_str("0x0").unwrap();
let expected_balance_uint256 = vec![zero, zero];
assert_eq!(result, expected_balance_uint256);
}
#[test]
fn correct_latest_block() {
let config = StarknetConfig::default();
let mut starknet = Starknet::new(&config).unwrap();
starknet.get_latest_block().err().unwrap();
starknet.generate_new_block(StateDiff::default()).unwrap();
let added_block = starknet.blocks.num_to_block.get(&BlockNumber(0)).unwrap();
let block_number = starknet.get_latest_block().unwrap().block_number();
assert_eq!(block_number.0, added_block.header.block_number.0);
starknet.generate_new_block(StateDiff::default()).unwrap();
let added_block2 = starknet.blocks.num_to_block.get(&BlockNumber(1)).unwrap();
let block_number2 = starknet.get_latest_block().unwrap().block_number();
assert_eq!(block_number2.0, added_block2.header.block_number.0);
}
#[test]
fn gets_block_txs_count() {
let config = StarknetConfig::default();
let mut starknet = Starknet::new(&config).unwrap();
starknet.generate_new_block(StateDiff::default()).unwrap();
let num_no_transactions = starknet.get_block_txs_count(&BlockId::Number(0));
assert_eq!(num_no_transactions.unwrap(), 0);
let tx = dummy_declare_transaction_v1();
starknet.blocks.pending_block.add_transaction(tx.transaction_hash);
starknet.generate_new_block(StateDiff::default()).unwrap();
let num_one_transaction = starknet.get_block_txs_count(&BlockId::Number(1));
assert_eq!(num_one_transaction.unwrap(), 1);
}
#[test]
fn returns_chain_id() {
let config = StarknetConfig::default();
let starknet = Starknet::new(&config).unwrap();
let chain_id = starknet.chain_id();
assert_eq!(chain_id.to_string(), DEVNET_DEFAULT_CHAIN_ID.to_string());
}
#[test]
fn correct_state_at_specific_block() {
let mut starknet = Starknet::new(&StarknetConfig {
state_archive: StateArchiveCapacity::Full,
..Default::default()
})
.expect("Could not start Devnet");
starknet.generate_new_block(StateDiff::default()).unwrap();
starknet.state.state.increment_nonce(dummy_contract_address().try_into().unwrap()).unwrap();
let state_diff = starknet.state.extract_state_diff_from_pending_state().unwrap();
starknet.state.apply_state_difference(state_diff.clone()).unwrap();
let second_block = starknet.generate_new_block(state_diff).unwrap();
starknet.state.state.increment_nonce(dummy_contract_address().try_into().unwrap()).unwrap();
let state_diff = starknet.state.extract_state_diff_from_pending_state().unwrap();
starknet.state.apply_state_difference(state_diff.clone()).unwrap();
let third_block = starknet.generate_new_block(state_diff).unwrap();
let second_block_address_nonce = starknet
.blocks
.num_to_state
.get(&second_block)
.unwrap()
.state
.state
.address_to_nonce
.get(&dummy_contract_address())
.unwrap();
let second_block_expected_address_nonce = Felt::from(1);
assert_eq!(second_block_expected_address_nonce, *second_block_address_nonce);
let third_block_address_nonce = starknet
.blocks
.num_to_state
.get(&third_block)
.unwrap()
.state
.state
.address_to_nonce
.get(&dummy_contract_address())
.unwrap();
let third_block_expected_address_nonce = Felt::from(2);
assert_eq!(third_block_expected_address_nonce, *third_block_address_nonce);
}
#[test]
fn gets_latest_block() {
let config = StarknetConfig::default();
let mut starknet = Starknet::new(&config).unwrap();
starknet.generate_new_block(StateDiff::default()).unwrap();
starknet.generate_new_block(StateDiff::default()).unwrap();
starknet.generate_new_block(StateDiff::default()).unwrap();
let latest_block = starknet.get_latest_block();
assert_eq!(latest_block.unwrap().block_number(), BlockNumber(2));
}
#[test]
fn check_timestamp_of_newly_generated_block() {
let config = StarknetConfig::default();
let mut starknet = Starknet::new(&config).unwrap();
starknet.generate_new_block(StateDiff::default()).unwrap();
starknet
.blocks
.pending_block
.set_timestamp(BlockTimestamp(Starknet::get_unix_timestamp_as_seconds()));
let pending_block_timestamp = starknet.pending_block().header.timestamp;
let sleep_duration_secs = 5;
thread::sleep(Duration::from_secs(sleep_duration_secs));
starknet.generate_new_block(StateDiff::default()).unwrap();
let block_timestamp = starknet.get_latest_block().unwrap().header.timestamp;
assert!(pending_block_timestamp.0 + sleep_duration_secs <= block_timestamp.0);
}
}