Skip to content

Commit e5a95cf

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

File tree

5 files changed

+168
-20
lines changed

5 files changed

+168
-20
lines changed

crates/apollo_gateway/src/gateway_test.rs

Lines changed: 9 additions & 1 deletion
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, store_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
};
@@ -284,6 +285,13 @@ async fn setup_mock_state(
284285
&mut mock_dependencies.state_reader_factory.state_reader.blockifier_state_reader,
285286
);
286287

288+
if let RpcTransaction::Invoke(RpcInvokeTransaction::V3(invoke_tx)) = input_tx.clone() {
289+
store_block_hash_from_proof_facts(
290+
&invoke_tx.proof_facts,
291+
&mut mock_dependencies.state_reader_factory.state_reader.blockifier_state_reader,
292+
);
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/initial_test_state.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ 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;
13+
use crate::blockifier_versioned_constants::VersionedConstants;
1214
use crate::context::ChainInfo;
1315
use crate::state::cached_state::CachedState;
1416
use crate::state::contract_class_manager::ContractClassManager;
@@ -36,6 +38,26 @@ pub fn fund_account(
3638
}
3739
}
3840

41+
/// Stores block hash in the block hash contract from SNOS proof facts.
42+
/// This is a generic helper that can be used in any test that needs to set up block hashes
43+
/// for proof facts validation.
44+
pub fn store_block_hash_from_proof_facts(
45+
proof_facts: &ProofFacts,
46+
state_reader: &mut DictStateReader,
47+
) {
48+
if let Ok(ProofFactsVariant::Snos(snos_proof_facts)) = ProofFactsVariant::try_from(proof_facts)
49+
{
50+
let block_hash_contract_address = VersionedConstants::create_for_testing()
51+
.os_constants
52+
.os_contract_addresses
53+
.block_hash_contract_address();
54+
state_reader.storage_view.insert(
55+
(block_hash_contract_address, StorageKey::from(snos_proof_facts.block_number.0)),
56+
snos_proof_facts.block_hash.0,
57+
);
58+
}
59+
}
60+
3961
/// Sets up a state reader for testing:
4062
/// * "Declares" a Cairo0 ERC20 contract (class hash => class mapping set).
4163
/// * "Deploys" two ERC20 contracts (address => class hash mapping set) at the fee token addresses
@@ -83,6 +105,7 @@ pub fn setup_test_state(
83105
}
84106
}
85107

108+
// TODO(Meshi): create a client-side test state.
86109
pub fn test_state(
87110
chain_info: &ChainInfo,
88111
initial_balances: Fee,

crates/blockifier/src/transaction/account_transaction.rs

Lines changed: 67 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,65 @@ 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 that we validate here.
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+
let proof_facts = invoke_tx.proof_facts();
266+
let proof_facts_variant = ProofFactsVariant::try_from(&proof_facts)
267+
.map_err(|e| TransactionPreValidationError::InvalidProofFacts(e.to_string()))?;
268+
269+
let snos_proof_facts = match proof_facts_variant {
270+
ProofFactsVariant::Empty => return Ok(()),
271+
ProofFactsVariant::Snos(snos_proof_facts) => snos_proof_facts,
272+
};
273+
274+
let proof_block_number = snos_proof_facts.block_number;
275+
let proof_block_hash = snos_proof_facts.block_hash.0;
276+
277+
if current_block_number.0 < proof_block_number.0 {
278+
return Err(TransactionPreValidationError::InvalidProofFacts(format!(
279+
"Current block number {current_block_number} is less than proof block number \
280+
{proof_block_number}."
281+
)));
282+
}
283+
284+
// Validate the proof block number is not too recent.
285+
// Block-number to block-hash mapping is only guaranteed up to
286+
// current_block_number - STORED_BLOCK_HASH_BUFFER.
287+
if current_block_number.0 <= STORED_BLOCK_HASH_BUFFER + proof_block_number.0 {
288+
return Err(TransactionPreValidationError::InvalidProofFacts(format!(
289+
"Proof block number {proof_block_number} is too recent. Block hashes are only \
290+
available for blocks ≤ current_block_number - STORED_BLOCK_HASH_BUFFER \
291+
({current_block_number} - {STORED_BLOCK_HASH_BUFFER})."
292+
)));
293+
}
294+
295+
// Validate that the proof block hash matches the stored hash.
296+
let block_hash_contract_address =
297+
os_constants.os_contract_addresses.block_hash_contract_address();
298+
299+
let stored_block_hash = state
300+
.get_storage_at(block_hash_contract_address, StorageKey::from(proof_block_number.0))?;
301+
302+
assert_ne!(stored_block_hash, Felt::ZERO, "Stored block hash is zero");
303+
304+
if stored_block_hash != proof_block_hash {
305+
return Err(TransactionPreValidationError::InvalidProofFacts(format!(
306+
"Block hash mismatch for block {proof_block_number}. Proof block hash: \
307+
{proof_block_hash}, stored block hash: {stored_block_hash}."
308+
)));
261309
}
310+
262311
Ok(())
263312
}
264313

@@ -278,7 +327,11 @@ impl AccountTransaction {
278327
verify_can_pay_committed_bounds(state, tx_context).map_err(Box::new)?;
279328
}
280329

281-
self.validate_proof_facts()?;
330+
self.validate_proof_facts(
331+
&tx_context.block_context.versioned_constants.os_constants,
332+
tx_context.block_context.block_info.block_number,
333+
state,
334+
)?;
282335

283336
Ok(())
284337
}

crates/starknet_os_flow_tests/src/test_manager.rs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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,38 @@ 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> = LazyLock::new(|| {
123+
VersionedConstants::create_for_testing()
124+
.os_constants
125+
.os_contract_addresses
126+
.block_hash_contract_address()
127+
});
128+
129+
/// Stores block hash for invoke transactions with SNOS proof facts.
130+
/// This is needed so the OS can verify block hash proofs during execution.
131+
fn store_block_hash_for_proof_facts<S: UpdatableState>(tx: &BlockifierTransaction, state: &mut S) {
132+
if let BlockifierTransaction::Account(account_tx) = tx {
133+
if let AccountTransaction::Invoke(invoke_tx) = &account_tx.tx {
134+
if let Ok(ProofFactsVariant::Snos(snos_proof_facts)) =
135+
ProofFactsVariant::try_from(&invoke_tx.proof_facts())
136+
{
137+
let storage_writes = StateMaps {
138+
storage: HashMap::from([(
139+
(
140+
*BLOCK_HASH_CONTRACT_ADDRESS,
141+
StorageKey::from(snos_proof_facts.block_number.0),
142+
),
143+
snos_proof_facts.block_hash.0,
144+
)]),
145+
..Default::default()
146+
};
147+
state.apply_writes(&storage_writes, &HashMap::new());
148+
}
149+
}
150+
}
151+
}
152+
121153
#[derive(Default)]
122154
pub(crate) struct TestBuilderConfig {
123155
pub(crate) use_kzg_da: bool,
@@ -257,6 +289,23 @@ impl<S: FlowTestState> OsTestOutput<S> {
257289
);
258290
}
259291

292+
/// Adds the proof facts block hash storage mapping to the decompressed state diff.
293+
/// This is needed because the Blockifier writes block hash storage for proof facts
294+
/// verification, but the OS doesn't include this in its output.
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_for_proof_facts(&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: 14 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,18 @@ 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+
// Add proof facts block hash storage to the decompressed state diff.
1172+
// The Blockifier writes this for proof facts verification, but the OS doesn't include it.
1173+
// TODO(Meshi): write the mapping for the os validation as well.
1174+
if let Ok(ProofFactsVariant::Snos(snos_proof_facts)) = ProofFactsVariant::try_from(&proof_facts)
1175+
{
1176+
test_output.add_proof_facts_block_hash_storage(
1177+
snos_proof_facts.block_number,
1178+
snos_proof_facts.block_hash,
1179+
);
1180+
}
11691181

11701182
// Perform general validations and storage update validations.
11711183
test_output.perform_default_validations();

0 commit comments

Comments
 (0)