diff --git a/chain/chain/src/chain.rs b/chain/chain/src/chain.rs index 3b566b9ab2d..ecd4264e8bf 100644 --- a/chain/chain/src/chain.rs +++ b/chain/chain/src/chain.rs @@ -78,8 +78,8 @@ use near_primitives::static_clock::StaticClock; use near_primitives::transaction::{ExecutionOutcomeWithIdAndProof, SignedTransaction}; use near_primitives::types::chunk_extra::ChunkExtra; use near_primitives::types::{ - AccountId, Balance, BlockExtra, BlockHeight, BlockHeightDelta, EpochId, MerkleHash, NumBlocks, - ShardId, StateRoot, + AccountId, Balance, BlockExtra, BlockHeight, BlockHeightDelta, EpochId, Gas, MerkleHash, + NumBlocks, ShardId, StateRoot, }; use near_primitives::unwrap_or_return; #[cfg(feature = "new_epoch_sync")] @@ -458,14 +458,7 @@ impl Chain { genesis.hash(), &epoch_manager .shard_id_to_uid(chunk_header.shard_id(), &EpochId::default())?, - ChunkExtra::new( - state_root, - CryptoHash::default(), - vec![], - 0, - chain_genesis.gas_limit, - 0, - ), + Self::create_genesis_chunk_extra(state_root, chain_genesis.gas_limit), ); } @@ -577,6 +570,29 @@ impl Chain { self.last_time_head_updated } + fn create_genesis_chunk_extra(state_root: &StateRoot, gas_limit: Gas) -> ChunkExtra { + ChunkExtra::new(state_root, CryptoHash::default(), vec![], 0, gas_limit, 0) + } + + pub fn genesis_chunk_extra(&self, shard_id: ShardId) -> Result { + let shard_index = shard_id as usize; + let state_root = *get_genesis_state_roots(self.chain_store.store())? + .ok_or_else(|| Error::Other("genesis state roots do not exist in the db".to_owned()))? + .get(shard_index) + .ok_or_else(|| { + Error::Other(format!("genesis state root does not exist for shard {shard_index}")) + })?; + let gas_limit = self + .genesis + .chunks() + .get(shard_index) + .ok_or_else(|| { + Error::Other(format!("genesis chunk does not exist for shard {shard_index}")) + })? + .gas_limit(); + Ok(Self::create_genesis_chunk_extra(&state_root, gas_limit)) + } + /// Creates a light client block for the last final block from perspective of some other block /// /// # Arguments @@ -3257,11 +3273,9 @@ impl Chain { // Chunk validation not enabled yet. return Ok(false); } - let mut all_chunk_producers = self.epoch_manager.get_epoch_chunk_producers(epoch_id)?; - all_chunk_producers - .extend(self.epoch_manager.get_epoch_chunk_producers(&next_epoch_id)?.into_iter()); - let mut chunk_producer_accounts = all_chunk_producers.iter().map(|v| v.account_id()); - Ok(me.as_ref().map_or(false, |a| chunk_producer_accounts.contains(a))) + let Some(account_id) = me.as_ref() else { return Ok(false) }; + Ok(self.epoch_manager.is_chunk_producer_for_epoch(epoch_id, account_id)? + || self.epoch_manager.is_chunk_producer_for_epoch(&next_epoch_id, account_id)?) } /// Creates jobs which will update shards for the given block and incoming diff --git a/chain/chain/src/test_utils/kv_runtime.rs b/chain/chain/src/test_utils/kv_runtime.rs index 640e873edf6..8487838d243 100644 --- a/chain/chain/src/test_utils/kv_runtime.rs +++ b/chain/chain/src/test_utils/kv_runtime.rs @@ -696,6 +696,18 @@ impl EpochManagerAdapter for MockEpochManager { Ok(vec![]) } + /// We need to override the default implementation to make + /// `Chain::should_produce_state_witness_for_this_or_next_epoch` work + /// since `get_epoch_chunk_producers` returns empty Vec which results + /// in state transition data not being saved on disk. + fn is_chunk_producer_for_epoch( + &self, + _epoch_id: &EpochId, + _account_id: &AccountId, + ) -> Result { + Ok(true) + } + fn get_block_producer( &self, epoch_id: &EpochId, @@ -1054,7 +1066,7 @@ impl RuntimeAdapter for KeyValueRuntime { fn prepare_transactions( &self, - _storage: RuntimeStorageConfig, + storage: RuntimeStorageConfig, _chunk: PrepareTransactionsChunkContext, _prev_block: PrepareTransactionsBlockContext, transaction_groups: &mut dyn TransactionGroupIterator, @@ -1065,7 +1077,11 @@ impl RuntimeAdapter for KeyValueRuntime { while let Some(iter) = transaction_groups.next() { res.push(iter.next().unwrap()); } - Ok(PreparedTransactions { transactions: res, limited_by: None, storage_proof: None }) + Ok(PreparedTransactions { + transactions: res, + limited_by: None, + storage_proof: if storage.record_storage { Some(Default::default()) } else { None }, + }) } fn apply_chunk( @@ -1076,7 +1092,6 @@ impl RuntimeAdapter for KeyValueRuntime { receipts: &[Receipt], transactions: &[SignedTransaction], ) -> Result { - assert!(!storage_config.record_storage); let mut tx_results = vec![]; let shard_id = chunk.shard_id; @@ -1224,7 +1239,7 @@ impl RuntimeAdapter for KeyValueRuntime { validator_proposals: vec![], total_gas_burnt: 0, total_balance_burnt: 0, - proof: None, + proof: if storage_config.record_storage { Some(Default::default()) } else { None }, processed_delayed_receipts: vec![], applied_receipts_hash: hash(&borsh::to_vec(receipts).unwrap()), }) diff --git a/chain/client/Cargo.toml b/chain/client/Cargo.toml index abe33fdcf90..de33b311637 100644 --- a/chain/client/Cargo.toml +++ b/chain/client/Cargo.toml @@ -118,3 +118,6 @@ sandbox = [ new_epoch_sync = [ "near-chain/new_epoch_sync" ] +statelessnet_protocol = [ + "near-chain/statelessnet_protocol", +] diff --git a/chain/client/src/client.rs b/chain/client/src/client.rs index 3808a1d38c3..183d5ad9682 100644 --- a/chain/client/src/client.rs +++ b/chain/client/src/client.rs @@ -849,15 +849,11 @@ impl Client { let prepared_transactions = self.prepare_transactions(shard_uid, prev_block_hash, chunk_extra.as_ref())?; #[cfg(feature = "test_features")] - let prepared_transactions = PreparedTransactions { - transactions: Self::maybe_insert_invalid_transaction( - prepared_transactions.transactions, - prev_block_hash, - self.produce_invalid_tx_in_chunks, - ), - limited_by: prepared_transactions.limited_by, - storage_proof: prepared_transactions.storage_proof, - }; + let prepared_transactions = Self::maybe_insert_invalid_transaction( + prepared_transactions, + prev_block_hash, + self.produce_invalid_tx_in_chunks, + ); let num_filtered_transactions = prepared_transactions.transactions.len(); let (tx_root, _) = merklize(&prepared_transactions.transactions); let outgoing_receipts = self.chain.get_outgoing_receipts_for_shard( @@ -945,12 +941,12 @@ impl Client { #[cfg(feature = "test_features")] fn maybe_insert_invalid_transaction( - mut txs: Vec, + mut txs: PreparedTransactions, prev_block_hash: CryptoHash, insert: bool, - ) -> Vec { + ) -> PreparedTransactions { if insert { - txs.push(SignedTransaction::new( + txs.transactions.push(SignedTransaction::new( near_crypto::Signature::empty(near_crypto::KeyType::ED25519), near_primitives::transaction::Transaction::new( "test".parse().unwrap(), @@ -960,6 +956,9 @@ impl Client { prev_block_hash, ), )); + if txs.storage_proof.is_none() { + txs.storage_proof = Some(Default::default()); + } } txs } diff --git a/chain/client/src/stateless_validation/chunk_validator.rs b/chain/client/src/stateless_validation/chunk_validator.rs index 46b184558f0..a92a3557af0 100644 --- a/chain/client/src/stateless_validation/chunk_validator.rs +++ b/chain/client/src/stateless_validation/chunk_validator.rs @@ -210,6 +210,7 @@ pub(crate) fn pre_validate_chunk_state_witness( ))); }; let is_new_chunk = chunk.is_new_chunk(block.header().height()); + let is_genesis = block.header().is_genesis(); block_hash = *block.header().prev_hash(); if is_new_chunk { prev_chunks_seen += 1; @@ -219,7 +220,7 @@ pub(crate) fn pre_validate_chunk_state_witness( } else if prev_chunks_seen == 1 { blocks_after_last_last_chunk.push(block); } - if prev_chunks_seen == 2 { + if prev_chunks_seen == 2 || is_genesis { break; } } @@ -288,8 +289,14 @@ pub(crate) fn pre_validate_chunk_state_witness( }; } - Ok(PreValidationOutput { - main_transition_params: NewChunkData { + let main_transition_params = if last_chunk_block.header().is_genesis() { + MainTransition::Genesis { + chunk_extra: chain.genesis_chunk_extra(shard_id)?, + block_hash: *last_chunk_block.hash(), + shard_id, + } + } else { + MainTransition::NewChunk(NewChunkData { chunk_header: last_chunk_block.chunks().get(shard_id as usize).unwrap().clone(), transactions: state_witness.transactions.clone(), receipts: receipts_to_apply, @@ -308,7 +315,11 @@ pub(crate) fn pre_validate_chunk_state_witness( state_patch: Default::default(), record_storage: false, }, - }, + }) + }; + + Ok(PreValidationOutput { + main_transition_params, implicit_transition_params: blocks_after_last_chunk .into_iter() .rev() @@ -335,6 +346,21 @@ fn validate_source_receipt_proofs( receipt_source_blocks: &[Block], target_chunk_shard_id: ShardId, ) -> Result, Error> { + if receipt_source_blocks.iter().any(|block| block.header().is_genesis()) { + if receipt_source_blocks.len() != 1 { + return Err(Error::Other( + "Invalid chain state: receipt_source_blocks should not have any blocks alongside genesis".to_owned() + )); + } + if !source_receipt_proofs.is_empty() { + return Err(Error::InvalidChunkStateWitness(format!( + "genesis source_receipt_proofs should be empty, actual len is {}", + source_receipt_proofs.len() + ))); + } + return Ok(vec![]); + } + let mut receipts_to_apply = Vec::new(); let mut expected_proofs_len = 0; @@ -415,8 +441,29 @@ fn validate_receipt_proof( Ok(()) } +enum MainTransition { + Genesis { chunk_extra: ChunkExtra, block_hash: CryptoHash, shard_id: ShardId }, + NewChunk(NewChunkData), +} + +impl MainTransition { + fn block_hash(&self) -> CryptoHash { + match self { + Self::Genesis { block_hash, .. } => *block_hash, + Self::NewChunk(data) => data.block.block_hash, + } + } + + fn shard_id(&self) -> ShardId { + match self { + Self::Genesis { shard_id, .. } => *shard_id, + Self::NewChunk(data) => data.chunk_header.shard_id(), + } + } +} + pub(crate) struct PreValidationOutput { - main_transition_params: NewChunkData, + main_transition_params: MainTransition, implicit_transition_params: Vec, } @@ -426,31 +473,35 @@ pub(crate) fn validate_chunk_state_witness( epoch_manager: &dyn EpochManagerAdapter, runtime_adapter: &dyn RuntimeAdapter, ) -> Result<(), Error> { - let main_transition = pre_validation_output.main_transition_params; let _timer = metrics::CHUNK_STATE_WITNESS_VALIDATION_TIME - .with_label_values(&[&main_transition.chunk_header.shard_id().to_string()]) + .with_label_values(&[&state_witness.chunk_header.shard_id().to_string()]) .start_timer(); let span = tracing::debug_span!(target: "chain", "validate_chunk_state_witness").entered(); - let chunk_header = main_transition.chunk_header.clone(); - let epoch_id = epoch_manager.get_epoch_id(&main_transition.block.block_hash)?; - let shard_uid = - epoch_manager.shard_id_to_uid(main_transition.chunk_header.shard_id(), &epoch_id)?; - // Should we validate other fields? - let NewChunkResult { apply_result: mut main_apply_result, .. } = apply_new_chunk( - &span, - main_transition, - ShardContext { - shard_uid, - cares_about_shard_this_epoch: true, - will_shard_layout_change: false, - should_apply_chunk: true, - need_to_reshard: false, - }, - runtime_adapter, - epoch_manager, - )?; - let outgoing_receipts = std::mem::take(&mut main_apply_result.outgoing_receipts); - let mut chunk_extra = apply_result_to_chunk_extra(main_apply_result, &chunk_header); + let block_hash = pre_validation_output.main_transition_params.block_hash(); + let epoch_id = epoch_manager.get_epoch_id(&block_hash)?; + let shard_uid = epoch_manager + .shard_id_to_uid(pre_validation_output.main_transition_params.shard_id(), &epoch_id)?; + let (mut chunk_extra, outgoing_receipts) = match pre_validation_output.main_transition_params { + MainTransition::Genesis { chunk_extra, .. } => (chunk_extra, vec![]), + MainTransition::NewChunk(new_chunk_data) => { + let chunk_header = new_chunk_data.chunk_header.clone(); + let NewChunkResult { apply_result: mut main_apply_result, .. } = apply_new_chunk( + &span, + new_chunk_data, + ShardContext { + shard_uid, + cares_about_shard_this_epoch: true, + will_shard_layout_change: false, + should_apply_chunk: true, + need_to_reshard: false, + }, + runtime_adapter, + epoch_manager, + )?; + let outgoing_receipts = std::mem::take(&mut main_apply_result.outgoing_receipts); + (apply_result_to_chunk_extra(main_apply_result, &chunk_header), outgoing_receipts) + } + }; if chunk_extra.state_root() != &state_witness.main_state_transition.post_state_root { // This is an early check, it's not for correctness, only for better // error reporting in case of an invalid state witness due to a bug. @@ -589,34 +640,9 @@ impl Client { peer_id: PeerId, processing_done_tracker: Option, ) -> Result, Error> { - // TODO(#10502): Handle production of state witness for first chunk after genesis. - // Properly handle case for chunk right after genesis. - // Context: We are currently unable to handle production of the state witness for the - // first chunk after genesis as it's not possible to run the genesis chunk in runtime. let prev_block_hash = witness.inner.chunk_header.prev_block_hash(); - let prev_block = match self.chain.get_block(prev_block_hash) { - Ok(block) => block, - Err(_) => { - return Ok(Some(witness)); - } - }; - let prev_chunk_header = Chain::get_prev_chunk_header( - self.epoch_manager.as_ref(), - &prev_block, - witness.inner.chunk_header.shard_id(), - )?; - if prev_chunk_header.prev_block_hash() == &CryptoHash::default() { - let Some(signer) = self.validator_signer.as_ref() else { - return Err(Error::NotAChunkValidator); - }; - send_chunk_endorsement_to_block_producers( - &witness.inner.chunk_header, - self.epoch_manager.as_ref(), - signer.as_ref(), - &self.chunk_validator.network_sender, - self.chunk_endorsement_tracker.as_ref(), - ); - return Ok(None); + if self.chain.get_block(prev_block_hash).is_err() { + return Ok(Some(witness)); } // TODO(#10265): If the previous block does not exist, we should diff --git a/chain/client/src/stateless_validation/state_witness_producer.rs b/chain/client/src/stateless_validation/state_witness_producer.rs index 1110e1cbd8b..4e67687b8f6 100644 --- a/chain/client/src/stateless_validation/state_witness_producer.rs +++ b/chain/client/src/stateless_validation/state_witness_producer.rs @@ -5,7 +5,8 @@ use near_chain_primitives::Error; use near_network::types::{NetworkRequests, PeerManagerMessageRequest}; use near_primitives::challenge::PartialState; use near_primitives::checked_feature; -use near_primitives::hash::CryptoHash; +use near_primitives::hash::{hash, CryptoHash}; +use near_primitives::receipt::Receipt; use near_primitives::sharding::{ChunkHash, ReceiptProof, ShardChunk, ShardChunkHeader}; use near_primitives::stateless_validation::{ ChunkStateTransition, ChunkStateWitness, ChunkStateWitnessInner, StoredChunkStateTransitionData, @@ -43,10 +44,7 @@ impl Client { .ordered_chunk_validators(); let my_signer = self.validator_signer.as_ref().ok_or(Error::NotAValidator)?.clone(); - // TODO(#10502): Handle production of state witness for first chunk after genesis. - let witness = if prev_chunk_header.prev_block_hash() == &CryptoHash::default() { - ChunkStateWitness::empty(chunk.cloned_header()) - } else { + let witness = { let witness_inner = self.create_state_witness_inner( prev_block_header, prev_chunk_header, @@ -131,12 +129,12 @@ impl Client { chunk_header: &ShardChunkHeader, prev_chunk_header: &ShardChunkHeader, ) -> Result<(ChunkStateTransition, Vec, CryptoHash), Error> { + let store = self.chain.chain_store().store(); let shard_id = chunk_header.shard_id(); let epoch_id = self.epoch_manager.get_epoch_id_from_prev_block(chunk_header.prev_block_hash())?; let shard_uid = self.epoch_manager.shard_id_to_uid(shard_id, &epoch_id)?; let prev_chunk_height_included = prev_chunk_header.height_included(); - let mut prev_blocks = self.chain.get_blocks_until_height( *chunk_header.prev_block_hash(), prev_chunk_height_included, @@ -144,20 +142,25 @@ impl Client { )?; prev_blocks.reverse(); let (main_block, implicit_blocks) = prev_blocks.split_first().unwrap(); - let store = self.chain.chain_store().store(); - let StoredChunkStateTransitionData { base_state, receipts_hash } = store - .get_ser( - near_store::DBCol::StateTransitionData, - &near_primitives::utils::get_block_shard_id(main_block, shard_id), - )? - .ok_or(Error::Other(format!( - "Missing state proof for block {main_block} and shard {shard_id}" - )))?; + let (base_state, receipts_hash) = if prev_chunk_header.is_genesis() { + (Default::default(), hash(&borsh::to_vec::<[Receipt]>(&[]).unwrap())) + } else { + let StoredChunkStateTransitionData { base_state, receipts_hash } = store + .get_ser( + near_store::DBCol::StateTransitionData, + &near_primitives::utils::get_block_shard_id(main_block, shard_id), + )? + .ok_or(Error::Other(format!( + "Missing main transition state proof for block {main_block} and shard {shard_id}" + )))?; + (base_state, receipts_hash) + }; let main_transition = ChunkStateTransition { block_hash: *main_block, base_state, post_state_root: *self.chain.get_chunk_extra(main_block, &shard_uid)?.state_root(), }; + let mut implicit_transitions = vec![]; for block_hash in implicit_blocks { let StoredChunkStateTransitionData { base_state, .. } = store @@ -166,7 +169,7 @@ impl Client { &near_primitives::utils::get_block_shard_id(block_hash, shard_id), )? .ok_or(Error::Other(format!( - "Missing state proof for block {block_hash} and shard {shard_id}" + "Missing implicit transition state proof for block {block_hash} and shard {shard_id}" )))?; implicit_transitions.push(ChunkStateTransition { block_hash: *block_hash, @@ -190,7 +193,7 @@ impl Client { prev_block_header: &BlockHeader, prev_chunk_header: &ShardChunkHeader, ) -> Result, Error> { - if prev_chunk_header.prev_block_hash() == &CryptoHash::default() { + if prev_chunk_header.is_genesis() { // State witness which proves the execution of the first chunk in the blockchain // doesn't have any source receipts. return Ok(HashMap::new()); diff --git a/chain/client/src/view_client.rs b/chain/client/src/view_client.rs index 110b3dc5535..ef8679f352a 100644 --- a/chain/client/src/view_client.rs +++ b/chain/client/src/view_client.rs @@ -280,7 +280,7 @@ impl ViewClientActor { let cps: Vec = shard_ids .iter() .map(|&shard_id| { - let cp = epoch_info.sample_chunk_producer(block_height, shard_id); + let cp = epoch_info.sample_chunk_producer(block_height, shard_id).unwrap(); let cp = epoch_info.get_validator(cp).account_id().clone(); cp }) diff --git a/chain/epoch-manager/src/adapter.rs b/chain/epoch-manager/src/adapter.rs index ef550f041b8..629db77fdaf 100644 --- a/chain/epoch-manager/src/adapter.rs +++ b/chain/epoch-manager/src/adapter.rs @@ -292,6 +292,14 @@ pub trait EpochManagerAdapter: Send + Sync { ))) } + fn is_chunk_producer_for_epoch( + &self, + epoch_id: &EpochId, + account_id: &AccountId, + ) -> Result { + Ok(self.get_epoch_chunk_producers(epoch_id)?.iter().any(|v| v.account_id() == account_id)) + } + /// Epoch Manager init procedure that is necessary after Epoch Sync. fn epoch_sync_init_epoch_manager( &self, diff --git a/chain/epoch-manager/src/lib.rs b/chain/epoch-manager/src/lib.rs index 8404abd48b1..2200fb90374 100644 --- a/chain/epoch-manager/src/lib.rs +++ b/chain/epoch-manager/src/lib.rs @@ -1029,7 +1029,7 @@ impl EpochManager { shard_id: ShardId, ) -> Result { let epoch_info = self.get_epoch_info(epoch_id)?; - let validator_id = Self::chunk_producer_from_info(&epoch_info, height, shard_id); + let validator_id = Self::chunk_producer_from_info(&epoch_info, height, shard_id)?; Ok(epoch_info.get_validator(validator_id)) } @@ -1556,8 +1556,12 @@ impl EpochManager { epoch_info: &EpochInfo, height: BlockHeight, shard_id: ShardId, - ) -> ValidatorId { - epoch_info.sample_chunk_producer(height, shard_id) + ) -> Result { + epoch_info.sample_chunk_producer(height, shard_id).ok_or_else(|| { + EpochError::ChunkProducerSelectionError(format!( + "Invalid shard {shard_id} for height {height}" + )) + }) } /// Returns true, if given current block info, next block supposed to be in the next epoch. diff --git a/chain/epoch-manager/src/tests/mod.rs b/chain/epoch-manager/src/tests/mod.rs index 2083295c3ae..e403a728cd7 100644 --- a/chain/epoch-manager/src/tests/mod.rs +++ b/chain/epoch-manager/src/tests/mod.rs @@ -1131,7 +1131,7 @@ fn test_expected_chunks_prev_block_not_produced() { let prev_block_info = epoch_manager.get_block_info(&prev_block).unwrap(); let prev_height = prev_block_info.height(); let expected_chunk_producer = - EpochManager::chunk_producer_from_info(&epoch_info, prev_height + 1, 0); + EpochManager::chunk_producer_from_info(&epoch_info, prev_height + 1, 0).unwrap(); // test1 does not produce blocks during first epoch if block_producer == 0 && epoch_id == initial_epoch_id { expected += 1; @@ -1473,7 +1473,8 @@ fn test_chunk_validator_kickout() { &epoch_info, height, shard_id as u64, - ); + ) + .unwrap(); // test1 skips chunks if chunk_producer == 0 { expected += 1; diff --git a/chain/epoch-manager/src/types.rs b/chain/epoch-manager/src/types.rs index d9e40956f10..8ded84fb03f 100644 --- a/chain/epoch-manager/src/types.rs +++ b/chain/epoch-manager/src/types.rs @@ -138,7 +138,8 @@ impl EpochInfoAggregator { epoch_info, prev_block_height + 1, i as ShardId, - ); + ) + .unwrap(); let tracker = self.shard_tracker.entry(i as ShardId).or_insert_with(HashMap::new); tracker .entry(chunk_validator_id) diff --git a/chain/epoch-manager/src/validator_selection.rs b/chain/epoch-manager/src/validator_selection.rs index d71831eecc9..3d723654440 100644 --- a/chain/epoch-manager/src/validator_selection.rs +++ b/chain/epoch-manager/src/validator_selection.rs @@ -606,7 +606,7 @@ mod tests { let cp = epoch_info.sample_chunk_producer(h, shard_id); // Don't read too much into this. The reason the ValidatorId always // equals the ShardId is because the validators are assigned to shards in order. - assert_eq!(cp, shard_id) + assert_eq!(cp, Some(shard_id)) } } @@ -632,7 +632,7 @@ mod tests { for shard_id in 0..num_shards { let mut counts: [i32; 2] = [0, 0]; for h in 0..100_000 { - let cp = epoch_info.sample_chunk_producer(h, shard_id); + let cp = epoch_info.sample_chunk_producer(h, shard_id).unwrap(); // if ValidatorId is in the second half then it is the lower // stake validator (because they are sorted by decreasing stake). let index = if cp >= num_shards { 1 } else { 0 }; diff --git a/core/primitives/src/block_header.rs b/core/primitives/src/block_header.rs index c5a2837e9dd..37355151900 100644 --- a/core/primitives/src/block_header.rs +++ b/core/primitives/src/block_header.rs @@ -745,6 +745,11 @@ impl BlockHeader { } } + #[inline] + pub fn is_genesis(&self) -> bool { + self.prev_hash() == &CryptoHash::default() + } + #[inline] pub fn hash(&self) -> &CryptoHash { match self { diff --git a/core/primitives/src/epoch_manager.rs b/core/primitives/src/epoch_manager.rs index 0062c4aa58a..1a6c6a842b5 100644 --- a/core/primitives/src/epoch_manager.rs +++ b/core/primitives/src/epoch_manager.rs @@ -1081,33 +1081,37 @@ pub mod epoch_info { } } - pub fn sample_chunk_producer(&self, height: BlockHeight, shard_id: ShardId) -> ValidatorId { + pub fn sample_chunk_producer( + &self, + height: BlockHeight, + shard_id: ShardId, + ) -> Option { match &self { Self::V1(v1) => { let cp_settlement = &v1.chunk_producers_settlement; - let shard_cps = &cp_settlement[shard_id as usize]; - shard_cps[(height as u64 % (shard_cps.len() as u64)) as usize] + let shard_cps = cp_settlement.get(shard_id as usize)?; + shard_cps.get((height as u64 % (shard_cps.len() as u64)) as usize).copied() } Self::V2(v2) => { let cp_settlement = &v2.chunk_producers_settlement; - let shard_cps = &cp_settlement[shard_id as usize]; - shard_cps[(height as u64 % (shard_cps.len() as u64)) as usize] + let shard_cps = cp_settlement.get(shard_id as usize)?; + shard_cps.get((height as u64 % (shard_cps.len() as u64)) as usize).copied() } Self::V3(v3) => { let protocol_version = self.protocol_version(); let seed = Self::chunk_produce_seed(protocol_version, &v3.rng_seed, height, shard_id); let shard_id = shard_id as usize; - let sample = v3.chunk_producers_sampler[shard_id].sample(seed); - v3.chunk_producers_settlement[shard_id][sample] + let sample = v3.chunk_producers_sampler.get(shard_id)?.sample(seed); + v3.chunk_producers_settlement.get(shard_id)?.get(sample).copied() } Self::V4(v4) => { let protocol_version = self.protocol_version(); let seed = Self::chunk_produce_seed(protocol_version, &v4.rng_seed, height, shard_id); let shard_id = shard_id as usize; - let sample = v4.chunk_producers_sampler[shard_id].sample(seed); - v4.chunk_producers_settlement[shard_id][sample] + let sample = v4.chunk_producers_sampler.get(shard_id)?.sample(seed); + v4.chunk_producers_settlement.get(shard_id)?.get(sample).copied() } } } diff --git a/core/primitives/src/errors.rs b/core/primitives/src/errors.rs index 36c9d82e02a..aab6214220d 100644 --- a/core/primitives/src/errors.rs +++ b/core/primitives/src/errors.rs @@ -865,6 +865,8 @@ pub enum EpochError { }, /// Error selecting validators for a chunk. ChunkValidatorSelectionError(String), + /// Error selecting chunk producer for a shard. + ChunkProducerSelectionError(String), } impl std::error::Error for EpochError {} @@ -892,6 +894,9 @@ impl Display for EpochError { EpochError::ChunkValidatorSelectionError(err) => { write!(f, "Error selecting validators for a chunk: {}", err) } + EpochError::ChunkProducerSelectionError(err) => { + write!(f, "Error selecting chunk producer: {}", err) + } } } } @@ -915,6 +920,9 @@ impl Debug for EpochError { EpochError::ChunkValidatorSelectionError(err) => { write!(f, "ChunkValidatorSelectionError({})", err) } + EpochError::ChunkProducerSelectionError(err) => { + write!(f, "ChunkProducerSelectionError({})", err) + } } } } diff --git a/core/primitives/src/sharding.rs b/core/primitives/src/sharding.rs index 06decfd1032..2341ea24fe2 100644 --- a/core/primitives/src/sharding.rs +++ b/core/primitives/src/sharding.rs @@ -310,6 +310,11 @@ impl ShardChunkHeader { } } + #[inline] + pub fn is_genesis(&self) -> bool { + self.prev_block_hash() == &CryptoHash::default() + } + #[inline] pub fn encoded_merkle_root(&self) -> CryptoHash { match self { diff --git a/core/primitives/src/stateless_validation.rs b/core/primitives/src/stateless_validation.rs index 12e342854bd..bd6f6a23ff3 100644 --- a/core/primitives/src/stateless_validation.rs +++ b/core/primitives/src/stateless_validation.rs @@ -118,24 +118,6 @@ impl ChunkStateWitnessInner { } } -impl ChunkStateWitness { - // TODO(#10502): To be used only for creating state witness when previous chunk is genesis. - // Clean this up once we can properly handle creating state witness for genesis chunk. - pub fn empty(chunk_header: ShardChunkHeader) -> Self { - let inner = ChunkStateWitnessInner::new( - chunk_header, - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - ); - ChunkStateWitness { inner, signature: Signature::default() } - } -} - /// Represents the base state and the expected post-state-root of a chunk's state /// transition. The actual state transition itself is not included here. #[derive(Debug, Default, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] diff --git a/integration-tests/src/tests/client/features/adversarial_behaviors.rs b/integration-tests/src/tests/client/features/adversarial_behaviors.rs index 7fc96e0659a..96442b736b1 100644 --- a/integration-tests/src/tests/client/features/adversarial_behaviors.rs +++ b/integration-tests/src/tests/client/features/adversarial_behaviors.rs @@ -258,24 +258,16 @@ fn test_banning_chunk_producer_when_seeing_invalid_chunk_base( // This is the first block with invalid chunks in the current epoch. // In pre-stateless validation protocol the first block with invalid chunks - // was skipped, but stateless validation is able to deal with invalid chunks - // without skipping blocks. The expected behavior depends on whether we are - // using stateless validation or not. - if uses_stateless_validation { - // With stateless validation the block usually isn't skipped. Chunk validators - // won't send chunk endorsements for this chunk, which means that it won't be - // included in the block at all. The only exception is the first few blocks after - // genesis, which are currently handled in a special way. - // In this test the block with height 2 is skipped. - // TODO(#10502): Properly handle blocks right after genesis, ideally no blocks - // would be skipped when using stateless validation. - this_block_should_be_skipped = height < 3; - } else { - // In the old protocol, chunks are first included in the block and then the block - // is validated. This means that this block, which includes an invalid chunk, - // will be invalid and it should be skipped. Once this happens, the malicious - // chunk producer is banned for the whole epoch and no blocks are skipped until - // we reach the next epoch. + // was skipped. + // In the old protocol, chunks are first included in the block and then the block + // is validated. This means that this block, which includes an invalid chunk, + // will be invalid and it should be skipped. Once this happens, the malicious + // chunk producer is banned for the whole epoch and no blocks are skipped until + // we reach the next epoch. + // With stateless validation the block usually isn't skipped. Chunk validators + // won't send chunk endorsements for this chunk, which means that it won't be + // included in the block at all. + if !uses_stateless_validation { this_block_should_be_skipped = true; } } diff --git a/integration-tests/src/tests/client/features/stateless_validation.rs b/integration-tests/src/tests/client/features/stateless_validation.rs index feea26dbf62..2946c19b0bb 100644 --- a/integration-tests/src/tests/client/features/stateless_validation.rs +++ b/integration-tests/src/tests/client/features/stateless_validation.rs @@ -1,7 +1,7 @@ use near_epoch_manager::{EpochManager, EpochManagerAdapter}; use near_primitives::network::PeerId; use near_primitives::sharding::{ShardChunkHeader, ShardChunkHeaderV3}; -use near_primitives::stateless_validation::ChunkStateWitness; +use near_primitives::stateless_validation::{ChunkStateWitness, ChunkStateWitnessInner}; use near_primitives::validator_signer::EmptyValidatorSigner; use near_store::test_utils::create_test_store; use nearcore::config::GenesisExt; @@ -347,7 +347,7 @@ fn test_chunk_state_witness_bad_shard_id() { let previous_block = env.clients[0].chain.head().unwrap().prev_block_hash; let invalid_shard_id = 1000000000; - let shard_header = ShardChunkHeader::V3(ShardChunkHeaderV3::new( + let chunk_header = ShardChunkHeader::V3(ShardChunkHeaderV3::new( previous_block, Default::default(), Default::default(), @@ -363,7 +363,19 @@ fn test_chunk_state_witness_bad_shard_id() { Default::default(), &EmptyValidatorSigner::default(), )); - let witness = ChunkStateWitness::empty(shard_header); + let witness = ChunkStateWitness { + inner: ChunkStateWitnessInner::new( + chunk_header, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ), + signature: Default::default(), + }; // Client should reject this ChunkStateWitness and the error message should mention "shard" tracing::info!(target: "test", "Processing invalid ChunkStateWitness"); diff --git a/tools/state-viewer/src/epoch_info.rs b/tools/state-viewer/src/epoch_info.rs index ca2c083009b..35089c0a5a3 100644 --- a/tools/state-viewer/src/epoch_info.rs +++ b/tools/state-viewer/src/epoch_info.rs @@ -96,7 +96,7 @@ fn display_block_and_chunk_producers( let cps: Vec = shard_ids .iter() .map(|&shard_id| { - let cp = epoch_info.sample_chunk_producer(block_height, shard_id); + let cp = epoch_info.sample_chunk_producer(block_height, shard_id).unwrap(); let cp = epoch_info.get_validator(cp).account_id().clone(); cp.as_str().to_string() }) @@ -280,7 +280,8 @@ fn display_validator_info( .iter() .map(|&shard_id| (block_height, shard_id)) .filter(|&(block_height, shard_id)| { - epoch_info.sample_chunk_producer(block_height, shard_id) == *validator_id + epoch_info.sample_chunk_producer(block_height, shard_id) + == Some(*validator_id) }) .collect::>() })