Skip to content

Commit fbca6e8

Browse files
blockifier: verify that the proof facts block number matches the block hash
1 parent 8700e78 commit fbca6e8

File tree

6 files changed

+175
-22
lines changed

6 files changed

+175
-22
lines changed

crates/apollo_gateway/src/gateway_test.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use apollo_network_types::network_types::BroadcastedMessageMetadata;
3838
use apollo_test_utils::{get_rng, GetTestInstance};
3939
use blockifier::blockifier::config::ContractClassManagerConfig;
4040
use blockifier::context::ChainInfo;
41-
use blockifier::test_utils::initial_test_state::fund_account;
41+
use blockifier::test_utils::initial_test_state::{fund_account, setup_block_hash_from_proof_facts};
4242
use blockifier_test_utils::cairo_versions::{CairoVersion, RunnableCairo1};
4343
use blockifier_test_utils::calldata::create_trivial_calldata;
4444
use blockifier_test_utils::contracts::FeatureContract;
@@ -52,6 +52,7 @@ use starknet_api::executable_transaction::AccountTransaction;
5252
use starknet_api::rpc_transaction::{
5353
InternalRpcTransaction,
5454
RpcDeclareTransaction,
55+
RpcInvokeTransaction,
5556
RpcTransaction,
5657
RpcTransactionLabelValue,
5758
};
@@ -276,14 +277,21 @@ async fn setup_mock_state(
276277

277278
setup_transaction_converter_mock(&mut mock_dependencies.mock_transaction_converter, tx_args);
278279

280+
// Setup state: fund account and store proof block hash if needed.
281+
let state_reader =
282+
&mut mock_dependencies.state_reader_factory.state_reader.blockifier_state_reader;
279283
let address = expected_internal_tx.contract_address();
280284
fund_account(
281285
&mock_dependencies.config.chain_info,
282286
address,
283287
VALID_ACCOUNT_BALANCE,
284-
&mut mock_dependencies.state_reader_factory.state_reader.blockifier_state_reader,
288+
state_reader,
285289
);
286290

291+
if let RpcTransaction::Invoke(RpcInvokeTransaction::V3(invoke_tx)) = &input_tx {
292+
setup_block_hash_from_proof_facts(&invoke_tx.proof_facts, state_reader);
293+
}
294+
287295
let mempool_add_tx_args = AddTransactionArgs {
288296
tx: expected_internal_tx.clone(),
289297
account_state: AccountState { address, nonce: *input_tx.nonce() },

crates/blockifier/src/test_utils.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,3 +416,11 @@ pub fn maybe_dummy_block_hash_and_number(block_number: BlockNumber) -> Option<Bl
416416
hash: BlockHash(StarkHash::ONE),
417417
})
418418
}
419+
420+
/// Returns the contract address for the block hash contract used in tests.
421+
pub fn block_hash_contract_address() -> ContractAddress {
422+
VersionedConstants::create_for_testing()
423+
.os_constants
424+
.os_contract_addresses
425+
.block_hash_contract_address()
426+
}

crates/blockifier/src/test_utils/initial_test_state.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use starknet_api::block::FeeType;
55
use starknet_api::contract_class::compiled_class_hash::HashVersion;
66
use starknet_api::core::ContractAddress;
77
use starknet_api::felt;
8-
use starknet_api::transaction::fields::Fee;
8+
use starknet_api::state::StorageKey;
9+
use starknet_api::transaction::fields::{Fee, ProofFacts, ProofFactsVariant};
910
use strum::IntoEnumIterator;
1011

1112
use crate::blockifier::config::ContractClassManagerConfig;
@@ -16,6 +17,7 @@ use crate::state::state_reader_and_contract_manager::{
1617
FetchCompiledClasses,
1718
StateReaderAndContractManager,
1819
};
20+
use crate::test_utils::block_hash_contract_address;
1921
use crate::test_utils::contracts::FeatureContractData;
2022
use crate::test_utils::dict_state_reader::DictStateReader;
2123

@@ -36,6 +38,20 @@ pub fn fund_account(
3638
}
3739
}
3840

41+
/// Sets up block hash in the block hash contract from SNOS proof facts.
42+
///
43+
/// This is a test helper used to prepare state for proof-facts validation.
44+
pub fn setup_block_hash_from_proof_facts(
45+
proof_facts: &ProofFacts,
46+
state_reader: &mut DictStateReader,
47+
) {
48+
if let Ok(ProofFactsVariant::Snos(snos_proof_facts)) = proof_facts.try_into() {
49+
let contract = block_hash_contract_address();
50+
let storage_key = StorageKey::from(snos_proof_facts.block_number.0);
51+
state_reader.storage_view.insert((contract, storage_key), snos_proof_facts.block_hash.0);
52+
}
53+
}
54+
3955
/// Sets up a state reader for testing:
4056
/// * "Declares" a Cairo0 ERC20 contract (class hash => class mapping set).
4157
/// * "Deploys" two ERC20 contracts (address => class hash mapping set) at the fee token addresses
@@ -83,6 +99,7 @@ pub fn setup_test_state(
8399
}
84100
}
85101

102+
// TODO(Meshi): create a client-side test state.
86103
pub fn test_state(
87104
chain_info: &ChainInfo,
88105
initial_balances: Fee,

crates/blockifier/src/transaction/account_transaction.rs

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
use std::sync::Arc;
22

33
use starknet_api::abi::abi_utils::selector_from_name;
4-
use starknet_api::block::GasPriceVector;
4+
use starknet_api::block::{BlockNumber, GasPriceVector};
55
use starknet_api::calldata;
66
use starknet_api::contract_class::EntryPointType;
77
use starknet_api::core::{ClassHash, ContractAddress, EntryPointSelector, Nonce};
88
use starknet_api::data_availability::DataAvailabilityMode;
99
use starknet_api::executable_transaction::{AccountTransaction as Transaction, TransactionType};
1010
use starknet_api::execution_resources::GasAmount;
11+
use starknet_api::state::StorageKey;
1112
use starknet_api::transaction::fields::Resource::{L1DataGas, L1Gas, L2Gas};
1213
use starknet_api::transaction::fields::{
1314
AccountDeploymentData,
@@ -24,6 +25,8 @@ use starknet_api::transaction::{constants, TransactionHash, TransactionVersion};
2425
use starknet_types_core::felt::Felt;
2526

2627
use super::errors::ResourceBoundsError;
28+
use crate::abi::constants::STORED_BLOCK_HASH_BUFFER;
29+
use crate::blockifier_versioned_constants::OsConstants;
2730
use crate::context::{BlockContext, GasCounter, TransactionContext};
2831
use crate::execution::call_info::CallInfo;
2932
use crate::execution::common_hints::ExecutionMode;
@@ -246,19 +249,57 @@ impl AccountTransaction {
246249
}
247250
}
248251

249-
fn validate_proof_facts(&self) -> TransactionPreValidationResult<()> {
250-
if let Transaction::Invoke(tx) = &self.tx {
251-
if tx.tx.version() == TransactionVersion::THREE {
252-
let proof_facts_variant = ProofFactsVariant::try_from(&tx.tx.proof_facts())
253-
.map_err(|e| TransactionPreValidationError::InvalidProofFacts(e.to_string()))?;
254-
match proof_facts_variant {
255-
ProofFactsVariant::Empty => {}
256-
ProofFactsVariant::Snos(_snos_proof_facts) => {
257-
// TODO(Meshi/ AvivG): add proof facts validations.
258-
}
259-
}
260-
}
252+
fn validate_proof_facts(
253+
&self,
254+
os_constants: &OsConstants,
255+
current_block_number: BlockNumber,
256+
state: &mut dyn State,
257+
) -> TransactionPreValidationResult<()> {
258+
// Only Invoke V3 transactions can carry proof facts.
259+
let Transaction::Invoke(invoke_tx) = &self.tx else {
260+
return Ok(());
261+
};
262+
if invoke_tx.version() != TransactionVersion::THREE {
263+
return Ok(());
264+
}
265+
266+
// Parse proof facts.
267+
let proof_facts = invoke_tx.proof_facts();
268+
let snos_proof_facts = match ProofFactsVariant::try_from(&proof_facts)
269+
.map_err(|e| TransactionPreValidationError::InvalidProofFacts(e.to_string()))?
270+
{
271+
ProofFactsVariant::Empty => return Ok(()),
272+
ProofFactsVariant::Snos(snos_proof_facts) => snos_proof_facts,
273+
};
274+
275+
// Proof block must be old enough to have a stored block hash.
276+
// Stored block hashes are guaranteed only up to: current - STORED_BLOCK_HASH_BUFFER.
277+
let max_allowed = current_block_number.0.saturating_sub(STORED_BLOCK_HASH_BUFFER);
278+
279+
let proof_block_number = snos_proof_facts.block_number.0;
280+
if proof_block_number > max_allowed {
281+
return Err(TransactionPreValidationError::InvalidProofFacts(format!(
282+
"Invalid proof block number {proof_block_number}. Block hashes are only available \
283+
for blocks ≤ current_block_number - STORED_BLOCK_HASH_BUFFER \
284+
({current_block_number} - {STORED_BLOCK_HASH_BUFFER} = {max_allowed})."
285+
)));
286+
}
287+
288+
// Compare the proof's block hash with the stored block hash.
289+
let contract = os_constants.os_contract_addresses.block_hash_contract_address();
290+
291+
let stored_block_hash =
292+
state.get_storage_at(contract, StorageKey::from(proof_block_number))?;
293+
assert_ne!(stored_block_hash, Felt::ZERO, "Stored block hash is zero");
294+
295+
let proof_block_hash = snos_proof_facts.block_hash.0;
296+
if stored_block_hash != proof_block_hash {
297+
return Err(TransactionPreValidationError::InvalidProofFacts(format!(
298+
"Block hash mismatch for block {proof_block_number}. Proof block hash: \
299+
{proof_block_hash}, stored block hash: {stored_block_hash}."
300+
)));
261301
}
302+
262303
Ok(())
263304
}
264305

@@ -278,7 +319,11 @@ impl AccountTransaction {
278319
verify_can_pay_committed_bounds(state, tx_context).map_err(Box::new)?;
279320
}
280321

281-
self.validate_proof_facts()?;
322+
self.validate_proof_facts(
323+
&tx_context.block_context.versioned_constants.os_constants,
324+
tx_context.block_context.block_info.block_number,
325+
state,
326+
)?;
282327

283328
Ok(())
284329
}

crates/starknet_os_flow_tests/src/test_manager.rs

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use blockifier::state::cached_state::{CachedState, StateMaps};
88
use blockifier::state::state_api::{StateReader, UpdatableState};
99
use blockifier::state::stateful_compression_test_utils::decompress;
1010
use blockifier::test_utils::dict_state_reader::DictStateReader;
11-
use blockifier::test_utils::ALIAS_CONTRACT_ADDRESS;
11+
use blockifier::test_utils::{block_hash_contract_address, ALIAS_CONTRACT_ADDRESS};
1212
use blockifier::transaction::objects::TransactionExecutionInfo;
1313
use blockifier::transaction::transaction_execution::Transaction as BlockifierTransaction;
1414
use blockifier_test_utils::calldata::create_calldata;
@@ -47,7 +47,7 @@ use starknet_api::invoke_tx_args;
4747
use starknet_api::state::{SierraContractClass, StorageKey};
4848
use starknet_api::test_utils::invoke::{invoke_tx, InvokeTxArgs};
4949
use starknet_api::test_utils::{NonceManager, CHAIN_ID_FOR_TESTS};
50-
use starknet_api::transaction::fields::{Calldata, Fee, Tip};
50+
use starknet_api::transaction::fields::{Calldata, Fee, ProofFactsVariant, Tip};
5151
use starknet_api::transaction::{L1HandlerTransaction, L1ToL2Payload, MessageToL1};
5252
use starknet_committer::block_committer::input::{
5353
IsSubset,
@@ -118,6 +118,36 @@ pub(crate) static STRK_FEE_TOKEN_ADDRESS: LazyLock<ContractAddress> = LazyLock::
118118
pub(crate) static FUNDED_ACCOUNT_ADDRESS: LazyLock<ContractAddress> =
119119
LazyLock::new(|| get_initial_deploy_account_tx().contract_address);
120120

121+
/// The block hash contract address, cached to avoid repeated VersionedConstants creation.
122+
static BLOCK_HASH_CONTRACT_ADDRESS: LazyLock<ContractAddress> =
123+
LazyLock::new(block_hash_contract_address);
124+
125+
/// Stores block hash for invoke transactions with SNOS proof facts.
126+
/// This is needed so the OS can verify block hash proofs during execution.
127+
// TODO(Meshi): change it so the block number to block hash mapping will be a part of the state and
128+
// not written during the transaction execution.
129+
fn store_block_hash_from_transaction<S: UpdatableState>(tx: &BlockifierTransaction, state: &mut S) {
130+
if let BlockifierTransaction::Account(account_tx) = tx {
131+
if let AccountTransaction::Invoke(invoke_tx) = &account_tx.tx {
132+
if let Ok(ProofFactsVariant::Snos(snos_proof_facts)) =
133+
ProofFactsVariant::try_from(&invoke_tx.proof_facts())
134+
{
135+
let storage_writes = StateMaps {
136+
storage: HashMap::from([(
137+
(
138+
*BLOCK_HASH_CONTRACT_ADDRESS,
139+
StorageKey::from(snos_proof_facts.block_number.0),
140+
),
141+
snos_proof_facts.block_hash.0,
142+
)]),
143+
..Default::default()
144+
};
145+
state.apply_writes(&storage_writes, &HashMap::new());
146+
}
147+
}
148+
}
149+
}
150+
121151
#[derive(Default)]
122152
pub(crate) struct TestBuilderConfig {
123153
pub(crate) use_kzg_da: bool,
@@ -257,6 +287,25 @@ impl<S: FlowTestState> OsTestOutput<S> {
257287
);
258288
}
259289

290+
/// Adds the proof facts block hash storage mapping to the decompressed state diff.
291+
/// This is needed because the Blockifier writes block hash storage for proof facts
292+
/// verification, but the OS doesn't include this in its output.
293+
// TODO(Meshi): Remove this once we have a solution that dose not write to the state during the
294+
// transaction execution.
295+
pub(crate) fn add_proof_facts_block_hash_storage(
296+
&mut self,
297+
block_number: BlockNumber,
298+
block_hash: BlockHash,
299+
) {
300+
let storage_key = StarknetStorageKey(StorageKey::from(block_number.0));
301+
let storage_value = StarknetStorageValue(block_hash.0);
302+
self.decompressed_state_diff
303+
.storage_updates
304+
.entry(*BLOCK_HASH_CONTRACT_ADDRESS)
305+
.or_default()
306+
.insert(storage_key, storage_value);
307+
}
308+
260309
fn perform_global_validations(&self) {
261310
// TODO(Dori): Implement global validations for the OS test output.
262311

@@ -749,7 +798,10 @@ impl<S: FlowTestState> TestBuilder<S> {
749798

750799
let (block_txs, revert_reasons): (Vec<_>, Vec<_>) = block_txs_with_reason
751800
.into_iter()
752-
.map(|flow_test_tx| (flow_test_tx.tx, flow_test_tx.expected_revert_reason))
801+
.map(|flow_test_tx| {
802+
store_block_hash_from_transaction(&flow_test_tx.tx, &mut state);
803+
(flow_test_tx.tx, flow_test_tx.expected_revert_reason)
804+
})
753805
.unzip();
754806
// Clone the block info for later use.
755807
let block_info = block_context.block_info().clone();

crates/starknet_os_flow_tests/src/tests.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ use starknet_api::transaction::fields::{
5555
ContractAddressSalt,
5656
Fee,
5757
ProofFacts,
58+
ProofFactsVariant,
5859
ResourceBounds,
5960
Tip,
6061
TransactionSignature,
@@ -1086,7 +1087,7 @@ async fn test_new_class_execution_info(#[values(true, false)] use_kzg_da: bool)
10861087
main_contract_address, test_execution_info_selector_name, &expected_execution_info
10871088
),
10881089
resource_bounds: *NON_TRIVIAL_RESOURCE_BOUNDS,
1089-
proof_facts,
1090+
proof_facts: proof_facts.clone(),
10901091
};
10911092
// Put the tx hash in the signature.
10921093
let tx = InvokeTransaction::create(invoke_tx(invoke_tx_args.clone()), chain_id).unwrap();
@@ -1165,7 +1166,29 @@ async fn test_new_class_execution_info(#[values(true, false)] use_kzg_da: bool)
11651166
);
11661167

11671168
// Run the test.
1168-
let test_output = test_builder.build_and_run().await;
1169+
let mut test_output = test_builder.build_and_run().await;
1170+
1171+
// Reconcile proof facts block-hash storage between Blockifier and OS outputs.
1172+
//
1173+
// Context: To allow testing transactions with proof_facts validations, the Blockifier writes a
1174+
// storage entry: (block_hash_contract, proof_block_number) -> proof_block_hash.
1175+
//
1176+
// However, the OS doesn't emit this storage write in its output (the proof facts verification
1177+
// was not yet added to the OS, and thus having non non-empty (block_number -> block_hash)
1178+
// mapping is not yet required for the tests to pass).
1179+
//
1180+
// This causes a mismatch: Blockifier's state diff includes the write, but OS output doesn't.
1181+
// We manually add it here so the test comparison passes.
1182+
//
1183+
// TODO(Meshi): Move block hash storage setup to not affect the writes, then remove this fixup.
1184+
1185+
if let Ok(ProofFactsVariant::Snos(snos_proof_facts)) = ProofFactsVariant::try_from(&proof_facts)
1186+
{
1187+
test_output.add_proof_facts_block_hash_storage(
1188+
snos_proof_facts.block_number,
1189+
snos_proof_facts.block_hash,
1190+
);
1191+
}
11691192

11701193
// Perform general validations and storage update validations.
11711194
test_output.perform_default_validations();

0 commit comments

Comments
 (0)