Skip to content

Commit 1b993b5

Browse files
starknet_os_runner: virtual block executor
1 parent 7bd78e7 commit 1b993b5

File tree

2 files changed

+149
-130
lines changed

2 files changed

+149
-130
lines changed

crates/starknet_os_runner/src/virtual_block_executor.rs

Lines changed: 92 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ use blockifier::blockifier::transaction_executor::{
66
use blockifier::context::BlockContext;
77
use blockifier::state::cached_state::{CachedState, StateMaps};
88
use blockifier::state::contract_class_manager::ContractClassManager;
9-
use blockifier::state::state_reader_and_contract_manager::StateReaderAndContractManager;
9+
use blockifier::state::state_reader_and_contract_manager::{
10+
FetchCompiledClasses,
11+
StateReaderAndContractManager,
12+
};
1013
use blockifier::transaction::account_transaction::ExecutionFlags;
1114
use blockifier::transaction::transaction_execution::Transaction as BlockifierTransaction;
1215
use blockifier_reexecution::state_reader::rpc_state_reader::RpcStateReader;
1316
use starknet_api::block::BlockNumber;
14-
use starknet_api::core::ChainId;
17+
use starknet_api::transaction::fields::Fee;
1518
use starknet_api::transaction::{Transaction, TransactionHash};
1619

1720
use crate::errors::VirtualBlockExecutorError;
@@ -32,8 +35,9 @@ pub struct VirtualBlockExecutionData {
3235

3336
/// Executes a virtual block of transactions.
3437
///
35-
/// A virtual block executor runs transactions without block preprocessing
36-
/// (`pre_process_block`), which is useful for simulating execution or generating
38+
/// A virtual block executor runs transactions on top of a given, finalized block.
39+
/// This means that some parts, like block preprocessing
40+
/// (`pre_process_block`), are skipped. Useful for simulating execution or generating
3741
/// OS input for proving.
3842
///
3943
/// Implementations can fetch state from different sources (RPC nodes, local state,
@@ -47,13 +51,13 @@ pub struct VirtualBlockExecutionData {
4751
/// # Examples
4852
///
4953
/// ```text
50-
/// let executor = RpcVirtualBlockExecutor::new(
54+
/// let executor = RpcStateReader::new_with_config_from_url(
5155
/// "http://localhost:9545".to_string(),
52-
/// ChainId::Mainnet,
53-
/// contract_class_manager,
56+
/// ChainId::Mainnet,
57+
/// BlockNumber(1000),
5458
/// );
5559
///
56-
/// let execution_data = executor.execute(block_number, transactions)?;
60+
/// let execution_data = executor.execute(block_number, contract_class_manager, transactions)?;
5761
/// // Use execution_data to build OS input for proving...
5862
/// ```
5963
pub trait VirtualBlockExecutor {
@@ -62,6 +66,7 @@ pub trait VirtualBlockExecutor {
6266
/// # Arguments
6367
///
6468
/// * `block_number` - The block number to use for state and context
69+
/// * `contract_class_manager` - Manager for compiled contract classes
6570
/// * `txs` - Invoke transactions to execute (with their hashes)
6671
///
6772
/// # Returns
@@ -71,105 +76,16 @@ pub trait VirtualBlockExecutor {
7176
fn execute(
7277
&self,
7378
block_number: BlockNumber,
79+
contract_class_manager: ContractClassManager,
7480
txs: Vec<(Transaction, TransactionHash)>,
7581
) -> Result<VirtualBlockExecutionData, VirtualBlockExecutorError> {
7682
let blockifier_txs = Self::convert_invoke_txs(txs)?;
77-
self.execute_inner(block_number, blockifier_txs)
78-
}
79-
80-
fn execute_inner(
81-
&self,
82-
block_number: BlockNumber,
83-
txs: Vec<BlockifierTransaction>,
84-
) -> Result<VirtualBlockExecutionData, VirtualBlockExecutorError>;
85-
86-
/// Converts Invoke transactions to blockifier transactions.
87-
///
88-
/// Uses execution flags that skip fee charging and nonce check.
89-
/// Returns an error if any transaction is not an Invoke.
90-
fn convert_invoke_txs(
91-
txs: Vec<(Transaction, TransactionHash)>,
92-
) -> Result<Vec<BlockifierTransaction>, VirtualBlockExecutorError> {
93-
// Skip validation, fee charging, and nonce check for virtual block execution.
94-
let execution_flags = ExecutionFlags {
95-
validate: true,
96-
charge_fee: false,
97-
strict_nonce_check: false,
98-
only_query: false,
99-
};
100-
101-
txs.into_iter()
102-
.map(|(tx, tx_hash)| {
103-
if !matches!(tx, Transaction::Invoke(_)) {
104-
return Err(VirtualBlockExecutorError::UnsupportedTransactionType);
105-
}
106-
107-
BlockifierTransaction::from_api(
108-
tx,
109-
tx_hash,
110-
None, // class_info - not needed for Invoke.
111-
None, // paid_fee_on_l1 - not needed for Invoke.
112-
None, // deployed_contract_address - not needed for Invoke.
113-
execution_flags.clone(),
114-
)
115-
.map_err(|e| VirtualBlockExecutorError::TransactionExecutionError(e.to_string()))
116-
})
117-
.collect()
118-
}
119-
}
120-
121-
/// RPC-based virtual block executor.
122-
///
123-
/// This executor fetches historical state from an RPC node and executes transactions
124-
/// without block preprocessing. Validation and fee charging are always skipped,
125-
/// making it suitable for simulation and OS input generation.
126-
pub struct RpcVirtualBlockExecutor {
127-
node_url: String,
128-
chain_id: ChainId,
129-
contract_class_manager: ContractClassManager,
130-
}
131-
132-
impl RpcVirtualBlockExecutor {
133-
/// Creates a new RPC-based virtual block executor.
134-
///
135-
/// # Arguments
136-
///
137-
/// * `node_url` - URL of the RPC node to fetch state from
138-
/// * `chain_id` - The chain ID for transaction hash computation
139-
/// * `contract_class_manager` - Manager for compiled contract classes
140-
pub fn new(
141-
node_url: String,
142-
chain_id: ChainId,
143-
contract_class_manager: ContractClassManager,
144-
) -> Self {
145-
Self { node_url, chain_id, contract_class_manager }
146-
}
147-
}
148-
149-
impl VirtualBlockExecutor for RpcVirtualBlockExecutor {
150-
fn execute_inner(
151-
&self,
152-
block_number: BlockNumber,
153-
txs: Vec<BlockifierTransaction>,
154-
) -> Result<VirtualBlockExecutionData, VirtualBlockExecutorError> {
155-
// Create RPC state reader for the given block.
156-
let rpc_state_reader = RpcStateReader::new_with_config_from_url(
157-
self.node_url.clone(),
158-
self.chain_id.clone(),
159-
block_number,
160-
);
161-
162-
// Get block context from RPC.
163-
let block_context = rpc_state_reader
164-
.get_block_context()
165-
.map_err(|e| VirtualBlockExecutorError::ReexecutionError(Box::new(e)))?;
83+
let block_context = self.block_context(block_number)?;
84+
let state_reader = self.state_reader(block_number)?;
16685

16786
// Create state reader with contract manager.
168-
let state_reader_and_contract_manager = StateReaderAndContractManager::new(
169-
rpc_state_reader,
170-
self.contract_class_manager.clone(),
171-
None,
172-
);
87+
let state_reader_and_contract_manager =
88+
StateReaderAndContractManager::new(state_reader, contract_class_manager, None);
17389

17490
let block_state = CachedState::new(state_reader_and_contract_manager);
17591

@@ -181,7 +97,7 @@ impl VirtualBlockExecutor for RpcVirtualBlockExecutor {
18197
);
18298

18399
// Execute all transactions.
184-
let execution_results = transaction_executor.execute_txs(&txs, None);
100+
let execution_results = transaction_executor.execute_txs(&blockifier_txs, None);
185101

186102
// Collect results, returning error if any transaction fails.
187103
let execution_outputs: Vec<TransactionExecutionOutput> = execution_results
@@ -203,4 +119,77 @@ impl VirtualBlockExecutor for RpcVirtualBlockExecutor {
203119

204120
Ok(VirtualBlockExecutionData { execution_outputs, block_context, initial_reads })
205121
}
122+
123+
/// Converts Invoke transactions to blockifier transactions.
124+
///
125+
/// Uses execution flags that skip fee charging and nonce check.
126+
/// Returns an error if any transaction is not an Invoke.
127+
fn convert_invoke_txs(
128+
txs: Vec<(Transaction, TransactionHash)>,
129+
) -> Result<Vec<BlockifierTransaction>, VirtualBlockExecutorError> {
130+
txs.into_iter()
131+
.map(|(tx, tx_hash)| {
132+
if let Transaction::Invoke(invoke_tx) = tx {
133+
// Execute with validation, conditional fee charging based on resource bounds,
134+
// but skip strict nonce check for virtual block execution.
135+
let execution_flags = ExecutionFlags {
136+
only_query: false,
137+
charge_fee: invoke_tx.resource_bounds().max_possible_fee(invoke_tx.tip())
138+
> Fee(0),
139+
validate: true,
140+
strict_nonce_check: false,
141+
};
142+
143+
BlockifierTransaction::from_api(
144+
Transaction::Invoke(invoke_tx),
145+
tx_hash,
146+
None, // class_info - not needed for Invoke.
147+
None, // paid_fee_on_l1 - not needed for Invoke.
148+
None, // deployed_contract_address - not needed for Invoke.
149+
execution_flags,
150+
)
151+
.map_err(|e| {
152+
VirtualBlockExecutorError::TransactionExecutionError(e.to_string())
153+
})
154+
} else {
155+
Err(VirtualBlockExecutorError::UnsupportedTransactionType)
156+
}
157+
})
158+
.collect()
159+
}
160+
/// Returns the block context for the given block number.
161+
fn block_context(
162+
&self,
163+
block_number: BlockNumber,
164+
) -> Result<BlockContext, VirtualBlockExecutorError>;
165+
166+
/// Returns a state reader that implements `FetchCompiledClasses` for the given block number.
167+
/// Must be `Send + Sync + 'static` to be used in the transaction executor.
168+
fn state_reader(
169+
&self,
170+
block_number: BlockNumber,
171+
) -> Result<impl FetchCompiledClasses + Send + Sync + 'static, VirtualBlockExecutorError>;
172+
}
173+
174+
/// RPC-based virtual block executor.
175+
///
176+
/// This executor fetches historical state from an RPC node and executes transactions
177+
/// without block preprocessing. Validation and fee charging are always skipped,
178+
/// making it suitable for simulation and OS input generation.
179+
impl VirtualBlockExecutor for RpcStateReader {
180+
fn block_context(
181+
&self,
182+
_block_number: BlockNumber,
183+
) -> Result<BlockContext, VirtualBlockExecutorError> {
184+
self.get_block_context()
185+
.map_err(|e| VirtualBlockExecutorError::ReexecutionError(Box::new(e)))
186+
}
187+
188+
fn state_reader(
189+
&self,
190+
_block_number: BlockNumber,
191+
) -> Result<impl FetchCompiledClasses + Send + Sync + 'static, VirtualBlockExecutorError> {
192+
// RpcStateReader itself is the state reader
193+
Ok(self.clone())
194+
}
206195
}

crates/starknet_os_runner/src/virtual_block_executor_test.rs

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@ use std::env;
22

33
use blockifier::blockifier::config::ContractClassManagerConfig;
44
use blockifier::state::contract_class_manager::ContractClassManager;
5-
use blockifier::transaction::account_transaction::ExecutionFlags;
65
use blockifier::transaction::transaction_execution::Transaction as BlockifierTransaction;
6+
use blockifier_reexecution::state_reader::rpc_state_reader::RpcStateReader;
77
use starknet_api::abi::abi_utils::{get_storage_var_address, selector_from_name};
88
use starknet_api::block::BlockNumber;
99
use starknet_api::core::{ChainId, ContractAddress, Nonce};
1010
use starknet_api::test_utils::invoke::invoke_tx;
11-
use starknet_api::transaction::Transaction;
11+
use starknet_api::transaction::{Transaction, TransactionHash};
1212
use starknet_api::{calldata, felt, invoke_tx_args};
1313

14-
use crate::virtual_block_executor::{RpcVirtualBlockExecutor, VirtualBlockExecutor};
14+
use crate::errors::VirtualBlockExecutorError;
15+
use crate::virtual_block_executor::VirtualBlockExecutor;
1516

1617
/// Block number to use for testing (mainnet block with known state).
1718
const TEST_BLOCK_NUMBER: u64 = 800000;
@@ -25,11 +26,53 @@ const STRK_TOKEN_ADDRESS: &str =
2526
/// [call_array_len, (to, selector, data_offset, data_len)..., calldata_len, calldata...]
2627
const SENDER_ADDRESS: &str = "0x01176a1bd84444c89232ec27754698e5d2e7e1a7f1539f12027f28b23ec9f3d8";
2728

29+
/// Test wrapper for RpcStateReader that overrides execution flags to skip validation.
30+
struct TestRpcVirtualBlockExecutor(RpcStateReader);
31+
32+
impl VirtualBlockExecutor for TestRpcVirtualBlockExecutor {
33+
fn block_context(
34+
&self,
35+
block_number: BlockNumber,
36+
) -> Result<blockifier::context::BlockContext, VirtualBlockExecutorError> {
37+
self.0.block_context(block_number)
38+
}
39+
40+
fn state_reader(
41+
&self,
42+
block_number: BlockNumber,
43+
) -> Result<
44+
impl blockifier::state::state_reader_and_contract_manager::FetchCompiledClasses
45+
+ Send
46+
+ Sync
47+
+ 'static,
48+
VirtualBlockExecutorError,
49+
> {
50+
self.0.state_reader(block_number)
51+
}
52+
53+
// Override the default implementation to skip validation.
54+
fn convert_invoke_txs(
55+
txs: Vec<(Transaction, TransactionHash)>,
56+
) -> Result<Vec<BlockifierTransaction>, VirtualBlockExecutorError> {
57+
// Call the default trait implementation.
58+
let mut blockifier_txs = RpcStateReader::convert_invoke_txs(txs)?;
59+
60+
// Modify validate flag to false for all transactions.
61+
for tx in &mut blockifier_txs {
62+
if let BlockifierTransaction::Account(account_tx) = tx {
63+
account_tx.execution_flags.validate = false;
64+
}
65+
}
66+
67+
Ok(blockifier_txs)
68+
}
69+
}
70+
2871
/// Constructs an Invoke transaction that calls `balanceOf` on the STRK token contract.
2972
///
3073
/// Since we skip validation and fee charging, we can use dummy values for signature,
3174
/// nonce, and resource bounds.
32-
fn construct_balance_of_invoke() -> BlockifierTransaction {
75+
fn construct_balance_of_invoke() -> (Transaction, TransactionHash) {
3376
let strk_token = ContractAddress::try_from(felt!(STRK_TOKEN_ADDRESS)).unwrap();
3477
let sender = ContractAddress::try_from(felt!(SENDER_ADDRESS)).unwrap();
3578

@@ -56,24 +99,7 @@ fn construct_balance_of_invoke() -> BlockifierTransaction {
5699

57100
let tx = Transaction::Invoke(invoke_tx);
58101
let tx_hash = tx.calculate_transaction_hash(&ChainId::Mainnet).unwrap();
59-
60-
// Skip fee charging, nonce check and validation.
61-
let execution_flags = ExecutionFlags {
62-
validate: false,
63-
charge_fee: false,
64-
strict_nonce_check: false,
65-
only_query: false,
66-
};
67-
68-
BlockifierTransaction::from_api(
69-
tx,
70-
tx_hash,
71-
None, // class_info - not needed for Invoke.
72-
None, // paid_fee_on_l1 - not needed for Invoke.
73-
None, // deployed_contract_address - not needed for Invoke.
74-
execution_flags,
75-
)
76-
.unwrap()
102+
(tx, tx_hash)
77103
}
78104

79105
/// Integration test for RpcVirtualBlockExecutor with a constructed transaction.
@@ -93,22 +119,26 @@ fn construct_balance_of_invoke() -> BlockifierTransaction {
93119
/// NODE_URL=https://your-rpc-node cargo test -p starknet_os_runner -- --ignored
94120
/// ```
95121
#[test]
96-
#[ignore] // Requires RPC access - run with: cargo test -p starknet_os_runner -- --ignored
122+
#[ignore] // Requires RPC access
97123
fn test_execute_constructed_balance_of_transaction() {
98124
let node_url =
99125
env::var("NODE_URL").expect("NODE_URL environment variable required for this test");
100126

101127
// Construct a balanceOf transaction (with execution flags set).
102-
let tx = construct_balance_of_invoke();
128+
let (tx, tx_hash) = construct_balance_of_invoke();
103129

104130
// Create the virtual block executor.
105131
let contract_class_manager = ContractClassManager::start(ContractClassManagerConfig::default());
106-
let executor = RpcVirtualBlockExecutor::new(node_url, ChainId::Mainnet, contract_class_manager);
132+
let executor = TestRpcVirtualBlockExecutor(RpcStateReader::new_with_config_from_url(
133+
node_url,
134+
ChainId::Mainnet,
135+
BlockNumber(TEST_BLOCK_NUMBER),
136+
));
107137

108138
// Execute the transaction.
109139
let result = executor
110-
.execute_inner(BlockNumber(TEST_BLOCK_NUMBER), vec![tx])
111-
.expect("Virtual block execution should succeed");
140+
.execute(BlockNumber(TEST_BLOCK_NUMBER), contract_class_manager, vec![(tx, tx_hash)])
141+
.unwrap();
112142

113143
// Verify execution produced output.
114144
assert_eq!(result.execution_outputs.len(), 1, "Should have exactly one execution output");

0 commit comments

Comments
 (0)