use super::fee::charge_fee;
use super::{verify_version, Transaction};
use crate::core::contract_address::{compute_casm_class_hash, compute_sierra_class_hash};
use crate::definitions::constants::QUERY_VERSION_BASE;
use crate::execution::execution_entry_point::ExecutionResult;
use crate::services::api::contract_classes::deprecated_contract_class::EntryPointType;
use crate::services::api::contract_classes::compiled_class::CompiledClass;
use crate::state::cached_state::CachedState;
use crate::{
core::transaction_hash::calculate_declare_v2_transaction_hash,
definitions::{
block_context::BlockContext,
constants::{INITIAL_GAS_COST, VALIDATE_DECLARE_ENTRY_POINT_SELECTOR},
transaction_type::TransactionType,
},
execution::{
execution_entry_point::ExecutionEntryPoint, CallType, TransactionExecutionContext,
TransactionExecutionInfo,
},
state::state_api::{State, StateReader},
state::ExecutionResourcesManager,
transaction::{error::TransactionError, invoke_function::verify_no_calls_to_other_contracts},
utils::{calculate_tx_resources, Address},
};
use cairo_lang_starknet::casm_contract_class::CasmContractClass;
use cairo_lang_starknet::contract_class::ContractClass as SierraContractClass;
use cairo_vm::felt::Felt252;
use num_traits::Zero;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct DeclareV2 {
pub sender_address: Address,
pub validate_entry_point_selector: Felt252,
pub version: Felt252,
pub max_fee: u128,
pub signature: Vec<Felt252>,
pub nonce: Felt252,
pub compiled_class_hash: Felt252,
pub sierra_contract_class: SierraContractClass,
pub sierra_class_hash: Felt252,
pub hash_value: Felt252,
pub casm_class: Option<CasmContractClass>,
pub skip_validate: bool,
pub skip_execute: bool,
pub skip_fee_transfer: bool,
}
impl DeclareV2 {
#[allow(clippy::too_many_arguments)]
pub fn new(
sierra_contract_class: &SierraContractClass,
casm_contract_class: Option<CasmContractClass>,
compiled_class_hash: Felt252,
chain_id: Felt252,
sender_address: Address,
max_fee: u128,
version: Felt252,
signature: Vec<Felt252>,
nonce: Felt252,
) -> Result<Self, TransactionError> {
let sierra_class_hash = compute_sierra_class_hash(sierra_contract_class)?;
let hash_value = calculate_declare_v2_transaction_hash(
sierra_class_hash.clone(),
compiled_class_hash.clone(),
chain_id,
&sender_address,
max_fee,
version.clone(),
nonce.clone(),
)?;
Self::new_with_sierra_class_hash_and_tx_hash(
sierra_contract_class,
sierra_class_hash,
casm_contract_class,
compiled_class_hash,
sender_address,
max_fee,
version,
signature,
nonce,
hash_value,
)
}
#[allow(clippy::too_many_arguments)]
pub fn new_with_sierra_class_hash_and_tx_hash(
sierra_contract_class: &SierraContractClass,
sierra_class_hash: Felt252,
casm_contract_class: Option<CasmContractClass>,
compiled_class_hash: Felt252,
sender_address: Address,
max_fee: u128,
version: Felt252,
signature: Vec<Felt252>,
nonce: Felt252,
hash_value: Felt252,
) -> Result<Self, TransactionError> {
let validate_entry_point_selector = VALIDATE_DECLARE_ENTRY_POINT_SELECTOR.clone();
let internal_declare = DeclareV2 {
sierra_contract_class: sierra_contract_class.to_owned(),
sierra_class_hash,
sender_address,
validate_entry_point_selector,
version,
max_fee,
signature,
nonce,
compiled_class_hash,
hash_value,
casm_class: casm_contract_class,
skip_execute: false,
skip_validate: false,
skip_fee_transfer: false,
};
verify_version(
&internal_declare.version,
internal_declare.max_fee,
&internal_declare.nonce,
&internal_declare.signature,
)?;
Ok(internal_declare)
}
#[allow(clippy::too_many_arguments)]
pub fn new_with_tx_hash(
sierra_contract_class: &SierraContractClass,
casm_contract_class: Option<CasmContractClass>,
compiled_class_hash: Felt252,
sender_address: Address,
max_fee: u128,
version: Felt252,
signature: Vec<Felt252>,
nonce: Felt252,
hash_value: Felt252,
) -> Result<Self, TransactionError> {
let sierra_class_hash = compute_sierra_class_hash(sierra_contract_class)?;
Self::new_with_sierra_class_hash_and_tx_hash(
sierra_contract_class,
sierra_class_hash,
casm_contract_class,
compiled_class_hash,
sender_address,
max_fee,
version,
signature,
nonce,
hash_value,
)
}
#[allow(clippy::too_many_arguments)]
pub fn new_with_sierra_class_hash(
sierra_contract_class: &SierraContractClass,
sierra_class_hash: Felt252,
casm_contract_class: Option<CasmContractClass>,
compiled_class_hash: Felt252,
chain_id: Felt252,
sender_address: Address,
max_fee: u128,
version: Felt252,
signature: Vec<Felt252>,
nonce: Felt252,
) -> Result<Self, TransactionError> {
let hash_value = calculate_declare_v2_transaction_hash(
sierra_class_hash.clone(),
compiled_class_hash.clone(),
chain_id,
&sender_address,
max_fee,
version.clone(),
nonce.clone(),
)?;
Self::new_with_sierra_class_hash_and_tx_hash(
sierra_contract_class,
sierra_class_hash,
casm_contract_class,
compiled_class_hash,
sender_address,
max_fee,
version,
signature,
nonce,
hash_value,
)
}
pub fn get_execution_context(&self, n_steps: u64) -> TransactionExecutionContext {
TransactionExecutionContext::new(
self.sender_address.clone(),
self.hash_value.clone(),
self.signature.clone(),
self.max_fee,
self.nonce.clone(),
n_steps,
self.version.clone(),
)
}
pub fn get_calldata(&self) -> Vec<Felt252> {
let bytes = self.compiled_class_hash.clone();
Vec::from([bytes])
}
#[allow(dead_code)]
fn handle_nonce<S: State + StateReader>(&self, state: &mut S) -> Result<(), TransactionError> {
if self.version.is_zero() || self.version == *QUERY_VERSION_BASE {
return Ok(());
}
let contract_address = &self.sender_address;
let current_nonce = state.get_nonce_at(contract_address)?;
if current_nonce != self.nonce {
return Err(TransactionError::InvalidTransactionNonce(
current_nonce.to_string(),
self.nonce.to_string(),
));
}
state.increment_nonce(contract_address)?;
Ok(())
}
pub fn execute<S: StateReader>(
&self,
state: &mut CachedState<S>,
block_context: &BlockContext,
) -> Result<TransactionExecutionInfo, TransactionError> {
verify_version(&self.version, self.max_fee, &self.nonce, &self.signature)?;
let initial_gas = INITIAL_GAS_COST;
let mut resources_manager = ExecutionResourcesManager::default();
let (execution_result, _remaining_gas) = if self.skip_validate {
(ExecutionResult::default(), 0)
} else {
let (info, gas) = self.run_validate_entrypoint(
initial_gas,
state,
&mut resources_manager,
block_context,
)?;
(info, gas)
};
let storage_changes = state.count_actual_storage_changes(Some((
&block_context.starknet_os_config.fee_token_address,
&self.sender_address,
)))?;
let actual_resources = calculate_tx_resources(
resources_manager,
&[execution_result.call_info.clone()],
TransactionType::Declare,
storage_changes,
None,
execution_result.n_reverted_steps,
)?;
let mut tx_execution_context =
self.get_execution_context(block_context.invoke_tx_max_n_steps);
let (fee_transfer_info, actual_fee) = charge_fee(
state,
&actual_resources,
block_context,
self.max_fee,
&mut tx_execution_context,
self.skip_fee_transfer,
)?;
self.compile_and_store_casm_class(state)?;
let mut tx_exec_info = TransactionExecutionInfo::new_without_fee_info(
execution_result.call_info,
None,
None,
actual_resources,
Some(TransactionType::Declare),
);
tx_exec_info.set_fee_info(actual_fee, fee_transfer_info);
Ok(tx_exec_info)
}
pub(crate) fn compile_and_store_casm_class<S: State + StateReader>(
&self,
state: &mut S,
) -> Result<(), TransactionError> {
let casm_class = match &self.casm_class {
None => {
CasmContractClass::from_contract_class(self.sierra_contract_class.clone(), true)
.map_err(|e| TransactionError::SierraCompileError(e.to_string()))?
}
Some(casm_contract_class) => casm_contract_class.clone(),
};
let casm_class_hash = compute_casm_class_hash(&casm_class)?;
if casm_class_hash != self.compiled_class_hash {
return Err(TransactionError::InvalidCompiledClassHash(
casm_class_hash.to_string(),
self.compiled_class_hash.to_string(),
));
}
state.set_compiled_class_hash(&self.sierra_class_hash, &self.compiled_class_hash)?;
state.set_compiled_class_hash(&self.compiled_class_hash, &self.compiled_class_hash)?;
state.set_contract_class(
&self.compiled_class_hash.to_be_bytes(),
&CompiledClass::Casm(Arc::new(casm_class)),
)?;
Ok(())
}
fn run_validate_entrypoint<S: StateReader>(
&self,
mut remaining_gas: u128,
state: &mut CachedState<S>,
resources_manager: &mut ExecutionResourcesManager,
block_context: &BlockContext,
) -> Result<(ExecutionResult, u128), TransactionError> {
let calldata = [self.compiled_class_hash.clone()].to_vec();
let entry_point = ExecutionEntryPoint {
contract_address: self.sender_address.clone(),
entry_point_selector: self.validate_entry_point_selector.clone(),
initial_gas: remaining_gas,
entry_point_type: EntryPointType::External,
calldata,
caller_address: Address(Felt252::zero()),
code_address: None,
class_hash: None,
call_type: CallType::Call,
};
let mut tx_execution_context =
self.get_execution_context(block_context.validate_max_n_steps);
let execution_result = if self.skip_execute {
ExecutionResult::default()
} else {
entry_point.execute(
state,
block_context,
resources_manager,
&mut tx_execution_context,
true,
block_context.validate_max_n_steps,
)?
};
if execution_result.call_info.is_some() {
verify_no_calls_to_other_contracts(&execution_result.call_info)?;
remaining_gas -= execution_result.call_info.clone().unwrap().gas_consumed;
}
Ok((execution_result, remaining_gas))
}
pub(crate) fn create_for_simulation(
&self,
skip_validate: bool,
skip_execute: bool,
skip_fee_transfer: bool,
ignore_max_fee: bool,
) -> Transaction {
let tx = DeclareV2 {
skip_validate,
skip_execute,
skip_fee_transfer,
max_fee: if ignore_max_fee {
u128::MAX
} else {
self.max_fee
},
..self.clone()
};
Transaction::DeclareV2(Box::new(tx))
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::{collections::HashMap, fs::File, io::BufReader, path::PathBuf};
use super::DeclareV2;
use crate::core::contract_address::{compute_casm_class_hash, compute_sierra_class_hash};
use crate::definitions::constants::QUERY_VERSION_BASE;
use crate::services::api::contract_classes::compiled_class::CompiledClass;
use crate::state::state_api::StateReader;
use crate::{
state::cached_state::CachedState, state::in_memory_state_reader::InMemoryStateReader,
utils::Address,
};
use cairo_lang_starknet::casm_contract_class::CasmContractClass;
use cairo_vm::felt::Felt252;
use num_traits::{One, Zero};
#[test]
fn create_declare_v2_without_casm_contract_class_test() {
let version;
let path;
#[cfg(not(feature = "cairo_1_tests"))]
{
version = Felt252::from(2);
path = PathBuf::from("starknet_programs/cairo2/fibonacci.sierra");
}
#[cfg(feature = "cairo_1_tests")]
{
version = Felt252::from(1);
path = PathBuf::from("starknet_programs/cairo1/fibonacci.sierra");
}
let file = File::open(path).unwrap();
let reader = BufReader::new(file);
let sierra_contract_class: cairo_lang_starknet::contract_class::ContractClass =
serde_json::from_reader(reader).unwrap();
let sender_address = Address(1.into());
let casm_class =
CasmContractClass::from_contract_class(sierra_contract_class.clone(), true).unwrap();
let casm_class_hash = compute_casm_class_hash(&casm_class).unwrap();
let internal_declare = DeclareV2::new_with_tx_hash(
&sierra_contract_class,
None,
casm_class_hash,
sender_address,
0,
version,
[1.into()].to_vec(),
Felt252::zero(),
Felt252::one(),
)
.unwrap();
let casm_contract_class_cache = HashMap::new();
let state_reader = Arc::new(InMemoryStateReader::default());
let mut state = CachedState::new(state_reader, casm_contract_class_cache);
assert!(internal_declare
.compile_and_store_casm_class(&mut state)
.is_ok());
let expected_casm_class = CasmContractClass::from_contract_class(
internal_declare.sierra_contract_class.clone(),
true,
)
.unwrap();
let casm_class = match state
.get_contract_class(&internal_declare.compiled_class_hash.to_be_bytes())
.unwrap()
{
CompiledClass::Casm(casm) => casm.as_ref().clone(),
_ => unreachable!(),
};
assert_eq!(expected_casm_class, casm_class);
}
#[test]
fn create_declare_v2_with_casm_contract_class_test() {
let version;
let path;
#[cfg(not(feature = "cairo_1_tests"))]
{
version = Felt252::from(2);
path = PathBuf::from("starknet_programs/cairo2/fibonacci.sierra");
}
#[cfg(feature = "cairo_1_tests")]
{
version = Felt252::from(1);
path = PathBuf::from("starknet_programs/cairo1/fibonacci.sierra");
}
let file = File::open(path).unwrap();
let reader = BufReader::new(file);
let sierra_contract_class: cairo_lang_starknet::contract_class::ContractClass =
serde_json::from_reader(reader).unwrap();
let sender_address = Address(1.into());
let casm_class =
CasmContractClass::from_contract_class(sierra_contract_class.clone(), true).unwrap();
let casm_class_hash = compute_casm_class_hash(&casm_class).unwrap();
let internal_declare = DeclareV2::new_with_tx_hash(
&sierra_contract_class,
Some(casm_class),
casm_class_hash,
sender_address,
0,
version,
[1.into()].to_vec(),
Felt252::zero(),
Felt252::one(),
)
.unwrap();
let casm_contract_class_cache = HashMap::new();
let state_reader = Arc::new(InMemoryStateReader::default());
let mut state = CachedState::new(state_reader, casm_contract_class_cache);
assert!(internal_declare
.compile_and_store_casm_class(&mut state)
.is_ok());
let expected_casm_class = CasmContractClass::from_contract_class(
internal_declare.sierra_contract_class.clone(),
true,
)
.unwrap();
let casm_class = match state
.get_contract_class(&internal_declare.compiled_class_hash.to_be_bytes())
.unwrap()
{
CompiledClass::Casm(casm) => casm.as_ref().clone(),
_ => unreachable!(),
};
assert_eq!(expected_casm_class, casm_class);
}
#[test]
fn create_declare_v2_test_with_version_query() {
let version;
let path;
#[cfg(not(feature = "cairo_1_tests"))]
{
version = &2.into() | &QUERY_VERSION_BASE.clone();
path = PathBuf::from("starknet_programs/cairo2/fibonacci.sierra");
}
#[cfg(feature = "cairo_1_tests")]
{
version = &1.into() | &QUERY_VERSION_BASE.clone();
path = PathBuf::from("starknet_programs/cairo1/fibonacci.sierra");
}
let file = File::open(path).unwrap();
let reader = BufReader::new(file);
let sierra_contract_class: cairo_lang_starknet::contract_class::ContractClass =
serde_json::from_reader(reader).unwrap();
let sierra_class_hash = compute_sierra_class_hash(&sierra_contract_class).unwrap();
let sender_address = Address(1.into());
let casm_class =
CasmContractClass::from_contract_class(sierra_contract_class.clone(), true).unwrap();
let casm_class_hash = compute_casm_class_hash(&casm_class).unwrap();
let internal_declare = DeclareV2::new_with_sierra_class_hash_and_tx_hash(
&sierra_contract_class,
sierra_class_hash,
Some(casm_class),
casm_class_hash,
sender_address,
0,
version,
vec![],
Felt252::zero(),
Felt252::zero(),
)
.unwrap();
let casm_contract_class_cache = HashMap::new();
let state_reader = Arc::new(InMemoryStateReader::default());
let mut state = CachedState::new(state_reader, casm_contract_class_cache);
assert!(internal_declare
.compile_and_store_casm_class(&mut state)
.is_ok());
let expected_casm_class = CasmContractClass::from_contract_class(
internal_declare.sierra_contract_class.clone(),
true,
)
.unwrap();
let casm_class = match state
.get_contract_class(&internal_declare.compiled_class_hash.to_be_bytes())
.unwrap()
{
CompiledClass::Casm(casm) => casm.as_ref().clone(),
_ => unreachable!(),
};
assert_eq!(expected_casm_class, casm_class);
}
#[test]
fn create_declare_v2_with_casm_contract_class_none_test() {
let version;
let path;
#[cfg(not(feature = "cairo_1_tests"))]
{
version = Felt252::from(2);
path = PathBuf::from("starknet_programs/cairo2/fibonacci.sierra");
}
#[cfg(feature = "cairo_1_tests")]
{
version = Felt252::from(1);
path = PathBuf::from("starknet_programs/cairo1/fibonacci.sierra");
}
let file = File::open(path).unwrap();
let reader = BufReader::new(file);
let sierra_contract_class: cairo_lang_starknet::contract_class::ContractClass =
serde_json::from_reader(reader).unwrap();
let sender_address = Address(1.into());
let casm_class =
CasmContractClass::from_contract_class(sierra_contract_class.clone(), true).unwrap();
let casm_class_hash = compute_casm_class_hash(&casm_class).unwrap();
let internal_declare = DeclareV2::new_with_tx_hash(
&sierra_contract_class,
None,
casm_class_hash,
sender_address,
0,
version,
[1.into()].to_vec(),
Felt252::zero(),
Felt252::one(),
)
.unwrap();
let casm_contract_class_cache = HashMap::new();
let state_reader = Arc::new(InMemoryStateReader::default());
let mut state = CachedState::new(state_reader, casm_contract_class_cache);
assert!(internal_declare
.compile_and_store_casm_class(&mut state)
.is_ok());
let expected_casm_class = CasmContractClass::from_contract_class(
internal_declare.sierra_contract_class.clone(),
true,
)
.unwrap();
let casm_class = match state
.get_contract_class(&internal_declare.compiled_class_hash.to_be_bytes())
.unwrap()
{
CompiledClass::Casm(casm) => casm.as_ref().clone(),
_ => unreachable!(),
};
assert_eq!(expected_casm_class, casm_class);
}
#[test]
fn create_declare_v2_wrong_casm_class_hash_test() {
let version;
let path;
#[cfg(not(feature = "cairo_1_tests"))]
{
version = Felt252::from(2);
path = PathBuf::from("starknet_programs/cairo2/fibonacci.sierra");
}
#[cfg(feature = "cairo_1_tests")]
{
version = Felt252::from(1);
path = PathBuf::from("starknet_programs/cairo1/fibonacci.sierra");
}
let file = File::open(path).unwrap();
let reader = BufReader::new(file);
let sierra_contract_class: cairo_lang_starknet::contract_class::ContractClass =
serde_json::from_reader(reader).unwrap();
let sender_address = Address(1.into());
let casm_class =
CasmContractClass::from_contract_class(sierra_contract_class.clone(), true).unwrap();
let casm_class_hash = compute_casm_class_hash(&casm_class).unwrap();
let sended_class_hash = Felt252::from(5);
let internal_declare = DeclareV2::new_with_tx_hash(
&sierra_contract_class,
None,
sended_class_hash.clone(),
sender_address,
0,
version,
[1.into()].to_vec(),
Felt252::zero(),
Felt252::one(),
)
.unwrap();
let casm_contract_class_cache = HashMap::new();
let state_reader = Arc::new(InMemoryStateReader::default());
let mut state = CachedState::new(state_reader, casm_contract_class_cache);
let expected_err = format!(
"Invalid compiled class, expected class hash: {}, but received: {}",
casm_class_hash, sended_class_hash
);
assert_eq!(
internal_declare
.compile_and_store_casm_class(&mut state)
.unwrap_err()
.to_string(),
expected_err
);
}
}