Skip to content

Commit 5a200cb

Browse files
blockifier: verify that the proof facts block number matches the block hash
1 parent 8ba1455 commit 5a200cb

File tree

6 files changed

+290
-21
lines changed

6 files changed

+290
-21
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/blockifier/src/transaction/account_transactions_test.rs

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use starknet_api::abi::abi_utils::{
1717
get_storage_var_address,
1818
selector_from_name,
1919
};
20-
use starknet_api::block::{FeeType, GasPrice};
20+
use starknet_api::block::{BlockHash, BlockNumber, FeeType, GasPrice};
2121
use starknet_api::contract_class::compiled_class_hash::{HashVersion, HashableCompiledClass};
2222
use starknet_api::contract_class::ContractClass;
2323
use starknet_api::core::{
@@ -41,6 +41,7 @@ use starknet_api::test_utils::deploy_account::executable_deploy_account_tx;
4141
use starknet_api::test_utils::invoke::{executable_invoke_tx, InvokeTxArgs};
4242
use starknet_api::test_utils::{
4343
NonceManager,
44+
CURRENT_BLOCK_NUMBER,
4445
DEFAULT_L1_DATA_GAS_MAX_AMOUNT,
4546
DEFAULT_L1_GAS_AMOUNT,
4647
DEFAULT_L2_GAS_MAX_AMOUNT,
@@ -55,9 +56,12 @@ use starknet_api::transaction::fields::{
5556
ContractAddressSalt,
5657
Fee,
5758
GasVectorComputationMode,
59+
ProofFacts,
5860
Resource,
5961
ResourceBounds,
62+
SnosProofFacts,
6063
ValidResourceBounds,
64+
VIRTUAL_SNOS,
6165
};
6266
use starknet_api::transaction::{
6367
DeclareTransaction,
@@ -79,6 +83,7 @@ use starknet_api::{
7983
};
8084
use starknet_types_core::felt::Felt;
8185

86+
use crate::abi::constants::STORED_BLOCK_HASH_BUFFER;
8287
use crate::context::{BlockContext, TransactionContext};
8388
use crate::execution::call_info::CallInfo;
8489
use crate::execution::contract_class::TrackedResource;
@@ -2191,3 +2196,119 @@ fn test_missing_validate_entrypoint_rejects(
21912196
if ret == retdata![Felt::from_hex(ENTRYPOINT_NOT_FOUND_ERROR).unwrap()]
21922197
);
21932198
}
2199+
2200+
/// Converts SnosProofFacts to ProofFacts for testing.
2201+
fn snos_to_proof_facts(snos: SnosProofFacts) -> ProofFacts {
2202+
vec![
2203+
felt!(VIRTUAL_SNOS),
2204+
snos.program_hash,
2205+
felt!(snos.block_number.0),
2206+
snos.block_hash.0,
2207+
snos.config_hash,
2208+
]
2209+
.into()
2210+
}
2211+
2212+
/// Returns valid SNOS proof facts.
2213+
/// Block number must be more than STORED_BLOCK_HASH_BUFFER blocks old (difference > buffer).
2214+
fn valid_snos_proof_facts() -> SnosProofFacts {
2215+
SnosProofFacts {
2216+
program_hash: felt!(0x1_u64),
2217+
block_number: BlockNumber(CURRENT_BLOCK_NUMBER - STORED_BLOCK_HASH_BUFFER - 1),
2218+
block_hash: BlockHash(felt!(0xABCDEF_u64)),
2219+
config_hash: felt!(0x2_u64),
2220+
}
2221+
}
2222+
2223+
/// Returns valid proof facts.
2224+
fn valid_proof_facts() -> ProofFacts {
2225+
snos_to_proof_facts(valid_snos_proof_facts())
2226+
}
2227+
2228+
/// Returns invalid proof_facts with a too recent block number.
2229+
fn too_recent_block_proof_facts() -> ProofFacts {
2230+
snos_to_proof_facts(SnosProofFacts {
2231+
block_number: BlockNumber(CURRENT_BLOCK_NUMBER - STORED_BLOCK_HASH_BUFFER),
2232+
..valid_snos_proof_facts()
2233+
})
2234+
}
2235+
2236+
/// Returns invalid proof_facts with a mismatched block hash.
2237+
fn mismatched_hash_proof_facts() -> ProofFacts {
2238+
snos_to_proof_facts(SnosProofFacts {
2239+
block_hash: BlockHash(felt!(0xDEADBEEF_u64)),
2240+
..valid_snos_proof_facts()
2241+
})
2242+
}
2243+
2244+
/// Returns invalid proof_facts with a block number greater than the current block number.
2245+
fn future_block_proof_facts() -> ProofFacts {
2246+
snos_to_proof_facts(SnosProofFacts {
2247+
block_number: BlockNumber(CURRENT_BLOCK_NUMBER + 100),
2248+
..valid_snos_proof_facts()
2249+
})
2250+
}
2251+
2252+
/// Returns invalid proof_facts with block number 0 (for testing low current block number).
2253+
fn block_zero_proof_facts() -> ProofFacts {
2254+
snos_to_proof_facts(SnosProofFacts { block_number: BlockNumber(0), ..valid_snos_proof_facts() })
2255+
}
2256+
2257+
/// Tests the `validate_proof_facts` function for Invoke V3 transactions.
2258+
/// Covers: valid proof facts, too recent block number, mismatched block hash, future block,
2259+
/// and low current block number.
2260+
#[rstest]
2261+
#[case::valid_proof_facts(valid_proof_facts(), CURRENT_BLOCK_NUMBER)]
2262+
#[should_panic(expected = "is too recent")]
2263+
#[case::too_recent_block(too_recent_block_proof_facts(), CURRENT_BLOCK_NUMBER)]
2264+
#[should_panic(expected = "Block hash mismatch")]
2265+
#[case::mismatched_block_hash(mismatched_hash_proof_facts(), CURRENT_BLOCK_NUMBER)]
2266+
#[should_panic(expected = "is less than proof block number")]
2267+
#[case::future_block(future_block_proof_facts(), CURRENT_BLOCK_NUMBER)]
2268+
#[should_panic(expected = "is less than STORED_BLOCK_HASH_BUFFER")]
2269+
#[case::low_current_block_number(block_zero_proof_facts(), STORED_BLOCK_HASH_BUFFER)]
2270+
fn test_validate_proof_facts(
2271+
default_all_resource_bounds: ValidResourceBounds,
2272+
#[case] proof_facts: ProofFacts,
2273+
#[case] current_block_number: u64,
2274+
) {
2275+
let mut block_context = BlockContext::create_for_account_testing();
2276+
block_context.block_info.block_number = BlockNumber(current_block_number);
2277+
2278+
let chain_info = &block_context.chain_info;
2279+
let account = FeatureContract::AccountWithoutValidations(CairoVersion::Cairo0);
2280+
let mut state = test_state(chain_info, BALANCE, &[(account, 1u16)]);
2281+
let account_address = account.get_instance_address(0_u16);
2282+
2283+
// Get the block hash contract address from versioned constants.
2284+
let block_hash_contract_address = block_context
2285+
.versioned_constants
2286+
.os_constants
2287+
.os_contract_addresses
2288+
.block_hash_contract_address();
2289+
2290+
// Store block hashes for test blocks.
2291+
// Valid block number: more than STORED_BLOCK_HASH_BUFFER blocks old.
2292+
let valid_proof_block_number = CURRENT_BLOCK_NUMBER - STORED_BLOCK_HASH_BUFFER - 1;
2293+
let stored_block_hash = felt!(0xABCDEF_u64);
2294+
2295+
// Store the block hash in the block hash contract.
2296+
state
2297+
.set_storage_at(
2298+
block_hash_contract_address,
2299+
StorageKey::try_from(felt!(valid_proof_block_number)).unwrap(),
2300+
stored_block_hash,
2301+
)
2302+
.unwrap();
2303+
2304+
let tx = invoke_tx_with_default_flags(invoke_tx_args! {
2305+
sender_address: account_address,
2306+
resource_bounds: default_all_resource_bounds,
2307+
version: TransactionVersion::THREE,
2308+
proof_facts: proof_facts,
2309+
});
2310+
2311+
// Run only pre-validation stage (which includes proof facts validation).
2312+
let tx_context = block_context.to_tx_context(&tx);
2313+
tx.perform_pre_validation_stage(&mut state, &tx_context).unwrap();
2314+
}

0 commit comments

Comments
 (0)