Skip to content

Commit

Permalink
feat(epoch-sync): calculate legacy epoch_sync_data_hash from EpochSyn…
Browse files Browse the repository at this point in the history
…cInfo (#10120)

First part of #10031
Adding support for `EpochInfo` validation through `epoch_sync_data_hash`
in new epoch sync.

Included in PR:
- reconstruction of `BlockInfo` headers
- calculation of `epoch_sync_data_hash` from `EpochSyncInfo`
- bad attempt at good errors in all of that
- test for both `BlockInfo` and `epoch_sync_data_hash` reconstruction

Coming next: epoch sync testing tool, specifically command to test that
reconstructed `epoch_sync_data_hash` matches recorded
`epoch_sync_data_hash` on all real data in testnet/mainnet.
  • Loading branch information
posvyatokum authored Nov 8, 2023
1 parent 76a8e59 commit d8ada79
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions chain/chain/src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6007,6 +6007,10 @@ impl<'a> ChainUpdate<'a> {
}
}

// We don't need to save `next_epoch_first_hash` during `EpochSyncInfo` processing.
// It is only needed for validation.
headers_to_save.remove(next_epoch_first_hash);

Ok((headers, headers_to_save))
}

Expand Down
3 changes: 3 additions & 0 deletions core/primitives/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ tracing.workspace = true

near-crypto.workspace = true
near-fmt.workspace = true
near-o11y.workspace = true
near-primitives-core.workspace = true
near-rpc-error-macro.workspace = true
near-vm-runner.workspace = true
Expand All @@ -59,12 +60,14 @@ nightly = [
"protocol_feature_reject_blocks_with_outdated_protocol_version",
"protocol_feature_simple_nightshade_v2",
"near-fmt/nightly",
"near-o11y/nightly",
"near-primitives-core/nightly",
"near-vm-runner/nightly",
]

nightly_protocol = [
"near-fmt/nightly_protocol",
"near-o11y/nightly_protocol",
"near-primitives-core/nightly_protocol",
"near-vm-runner/nightly_protocol",
]
Expand Down
93 changes: 93 additions & 0 deletions core/primitives/src/epoch_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1232,8 +1232,11 @@ pub enum SlashState {
#[cfg(feature = "new_epoch_sync")]
pub mod epoch_sync {
use crate::block_header::BlockHeader;
use crate::epoch_manager::block_info::BlockInfo;
use crate::epoch_manager::epoch_info::EpochInfo;
use crate::errors::epoch_sync::{EpochSyncHashType, EpochSyncInfoError};
use borsh::{BorshDeserialize, BorshSerialize};
use near_o11y::log_assert;
use near_primitives_core::hash::CryptoHash;
use std::collections::{HashMap, HashSet};

Expand All @@ -1258,4 +1261,94 @@ pub mod epoch_sync {
pub next_epoch_info: EpochInfo,
pub next_next_epoch_info: EpochInfo,
}

impl EpochSyncInfo {
/// Reconstruct BlockInfo for `hash` from information in EpochSyncInfo.
pub fn get_block_info(&self, hash: &CryptoHash) -> Result<BlockInfo, EpochSyncInfoError> {
let epoch_height = self.epoch_info.epoch_height();

let epoch_first_hash = self
.all_block_hashes
.first()
.ok_or(EpochSyncInfoError::ShortEpoch { epoch_height })?;
let epoch_first_header =
self.get_header(*epoch_first_hash, EpochSyncHashType::FirstEpochBlock)?;

let header = self.get_header(*hash, EpochSyncHashType::Other)?;

log_assert!(
epoch_first_header.epoch_id() == header.epoch_id(),
"We can only correctly reconstruct headers from this epoch"
);

let last_finalized_height = if *header.last_final_block() == CryptoHash::default() {
0
} else {
let last_finalized_header =
self.get_header(*header.last_final_block(), EpochSyncHashType::LastFinalBlock)?;
last_finalized_header.height()
};
let mut block_info = BlockInfo::new(
*header.hash(),
header.height(),
last_finalized_height,
*header.last_final_block(),
*header.prev_hash(),
header.prev_validator_proposals().collect(),
header.chunk_mask().to_vec(),
vec![],
header.total_supply(),
header.latest_protocol_version(),
header.raw_timestamp(),
);

*block_info.epoch_id_mut() = epoch_first_header.epoch_id().clone();
*block_info.epoch_first_block_mut() = *epoch_first_header.hash();
Ok(block_info)
}

/// Reconstruct legacy `epoch_sync_data_hash` from `EpochSyncInfo`.
/// `epoch_sync_data_hash` was introduced in `BlockHeaderInnerRestV3`.
/// Using this hash we can verify that `EpochInfo` data provided in `EpochSyncInfo` is correct.
pub fn calculate_epoch_sync_data_hash(&self) -> Result<CryptoHash, EpochSyncInfoError> {
let epoch_height = self.epoch_info.epoch_height();

if self.all_block_hashes.len() < 2 {
return Err(EpochSyncInfoError::ShortEpoch { epoch_height });
}
let epoch_first_block = self.all_block_hashes[0];
let epoch_prev_last_block = self.all_block_hashes[self.all_block_hashes.len() - 2];
let epoch_last_block = self.all_block_hashes[self.all_block_hashes.len() - 1];

Ok(CryptoHash::hash_borsh(&(
self.get_block_info(&epoch_first_block)?,
self.get_block_info(&epoch_prev_last_block)?,
self.get_block_info(&epoch_last_block)?,
&self.epoch_info,
&self.next_epoch_info,
&self.next_next_epoch_info,
)))
}

/// Read legacy `epoch_sync_data_hash` from next epoch first header.
/// `epoch_sync_data_hash` was introduced in `BlockHeaderInnerRestV3`.
/// Using this hash we can verify that `EpochInfo` data provided in `EpochSyncInfo` is correct.
pub fn get_epoch_sync_data_hash(&self) -> Result<Option<CryptoHash>, EpochSyncInfoError> {
let next_epoch_first_header =
self.get_header(self.next_epoch_first_hash, EpochSyncHashType::Other)?;
Ok(next_epoch_first_header.epoch_sync_data_hash())
}

fn get_header(
&self,
hash: CryptoHash,
hash_type: EpochSyncHashType,
) -> Result<&BlockHeader, EpochSyncInfoError> {
self.headers.get(&hash).ok_or(EpochSyncInfoError::HashNotFound {
hash,
hash_type,
epoch_height: self.epoch_info.epoch_height(),
})
}
}
}
23 changes: 23 additions & 0 deletions core/primitives/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1205,3 +1205,26 @@ impl From<near_vm_runner::logic::errors::FunctionCallError> for FunctionCallErro
}
}
}

#[cfg(feature = "new_epoch_sync")]
pub mod epoch_sync {
use near_primitives_core::hash::CryptoHash;
use near_primitives_core::types::EpochHeight;
use std::fmt::Debug;

#[derive(Eq, PartialEq, Clone, strum::Display, Debug)]
pub enum EpochSyncHashType {
LastFinalBlock,
FirstEpochBlock,
NextEpochFirstBlock,
Other,
}

#[derive(Eq, PartialEq, Clone, thiserror::Error, Debug)]
pub enum EpochSyncInfoError {
#[error("{hash_type} hash {hash:?} not a part of EpochSyncInfo for epoch {epoch_height}")]
HashNotFound { hash: CryptoHash, hash_type: EpochSyncHashType, epoch_height: EpochHeight },
#[error("all_block_hashes.len() < 2 for epoch {epoch_height}")]
ShortEpoch { epoch_height: EpochHeight },
}
}
72 changes: 72 additions & 0 deletions integration-tests/src/tests/client/epoch_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use near_crypto::{InMemorySigner, KeyType};
use near_network::test_utils::WaitOrTimeoutActor;
use near_o11y::testonly::{init_integration_logger, init_test_logger};
use near_o11y::WithSpanContextExt;
use near_primitives::epoch_manager::block_info::BlockInfo;
use near_primitives::epoch_manager::epoch_sync::EpochSyncInfo;
use near_primitives::test_utils::create_test_signer;
use near_primitives::transaction::{
Expand Down Expand Up @@ -234,3 +235,74 @@ fn test_continuous_epoch_sync_info_population_on_header_sync() {
}
});
}

/// Check that we can reconstruct `BlockInfo` and `epoch_sync_data_hash` from `EpochSyncInfo`.
#[test]
fn test_epoch_sync_data_hash_from_epoch_sync_info() {
init_test_logger();

let epoch_length = 5;
let max_height = epoch_length * 4 + 3;

let mut genesis = Genesis::test(vec!["test0".parse().unwrap(), "test1".parse().unwrap()], 1);

genesis.config.epoch_length = epoch_length;
let mut chain_genesis = ChainGenesis::test();
chain_genesis.epoch_length = epoch_length;
let mut env = TestEnv::builder(chain_genesis)
.real_epoch_managers(&genesis.config)
.nightshade_runtimes(&genesis)
.build();

let mut last_hash = *env.clients[0].chain.genesis().hash();
let mut last_epoch_id = EpochId::default();

for h in 1..max_height {
for tx in generate_transactions(&last_hash, h) {
assert_eq!(env.clients[0].process_tx(tx, false, false), ProcessTxResponse::ValidTx);
}

let block = env.clients[0].produce_block(h).unwrap().unwrap();
env.process_block(0, block.clone(), Provenance::PRODUCED);
last_hash = *block.hash();

let last_final_hash = block.header().last_final_block();
if *last_final_hash == CryptoHash::default() {
continue;
}
let last_final_header =
env.clients[0].chain.store().get_block_header(last_final_hash).unwrap();

if *last_final_header.epoch_id() != last_epoch_id {
let epoch_id = last_epoch_id.clone();

let epoch_sync_info =
env.clients[0].chain.store().get_epoch_sync_info(&epoch_id).unwrap();

tracing::debug!("Checking epoch sync info: {:?}", &epoch_sync_info);

// Check that all BlockInfos needed for new epoch sync can be reconstructed.
// This also helps with debugging if `epoch_sync_data_hash` doesn't match.
for hash in &epoch_sync_info.headers_to_save {
let block_info = env.clients[0]
.chain
.store()
.store()
.get_ser::<BlockInfo>(DBCol::BlockInfo, hash.as_ref())
.unwrap()
.unwrap();
let reconstructed_block_info = epoch_sync_info.get_block_info(hash).unwrap();
assert_eq!(block_info, reconstructed_block_info);
}

assert_eq!(
epoch_sync_info.calculate_epoch_sync_data_hash().unwrap(),
epoch_sync_info.get_epoch_sync_data_hash().unwrap().unwrap(),
);

tracing::debug!("OK");
}

last_epoch_id = last_final_header.epoch_id().clone();
}
}

0 comments on commit d8ada79

Please sign in to comment.