Skip to content
Merged
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
6 changes: 6 additions & 0 deletions Cargo.lock

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

10 changes: 10 additions & 0 deletions crates/blockifier_reexecution/src/state_reader/rpc_state_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,16 @@ impl RpcStateReader {
}
}

/// Creates an RpcStateReader from a node URL, chain ID, and block number.
pub fn new_with_config_from_url(
node_url: String,
chain_id: ChainId,
block_number: BlockNumber,
) -> Self {
let config = RpcStateReaderConfig::from_url(node_url);
Self::new(&config, chain_id, block_number, false)
}

pub fn new_for_testing(block_number: BlockNumber) -> Self {
RpcStateReader::new(&get_rpc_state_reader_config(), ChainId::Mainnet, block_number, false)
}
Expand Down
4 changes: 4 additions & 0 deletions crates/starknet_os_runner/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ license-file.workspace = true
description = "Runs transactions through the Starknet OS and returns Cairo PIE and OS output."

[dependencies]
blockifier.workspace = true
blockifier_reexecution.workspace = true
starknet_api.workspace = true
thiserror.workspace = true

[lints]
workspace = true
18 changes: 18 additions & 0 deletions crates/starknet_os_runner/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use blockifier_reexecution::errors::ReexecutionError;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum VirtualBlockExecutorError {
#[error(transparent)]
// Boxed to reduce the size of Result on the stack (ReexecutionError is >128 bytes).
ReexecutionError(#[from] Box<ReexecutionError>),

#[error("Transaction execution failed: {0}")]
TransactionExecutionError(String),

#[error("Block state unavailable after execution")]
StateUnavailable,

#[error("Unsupported transaction type: only Invoke transactions are supported")]
UnsupportedTransactionType,
}
3 changes: 2 additions & 1 deletion crates/starknet_os_runner/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
//! Starknet OS Runner - executes transactions through the OS and returns Cairo PIE and output.
pub mod errors;
pub mod virtual_block_executor;
206 changes: 206 additions & 0 deletions crates/starknet_os_runner/src/virtual_block_executor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
use blockifier::blockifier::config::TransactionExecutorConfig;
use blockifier::blockifier::transaction_executor::{
TransactionExecutionOutput,
TransactionExecutor,
};
use blockifier::context::BlockContext;
use blockifier::state::cached_state::{CachedState, StateMaps};
use blockifier::state::contract_class_manager::ContractClassManager;
use blockifier::state::state_reader_and_contract_manager::StateReaderAndContractManager;
use blockifier::transaction::account_transaction::ExecutionFlags;
use blockifier::transaction::transaction_execution::Transaction as BlockifierTransaction;
use blockifier_reexecution::state_reader::rpc_state_reader::RpcStateReader;
use starknet_api::block::BlockNumber;
use starknet_api::core::ChainId;
use starknet_api::transaction::{Transaction, TransactionHash};

use crate::errors::VirtualBlockExecutorError;

/// Captures execution data for a virtual block (multiple transactions).
///
/// A virtual block is a set of transactions executed together without block preprocessing,
/// useful for OS input generation and proving. This struct contains all the transaction execution
/// outputs, block context, and initial state reads needed for proof generation.
pub struct VirtualBlockExecutionData {
/// Execution outputs for all transactions in the virtual block.
pub execution_outputs: Vec<TransactionExecutionOutput>,
/// The block context in which the transactions were executed.
pub block_context: BlockContext,
/// The initial state reads (accessed state) during execution.
pub initial_reads: StateMaps,
}

/// Executes a virtual block of transactions.
///
/// A virtual block executor runs transactions without block preprocessing
/// (`pre_process_block`), which is useful for simulating execution or generating
/// OS input for proving.
///
/// Implementations can fetch state from different sources (RPC nodes, local state,
/// mocked data, etc.).
///
/// # Note
///
/// - Currently only Invoke transactions are supported.
/// - fee charging and nonce check are always skipped (useful for simulation/proving).
///
/// # Examples
///
/// ```text
/// let executor = RpcVirtualBlockExecutor::new(
/// "http://localhost:9545".to_string(),
/// ChainId::Mainnet,
/// contract_class_manager,
/// );
///
/// let execution_data = executor.execute(block_number, transactions)?;
/// // Use execution_data to build OS input for proving...
/// ```
pub trait VirtualBlockExecutor {
/// Executes a virtual block based on the state and context at the given block number.
///
/// # Arguments
///
/// * `block_number` - The block number to use for state and context
/// * `txs` - Invoke transactions to execute (with their hashes)
///
/// # Returns
///
/// Returns `VirtualBlockExecutionData` containing execution outputs for all
/// transactions, or an error if any transaction fails or is not an Invoke.
fn execute(
&self,
block_number: BlockNumber,
txs: Vec<(Transaction, TransactionHash)>,
) -> Result<VirtualBlockExecutionData, VirtualBlockExecutorError> {
let blockifier_txs = Self::convert_invoke_txs(txs)?;
self.execute_inner(block_number, blockifier_txs)
}

fn execute_inner(
&self,
block_number: BlockNumber,
txs: Vec<BlockifierTransaction>,
) -> Result<VirtualBlockExecutionData, VirtualBlockExecutorError>;

/// Converts Invoke transactions to blockifier transactions.
///
/// Uses execution flags that skip fee charging and nonce check.
/// Returns an error if any transaction is not an Invoke.
fn convert_invoke_txs(
txs: Vec<(Transaction, TransactionHash)>,
) -> Result<Vec<BlockifierTransaction>, VirtualBlockExecutorError> {
// Skip validation, fee charging, and nonce check for virtual block execution.
let execution_flags = ExecutionFlags {
validate: true,
charge_fee: false,
strict_nonce_check: false,
only_query: false,
};

txs.into_iter()
.map(|(tx, tx_hash)| {
if !matches!(tx, Transaction::Invoke(_)) {
return Err(VirtualBlockExecutorError::UnsupportedTransactionType);
}

BlockifierTransaction::from_api(
tx,
tx_hash,
None, // class_info - not needed for Invoke.
None, // paid_fee_on_l1 - not needed for Invoke.
None, // deployed_contract_address - not needed for Invoke.
execution_flags.clone(),
)
.map_err(|e| VirtualBlockExecutorError::TransactionExecutionError(e.to_string()))
})
.collect()
}
}

/// RPC-based virtual block executor.
///
/// This executor fetches historical state from an RPC node and executes transactions
/// without block preprocessing. Validation and fee charging are always skipped,
/// making it suitable for simulation and OS input generation.
pub struct RpcVirtualBlockExecutor {
node_url: String,
chain_id: ChainId,
contract_class_manager: ContractClassManager,
}

impl RpcVirtualBlockExecutor {
/// Creates a new RPC-based virtual block executor.
///
/// # Arguments
///
/// * `node_url` - URL of the RPC node to fetch state from
/// * `chain_id` - The chain ID for transaction hash computation
/// * `contract_class_manager` - Manager for compiled contract classes
pub fn new(
node_url: String,
chain_id: ChainId,
contract_class_manager: ContractClassManager,
) -> Self {
Self { node_url, chain_id, contract_class_manager }
}
}

impl VirtualBlockExecutor for RpcVirtualBlockExecutor {
fn execute_inner(
&self,
block_number: BlockNumber,
txs: Vec<BlockifierTransaction>,
) -> Result<VirtualBlockExecutionData, VirtualBlockExecutorError> {
// Create RPC state reader for the given block.
let rpc_state_reader = RpcStateReader::new_with_config_from_url(
self.node_url.clone(),
self.chain_id.clone(),
block_number,
);

// Get block context from RPC.
let block_context = rpc_state_reader
.get_block_context()
.map_err(|e| VirtualBlockExecutorError::ReexecutionError(Box::new(e)))?;

// Create state reader with contract manager.
let state_reader_and_contract_manager = StateReaderAndContractManager::new(
rpc_state_reader,
self.contract_class_manager.clone(),
None,
);

let block_state = CachedState::new(state_reader_and_contract_manager);

// Create executor WITHOUT preprocessing (no pre_process_block call).
let mut transaction_executor = TransactionExecutor::new(
block_state,
block_context.clone(),
TransactionExecutorConfig::default(),
);

// Execute all transactions.
let execution_results = transaction_executor.execute_txs(&txs, None);

// Collect results, returning error if any transaction fails.
let execution_outputs: Vec<TransactionExecutionOutput> = execution_results
.into_iter()
.map(|result| {
result.map_err(|e| {
VirtualBlockExecutorError::TransactionExecutionError(e.to_string())
})
})
.collect::<Result<Vec<_>, _>>()?;

// Get initial state reads.
let initial_reads = transaction_executor
.block_state
.as_ref()
.ok_or(VirtualBlockExecutorError::StateUnavailable)?
.get_initial_reads()
.map_err(|e| VirtualBlockExecutorError::ReexecutionError(Box::new(e.into())))?;

Ok(VirtualBlockExecutionData { execution_outputs, block_context, initial_reads })
}
}
Loading