diff --git a/CHANGELOG.md b/CHANGELOG.md index 968c80d9d4..6c9e996823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ## v0.8.0 +- feat: L1 gas price/fix +- feat: Declare V0 RPC call - feat: add `TransactionFilter` to pallet-starknet `Config` - chore: remove `ignore` from `storage_changes_should_revert_on_transaction_revert` test diff --git a/Cargo.lock b/Cargo.lock index 5e130f9de5..751da66cd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5816,6 +5816,7 @@ version = "0.8.0" dependencies = [ "blockifier", "cairo-vm", + "indexmap 2.2.5", "jsonrpsee", "log", "mc-db", diff --git a/crates/client/l1-gas-price/src/types.rs b/crates/client/l1-gas-price/src/types.rs index d867acb28d..7257f6bf8a 100644 --- a/crates/client/l1-gas-price/src/types.rs +++ b/crates/client/l1-gas-price/src/types.rs @@ -29,7 +29,7 @@ pub struct FeeHistory { /// of the returned range, because this value can be derived from the newest block. Zeroes /// are returned for pre-EIP-4844 blocks. #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub base_fee_per_blob_gas: Vec, + pub base_fee_per_blob_gas: Vec, /// An array of block blob gas used ratios. These are calculated as the ratio of gasUsed and /// gasLimit. #[serde(default, skip_serializing_if = "Vec::is_empty")] diff --git a/crates/client/l1-gas-price/src/worker.rs b/crates/client/l1-gas-price/src/worker.rs index 2198c7d639..8fae3de46e 100644 --- a/crates/client/l1-gas-price/src/worker.rs +++ b/crates/client/l1-gas-price/src/worker.rs @@ -69,10 +69,14 @@ async fn update_gas_price( // The RPC responds with 301 elements for some reason. It's also just safer to manually // take the last 300. We choose 300 to get average gas caprice for last one hour (300 * 12 sec block // time). - let (_, blob_fee_history_one_hour) = - fee_history.result.base_fee_per_blob_gas.split_at(fee_history.result.base_fee_per_blob_gas.len() - 300); + let (_, blob_fee_history_one_hour) = fee_history + .result + .base_fee_per_blob_gas + .split_at(fee_history.result.base_fee_per_blob_gas.len().max(300) - 300); - let avg_blob_base_fee = blob_fee_history_one_hour.iter().sum::() / blob_fee_history_one_hour.len() as u128; + let avg_blob_base_fee = + blob_fee_history_one_hour.iter().map(|hex_str| u128::from_str_radix(&hex_str[2..], 16).unwrap()).sum::() + / blob_fee_history_one_hour.len() as u128; let eth_gas_price = u128::from_str_radix( fee_history @@ -84,17 +88,20 @@ async fn update_gas_price( 16, )?; + log::info!("avg_blob_base_fee : {:?}", avg_blob_base_fee); + log::info!("eth_gas_price : {:?}", eth_gas_price); + // TODO: fetch this from the oracle let eth_strk_price = 2425; let mut gas_price = gas_price.lock().await; gas_price.eth_l1_gas_price = NonZeroU128::new(eth_gas_price).ok_or(format_err!("Failed to convert `eth_gas_price` to NonZeroU128"))?; - gas_price.eth_l1_data_gas_price = NonZeroU128::new(avg_blob_base_fee) + gas_price.eth_l1_data_gas_price = NonZeroU128::new(avg_blob_base_fee + 1) .ok_or(format_err!("Failed to convert `eth_l1_data_gas_price` to NonZeroU128"))?; gas_price.strk_l1_gas_price = NonZeroU128::new(eth_gas_price.saturating_mul(eth_strk_price)) .ok_or(format_err!("Failed to convert `strk_l1_gas_price` to NonZeroU128"))?; - gas_price.strk_l1_data_gas_price = NonZeroU128::new(avg_blob_base_fee.saturating_mul(eth_strk_price)) + gas_price.strk_l1_data_gas_price = NonZeroU128::new((avg_blob_base_fee + 1).saturating_mul(eth_strk_price)) .ok_or(format_err!("Failed to convert `strk_l1_data_gas_price` to NonZeroU128"))?; gas_price.last_update_timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_millis(); // explicitly dropping gas price here to avoid long waits when fetching the value @@ -103,3 +110,6 @@ async fn update_gas_price( Ok(()) } + +#[tokio::test] +async fn test_l1_gas_price() {} diff --git a/crates/client/rpc-core/src/lib.rs b/crates/client/rpc-core/src/lib.rs index c72b0843e0..0132b4d76c 100644 --- a/crates/client/rpc-core/src/lib.rs +++ b/crates/client/rpc-core/src/lib.rs @@ -7,10 +7,15 @@ #[cfg(test)] mod tests; +use blockifier::transaction::transactions::DeclareTransaction; +use indexmap::IndexMap; use jsonrpsee::core::RpcResult; use jsonrpsee::proc_macros::rpc; use serde::{Deserialize, Serialize}; use serde_with::serde_as; +use starknet_api::core::ClassHash; +use starknet_api::deprecated_contract_class::{EntryPoint, EntryPointType}; +use starknet_api::transaction::{DeclareTransactionV0V1, TransactionHash}; pub mod utils; @@ -36,11 +41,38 @@ pub struct PredeployedAccountWithBalance { pub balance: FieldElement, } +#[serde_as] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DeclareV0Result { + pub txn_hash: TransactionHash, + pub class_hash: ClassHash, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct CustomDeclareV0Transaction { + pub declare_transaction: DeclareTransactionV0V1, + pub program_vec: Vec, + pub entrypoints: IndexMap>, + pub abi_length: usize, +} + +#[derive(PartialEq, Eq, Debug)] +pub enum DeclareTransactionWithV0 { + V0(Box, Vec, IndexMap>, usize), + V1(DeclareTransaction), +} + /// Madara rpc interface for additional features. #[rpc(server, namespace = "madara")] pub trait MadaraRpcApi: StarknetReadRpcApi { #[method(name = "predeployedAccounts")] fn predeployed_accounts(&self) -> RpcResult>; + + // There is an issue in deserialisation when we try to send the class info directly + // That's why we are sending the components saperately here and then building the + // transaction here in madara and executing the function in the pallet. + #[method(name = "declareV0")] + async fn declare_v0_contract(&self, params: CustomDeclareV0Transaction) -> RpcResult; } /// Starknet write rpc interface. diff --git a/crates/client/rpc/Cargo.toml b/crates/client/rpc/Cargo.toml index a062e8cb94..1cb5655b9a 100644 --- a/crates/client/rpc/Cargo.toml +++ b/crates/client/rpc/Cargo.toml @@ -37,6 +37,7 @@ sc-network-sync = { workspace = true } # Starknet blockifier = { workspace = true } cairo-vm = { workspace = true } +indexmap = { workspace = true } jsonrpsee = { workspace = true, features = ["server", "macros"] } log = { workspace = true } mp-block = { workspace = true } diff --git a/crates/client/rpc/src/lib.rs b/crates/client/rpc/src/lib.rs index 4065b278a3..3e37d270a2 100644 --- a/crates/client/rpc/src/lib.rs +++ b/crates/client/rpc/src/lib.rs @@ -15,14 +15,17 @@ use std::collections::HashMap; use std::marker::PhantomData; use std::sync::Arc; +use blockifier::execution::contract_class::{ClassInfo, ContractClassV0, ContractClassV0Inner}; use blockifier::transaction::account_transaction::AccountTransaction; use blockifier::transaction::objects::{ResourcesMapping, TransactionExecutionInfo}; -use blockifier::transaction::transactions::L1HandlerTransaction; +use blockifier::transaction::transactions::{DeclareTransaction, L1HandlerTransaction}; +use cairo_vm::types::program::Program; use errors::StarknetRpcApiError; use jsonrpsee::core::{async_trait, RpcResult}; use log::error; use mc_genesis_data_provider::GenesisProvider; pub use mc_rpc_core::utils::*; +use mc_rpc_core::{DeclareTransactionWithV0, DeclareV0Result}; pub use mc_rpc_core::{ Felt, MadaraRpcApiServer, PredeployedAccountWithBalance, StarknetReadRpcApiServer, StarknetTraceRpcApiServer, StarknetWriteRpcApiServer, @@ -50,10 +53,11 @@ use sp_api::ProvideRuntimeApi; use sp_arithmetic::traits::UniqueSaturatedInto; use sp_blockchain::HeaderBackend; use sp_core::H256; +use sp_runtime::codec::DecodeAll; use sp_runtime::traits::{Block as BlockT, Header as HeaderT}; use sp_runtime::transaction_validity::InvalidTransaction; -use starknet_api::core::Nonce; -use starknet_api::hash::StarkFelt; +use starknet_api::core::{ClassHash, Nonce}; +use starknet_api::hash::{StarkFelt, StarkHash}; use starknet_api::transaction::{Calldata, Fee, TransactionHash, TransactionVersion}; use starknet_core::types::{ BlockHashAndNumber, BlockId, BlockStatus, BlockTag, BlockWithTxHashes, BlockWithTxs, BroadcastedDeclareTransaction, @@ -220,9 +224,96 @@ where } } +impl Starknet +where + A: ChainApi + 'static, + B: BlockT, + P: TransactionPool + 'static, + BE: Backend + 'static, + C: HeaderBackend + BlockBackend + StorageProvider + 'static, + C: ProvideRuntimeApi, + C::Api: StarknetRuntimeApi + ConvertTransactionRuntimeApi, + G: GenesisProvider + Send + Sync + 'static, + H: HasherT + Send + Sync + 'static, +{ + async fn declare_txn_common( + &self, + transaction_inputs: DeclareTransactionWithV0, + ) -> Result<(TransactionHash, ClassHash), StarknetRpcApiError> { + match transaction_inputs { + DeclareTransactionWithV0::V1(transaction) => self._declare_tx(transaction).await, + DeclareTransactionWithV0::V0(declare_txn, program_vec, entrypoints, abi_length) => { + let txn_hash: TransactionHash = TransactionHash(StarkHash { 0: FieldElement::ONE.to_bytes_be() }); + + let program_decoded = Program::decode_all(&mut &*program_vec).unwrap(); + + let class_info; + let declare_transaction; + + let try_class_info = ClassInfo::new( + &blockifier::execution::contract_class::ContractClass::V0(ContractClassV0(Arc::from( + ContractClassV0Inner { program: program_decoded, entry_points_by_type: entrypoints }, + ))), + 0, + abi_length, + ); + + match try_class_info { + Ok(val) => { + class_info = val; + } + Err(_e) => return Err(StarknetRpcApiError::InternalServerError), + } + + let try_declare_transaction = DeclareTransaction::new( + starknet_api::transaction::DeclareTransaction::V0(*declare_txn), + txn_hash, + class_info, + ); + + match try_declare_transaction { + Ok(val) => { + declare_transaction = val; + } + Err(e) => return Err(StarknetRpcApiError::from(e)), + } + + self._declare_tx(declare_transaction).await + } + } + } + + async fn _declare_tx(&self, txn: DeclareTransaction) -> Result<(TransactionHash, ClassHash), StarknetRpcApiError> { + let best_block_hash = self.get_best_block_hash(); + let current_block_hash = self.get_best_block_hash(); + let contract_class = self + .overrides + .for_block_hash(self.client.as_ref(), current_block_hash) + .contract_class_by_class_hash(current_block_hash, txn.class_hash()); + + if let Some(contract_class) = contract_class { + error!("Contract class already exists: {:?}", contract_class); + return Err(StarknetRpcApiError::ClassAlreadyDeclared); + } + + let extrinsic = + self.convert_tx_to_extrinsic(best_block_hash, AccountTransaction::Declare(txn.clone())).unwrap(); + + let res = submit_extrinsic(self.pool.clone(), best_block_hash, extrinsic).await; + + match res { + Ok(_val) => { + Ok((txn.tx.compute_hash(Felt252Wrapper::from(self.chain_id().unwrap().0), false), txn.class_hash())) + } + Err(e) => Err(e), + } + } +} + /// Taken from https://github.com/paritytech/substrate/blob/master/client/rpc/src/author/mod.rs#L78 const TX_SOURCE: TransactionSource = TransactionSource::External; +#[async_trait] impl MadaraRpcApiServer for Starknet where A: ChainApi + 'static, @@ -262,6 +353,22 @@ where }) .collect::>()) } + + async fn declare_v0_contract(&self, params: mc_rpc_core::CustomDeclareV0Transaction) -> RpcResult { + let try_declare = self + .declare_txn_common(DeclareTransactionWithV0::V0( + Box::from(params.declare_transaction), + params.program_vec, + params.entrypoints, + params.abi_length, + )) + .await; + + match try_declare { + Ok((txn_hash, class_hash)) => Ok(DeclareV0Result { class_hash, txn_hash }), + Err(e) => Err(e.into()), + } + } } #[async_trait] @@ -290,8 +397,6 @@ where &self, declare_transaction: BroadcastedDeclareTransaction, ) -> RpcResult { - let best_block_hash = self.get_best_block_hash(); - let opt_sierra_contract_class = if let BroadcastedDeclareTransaction::V2(ref tx) = declare_transaction { Some(flattened_sierra_to_sierra_contract_class(tx.contract_class.clone())) } else { @@ -301,25 +406,12 @@ where let chain_id = Felt252Wrapper(self.chain_id()?.0); let transaction = try_declare_tx_from_broadcasted_declare_tx(declare_transaction, chain_id).map_err(|e| { - error!("Failed to convert BroadcastedDeclareTransaction to DeclareTransaction, error: {e}"); + error!("Failed to convert Broadcasted DeclareTransaction to DeclareTransaction, error: {e}"); StarknetRpcApiError::InternalServerError })?; - let (class_hash, tx_hash) = (transaction.class_hash(), transaction.tx_hash()); - - let current_block_hash = self.get_best_block_hash(); - let contract_class = self - .overrides - .for_block_hash(self.client.as_ref(), current_block_hash) - .contract_class_by_class_hash(current_block_hash, class_hash); - - if let Some(contract_class) = contract_class { - error!("Contract class already exists: {:?}", contract_class); - return Err(StarknetRpcApiError::ClassAlreadyDeclared.into()); - } - let extrinsic = self.convert_tx_to_extrinsic(best_block_hash, AccountTransaction::Declare(transaction))?; - - submit_extrinsic(self.pool.clone(), best_block_hash, extrinsic).await?; + let (tx_hash, class_hash) = + self.declare_txn_common(mc_rpc_core::DeclareTransactionWithV0::V1(transaction)).await.unwrap(); if let Some(sierra_contract_class) = opt_sierra_contract_class { if let Some(e) = self.backend.sierra_classes().store_sierra_class(class_hash, sierra_contract_class).err() { diff --git a/crates/client/rpc/src/starknetrpcwrapper.rs b/crates/client/rpc/src/starknetrpcwrapper.rs index a2a778b9ce..0086fccdfe 100644 --- a/crates/client/rpc/src/starknetrpcwrapper.rs +++ b/crates/client/rpc/src/starknetrpcwrapper.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use jsonrpsee::core::{async_trait, RpcResult}; use mc_genesis_data_provider::GenesisProvider; +use mc_rpc_core::DeclareV0Result; pub use mc_rpc_core::{ Felt, MadaraRpcApiServer, PredeployedAccountWithBalance, StarknetReadRpcApiServer, StarknetTraceRpcApiServer, StarknetWriteRpcApiServer, @@ -36,6 +37,7 @@ impl Clone for StarknetRpcWrapper MadaraRpcApiServer for StarknetRpcWrapper where A: ChainApi + 'static, @@ -51,6 +53,10 @@ where fn predeployed_accounts(&self) -> RpcResult> { self.0.predeployed_accounts() } + + async fn declare_v0_contract(&self, params: mc_rpc_core::CustomDeclareV0Transaction) -> RpcResult { + self.0.declare_v0_contract(params).await + } } #[async_trait] diff --git a/crates/client/rpc/src/types.rs b/crates/client/rpc/src/types.rs index c098eb4360..f12afef37b 100644 --- a/crates/client/rpc/src/types.rs +++ b/crates/client/rpc/src/types.rs @@ -1,7 +1,11 @@ use std::num::ParseIntError; use std::{fmt, u64}; +use indexmap::IndexMap; use mp_felt::Felt252Wrapper; +use serde::{Deserialize, Serialize}; +use starknet_api::deprecated_contract_class::{EntryPoint, EntryPointType}; +use starknet_api::transaction::DeclareTransactionV0V1; use starknet_ff::FieldElement; pub struct RpcEventFilter { @@ -25,6 +29,14 @@ pub enum ParseTokenError { ParseFailed(ParseIntError), } +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct CustomDeclareV0Transaction { + pub declare_transaction: DeclareTransactionV0V1, + pub program_vec: Vec, + pub entrypoints: IndexMap>, + pub abi_length: usize, +} + impl fmt::Display for ContinuationToken { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:x},{:x}", self.block_n, self.event_n) diff --git a/crates/node/src/service.rs b/crates/node/src/service.rs index 08a016cce0..f3b8b11241 100644 --- a/crates/node/src/service.rs +++ b/crates/node/src/service.rs @@ -20,6 +20,7 @@ use mp_starknet_inherent::{ InherentDataProvider as StarknetInherentDataProvider, InherentError as StarknetInherentError, L1GasPrices, StarknetInherentData, DEFAULT_SEQUENCER_ADDRESS, SEQ_ADDR_STORAGE_KEY, }; +use pallet_starknet_runtime_api::StarknetRuntimeApi; use prometheus_endpoint::Registry; use sc_basic_authorship::ProposerFactory; use sc_client_api::{Backend, BlockBackend, BlockchainEvents, HeaderBackend}; @@ -33,7 +34,7 @@ use sc_telemetry::{Telemetry, TelemetryWorker}; use sc_transaction_pool::FullPool; use sc_transaction_pool_api::OffchainTransactionPoolFactory; use sp_api::offchain::OffchainStorage; -use sp_api::ConstructRuntimeApi; +use sp_api::{ConstructRuntimeApi, ProvideRuntimeApi}; use sp_consensus_aura::sr25519::AuthorityPair as AuraPair; use sp_offchain::STORAGE_PREFIX; @@ -346,18 +347,25 @@ pub fn new_full( ), ); - // Ensuring we've fetched the latest price before we start the node - futures::executor::block_on(mc_l1_gas_price::worker::run_worker( - ethereum_conf.clone(), - l1_gas_price.clone(), - false, - )); + let fees_disabled = client + .runtime_api() + .is_transaction_fee_disabled(client.chain_info().best_hash) + .expect("Failed to get fee status"); - task_manager.spawn_handle().spawn( - "l1-gas-prices-worker", - Some(MADARA_TASK_GROUP), - mc_l1_gas_price::worker::run_worker(ethereum_conf.clone(), l1_gas_price.clone(), true), - ); + if !fees_disabled { + // Ensuring we've fetched the latest price before we start the node + futures::executor::block_on(mc_l1_gas_price::worker::run_worker( + ethereum_conf.clone(), + l1_gas_price.clone(), + false, + )); + + task_manager.spawn_handle().spawn( + "l1-gas-prices-worker", + Some(MADARA_TASK_GROUP), + mc_l1_gas_price::worker::run_worker(ethereum_conf.clone(), l1_gas_price.clone(), true), + ); + } } } diff --git a/crates/pallets/starknet/src/lib.rs b/crates/pallets/starknet/src/lib.rs index 306723fc8a..90fd4f1c66 100644 --- a/crates/pallets/starknet/src/lib.rs +++ b/crates/pallets/starknet/src/lib.rs @@ -581,11 +581,17 @@ pub mod pallet { !ContractClasses::::contains_key(transaction.tx().class_hash().0), Error::::ClassHashAlreadyDeclared ); - // Check if contract is deployed - ensure!( - ContractClassHashes::::contains_key(transaction.tx().sender_address()), - Error::::AccountNotDeployed - ); + + let tx_version = transaction.tx().version(); + + // Avoiding account checks for V0 declares as they don't need to originate from an existing account + if tx_version != TransactionVersion::ZERO { + // Check if contract is deployed + ensure!( + ContractClassHashes::::contains_key(transaction.tx().sender_address()), + Error::::AccountNotDeployed + ); + } let mut state = BlockifierStateAdapter::::default(); let charge_fee = !::DisableTransactionFee::get(); diff --git a/crates/pallets/starknet/src/transaction_validation.rs b/crates/pallets/starknet/src/transaction_validation.rs index b45bb77af5..c6a4ad2038 100644 --- a/crates/pallets/starknet/src/transaction_validation.rs +++ b/crates/pallets/starknet/src/transaction_validation.rs @@ -123,6 +123,7 @@ impl Pallet { AccountTransaction::DeployAccount(tx) => tx.contract_address, AccountTransaction::Invoke(tx) => tx.tx.sender_address(), }; + if address == sender_address && account_nonce == Nonce(StarkFelt::ZERO) && incoming_tx_nonce == Nonce(StarkFelt::ONE) diff --git a/examples/messaging/eth-config.json b/examples/messaging/eth-config.json index 5657dfa26a..b1b997e40f 100644 --- a/examples/messaging/eth-config.json +++ b/examples/messaging/eth-config.json @@ -1,6 +1,6 @@ { "provider": { - "rpc_endpoint": "https://eth.merkle.io", + "rpc_endpoint": "http://127.0.0.1:8545", "gas_price_poll_ms": 10000 }, "contracts": {