Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion crates/apollo_batcher/src/block_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use blockifier::blockifier::transaction_executor::{
use blockifier::blockifier_versioned_constants::VersionedConstants;
use blockifier::bouncer::{BouncerWeights, CasmHashComputationData};
use blockifier::concurrency::worker_pool::WorkerPool;
use blockifier::context::BlockContext;
use blockifier::context::{parse_blocked_storage_keys, BlockContext};
use blockifier::state::cached_state::{CachedState, CommitmentStateDiff};
use blockifier::state::contract_class_manager::ContractClassManager;
use blockifier::state::errors::StateError;
Expand Down Expand Up @@ -781,6 +781,11 @@ impl BlockBuilderFactory {
block_builder_config.chain_info,
versioned_constants,
block_builder_config.bouncer_config,
)
.with_blocked_storage_keys(
parse_blocked_storage_keys(&block_builder_config.blocked_storage_keys)
.expect("Blocked storage keys are validated when the config is loaded."),
block_builder_config.blocked_storage_keys_error_message,
);

// Block production has no per-call deadline to bound this against; leave it unbounded, as
Expand Down
30 changes: 28 additions & 2 deletions crates/apollo_batcher_config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use blockifier::blockifier::config::{
};
use blockifier::blockifier_versioned_constants::VersionedConstantsOverrides;
use blockifier::bouncer::BouncerConfig;
use blockifier::context::ChainInfo;
use blockifier::context::{parse_blocked_storage_keys, ChainInfo};
use serde::{Deserialize, Serialize};
use starknet_api::block::{BlockHash, BlockNumber};
use url::Url;
Expand All @@ -35,12 +35,21 @@ pub const DEFAULT_TASKS_CHANNEL_SIZE: usize = 1000;
pub const DEFAULT_RESULTS_CHANNEL_SIZE: usize = 1000;

/// Configuration for the block builder component of the batcher.
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
#[derive(Clone, Debug, Default, Serialize, Deserialize, Validate, PartialEq)]
pub struct BlockBuilderConfig {
pub chain_info: ChainInfo,
pub execute_config: WorkerPoolConfig,
pub bouncer_config: BouncerConfig,
pub versioned_constants_overrides: Option<VersionedConstantsOverrides>,
#[validate(custom(function = "validate_blocked_storage_keys"))]
pub blocked_storage_keys: String,
pub blocked_storage_keys_error_message: String,
}

fn validate_blocked_storage_keys(blocked_storage_keys: &str) -> Result<(), ValidationError> {
parse_blocked_storage_keys(blocked_storage_keys).map(|_| ()).map_err(|error| {
ValidationError::new("Invalid blocked_storage_keys").with_message(error.into())
})
}

impl SerializeConfig for BlockBuilderConfig {
Expand All @@ -52,6 +61,22 @@ impl SerializeConfig for BlockBuilderConfig {
&self.versioned_constants_overrides,
"versioned_constants_overrides",
));
dump.append(&mut BTreeMap::from([
ser_param(
"blocked_storage_keys",
&self.blocked_storage_keys,
"Comma-separated list of hexadecimal storage keys, i.e., \"0x1,0x2\", that \
transactions must not access; a transaction that reads or writes any of them, in \
any contract, fails.",
ParamPrivacyInput::Public,
),
ser_param(
"blocked_storage_keys_error_message",
&self.blocked_storage_keys_error_message,
"The error message of a transaction that accessed a blocked storage key.",
ParamPrivacyInput::Public,
),
]));
dump
}
}
Expand Down Expand Up @@ -198,6 +223,7 @@ pub struct BatcherStaticConfig {
pub storage: StorageConfig,
pub outstream_content_buffer_size: usize,
pub input_stream_content_buffer_size: usize,
#[validate(nested)]
pub block_builder_config: BlockBuilderConfig,
pub pre_confirmed_block_writer_config: PreconfirmedBlockWriterConfig,
#[validate(nested)]
Expand Down
16 changes: 16 additions & 0 deletions crates/apollo_batcher_config/src/config_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use validator::Validate;

use crate::config::BatcherStaticConfig;

fn static_config_with_blocked_storage_keys(blocked_storage_keys: &str) -> BatcherStaticConfig {
let mut static_config = BatcherStaticConfig::default();
static_config.block_builder_config.blocked_storage_keys = blocked_storage_keys.to_string();
static_config
}

#[test]
fn blocked_storage_keys_are_validated() {
assert!(static_config_with_blocked_storage_keys("").validate().is_ok());
assert!(static_config_with_blocked_storage_keys("0x1, 0x2,").validate().is_ok());
assert!(static_config_with_blocked_storage_keys("0x1,not_a_key").validate().is_err());
}
2 changes: 2 additions & 0 deletions crates/apollo_batcher_config/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
pub mod config;
#[cfg(test)]
mod config_test;
10 changes: 10 additions & 0 deletions crates/apollo_node/resources/config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@
"privacy": "Public",
"value": 5000
},
"batcher_config.static_config.block_builder_config.blocked_storage_keys": {
"description": "Comma-separated list of hexadecimal storage keys, i.e., \"0x1,0x2\", that transactions must not access; a transaction that reads or writes any of them, in any contract, fails.",
"privacy": "Public",
"value": ""
},
"batcher_config.static_config.block_builder_config.blocked_storage_keys_error_message": {
"description": "The error message of a transaction that accessed a blocked storage key.",
"privacy": "Public",
"value": ""
},
"batcher_config.static_config.block_builder_config.bouncer_config.block_max_capacity.l1_gas": {
"description": "An upper bound on the total l1_gas used in a block.",
"privacy": "Public",
Expand Down
41 changes: 40 additions & 1 deletion crates/blockifier/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashSet};
use std::sync::{Arc, OnceLock};

use apollo_config::dumping::{prepend_sub_config_name, ser_param, SerializeConfig};
Expand All @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
use starknet_api::block::{BlockInfo, BlockNumber, BlockTimestamp, FeeType, GasPriceVector};
use starknet_api::core::{ChainId, ContractAddress, OsChainInfo};
use starknet_api::execution_resources::GasAmount;
use starknet_api::state::StorageKey;
use starknet_api::transaction::fields::{
AllResourceBounds,
Fee,
Expand Down Expand Up @@ -128,11 +129,33 @@ pub struct BlockContext {
pub(crate) chain_info: ChainInfo,
pub(crate) versioned_constants: VersionedConstants,
pub bouncer_config: BouncerConfig,
/// Storage keys that transactions must not access, in any contract. A transaction that reads
/// or writes any of them fails with `blocked_storage_keys_error_message`.
pub(crate) blocked_storage_keys: HashSet<StorageKey>,
pub(crate) blocked_storage_keys_error_message: String,
/// Cached on first access; derived from `chain_info`. Fixed for the lifetime of a block
/// context.
virtual_os_config_hash: OnceLock<Felt>,
}

/// Parses a comma-separated list of hexadecimal storage keys, e.g. "0x1,0x2". Whitespace around
/// entries is ignored, and empty entries (e.g. a trailing comma) are skipped.
pub fn parse_blocked_storage_keys(
blocked_storage_keys: &str,
) -> Result<HashSet<StorageKey>, String> {
blocked_storage_keys
.split(',')
.map(str::trim)
.filter(|entry| !entry.is_empty())
.map(|entry| {
let felt = Felt::from_hex(entry)
.map_err(|error| format!("Invalid blocked storage key {entry:?}: {error}"))?;
StorageKey::try_from(felt)
.map_err(|error| format!("Invalid blocked storage key {entry:?}: {error}"))
})
.collect()
}

impl BlockContext {
pub fn new(
block_info: BlockInfo,
Expand All @@ -145,10 +168,22 @@ impl BlockContext {
chain_info,
versioned_constants,
bouncer_config,
blocked_storage_keys: HashSet::new(),
blocked_storage_keys_error_message: String::new(),
virtual_os_config_hash: OnceLock::new(),
}
}

pub fn with_blocked_storage_keys(
mut self,
blocked_storage_keys: HashSet<StorageKey>,
blocked_storage_keys_error_message: String,
) -> Self {
self.blocked_storage_keys = blocked_storage_keys;
self.blocked_storage_keys_error_message = blocked_storage_keys_error_message;
self
}

pub fn block_info(&self) -> &BlockInfo {
&self.block_info
}
Expand Down Expand Up @@ -208,6 +243,10 @@ impl BlockContext {
#[cfg(any(test, feature = "testing"))]
pub fn with_chain_info(self, chain_info: ChainInfo) -> Self {
Self::new(self.block_info, chain_info, self.versioned_constants, self.bouncer_config)
.with_blocked_storage_keys(
self.blocked_storage_keys,
self.blocked_storage_keys_error_message,
)
}

/// Test util to allow overriding block gas limits.
Expand Down
2 changes: 2 additions & 0 deletions crates/blockifier/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ pub mod objects;
#[cfg(any(feature = "testing", test))]
pub mod test_utils;
pub mod transaction_execution;
#[cfg(test)]
pub mod transaction_execution_test;
pub mod transactions;
2 changes: 2 additions & 0 deletions crates/blockifier/src/transaction/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ pub enum TransactionExecutionError {
ContractClassVersionMismatch { declare_version: TransactionVersion, cairo_version: u64 },
#[error("{}", gen_tx_execution_error_trace(self))]
ContractConstructorExecutionFailed(#[from] ConstructorEntryPointExecutionError),
#[error("{message}")]
BlockedStorageKeyAccessed { message: String },
#[error("Class with hash {:#066x} is already declared.", **class_hash)]
DeclareTransactionError { class_hash: ClassHash },
#[error("{}", gen_tx_execution_error_trace(self))]
Expand Down
27 changes: 27 additions & 0 deletions crates/blockifier/src/transaction/transaction_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ use starknet_api::transaction::{

use crate::bouncer::verify_tx_weights_within_max_capacity;
use crate::context::BlockContext;
use crate::execution::call_info::CallInfo;
use crate::state::cached_state::TransactionalState;
use crate::state::state_api::UpdatableState;
use crate::transaction::account_transaction::{
AccountTransaction,
ExecutionFlags as AccountExecutionFlags,
};
use crate::transaction::errors::TransactionExecutionError;
use crate::transaction::objects::{
TransactionExecutionInfo,
TransactionExecutionResult,
Expand Down Expand Up @@ -154,6 +156,8 @@ impl<U: UpdatableState> ExecutableTransaction<U> for Transaction {
Self::L1Handler(tx) => tx.execute_raw(state, block_context, concurrency_mode)?,
};

verify_no_blocked_storage_key_accessed(&tx_execution_info, block_context)?;

// Check if the transaction is too large to fit any block.
// TODO(Yoni, 1/8/2024): consider caching these two.
let tx_execution_summary = tx_execution_info.summarize(&block_context.versioned_constants);
Expand All @@ -178,3 +182,26 @@ impl<U: UpdatableState> ExecutableTransaction<U> for Transaction {
Ok(tx_execution_info)
}
}

fn verify_no_blocked_storage_key_accessed(
tx_execution_info: &TransactionExecutionInfo,
block_context: &BlockContext,
) -> TransactionExecutionResult<()> {
if block_context.blocked_storage_keys.is_empty() {
return Ok(());
}
// `CallInfo::iter` walks the whole call tree, so inner calls are covered.
for call_info in tx_execution_info.non_optional_call_infos().flat_map(CallInfo::iter) {
if call_info
.storage_access_tracker
.accessed_storage_keys
.iter()
.any(|storage_key| block_context.blocked_storage_keys.contains(storage_key))
{
return Err(TransactionExecutionError::BlockedStorageKeyAccessed {
message: block_context.blocked_storage_keys_error_message.clone(),
});
}
}
Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted execute accesses bypass blocklist

Medium Severity

verify_no_blocked_storage_key_accessed only inspects call infos left on TransactionExecutionInfo. Reverted invoke and L1-handler execution clear execute_call_info, so an execute-phase read or write of a blocked key is not rejected and the transaction is still included as reverted.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e6d4cd3. Configure here.

Loading
Loading