From 57a05df62cdc2fd493a49d125d3a85d3dfa5c526 Mon Sep 17 00:00:00 2001 From: Saketh Are Date: Fri, 20 Sep 2024 09:25:14 -0400 Subject: [PATCH 01/49] feat(state-sync): enable EveryEpoch snapshot generation by default (#12112) In preparation for decentralizing state sync and deprecating cloud storage, we want to have all nodes produce snapshots so that they can share state parts with each other. --- core/store/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/store/src/config.rs b/core/store/src/config.rs index 38e551d58cb..f7c155ab32d 100644 --- a/core/store/src/config.rs +++ b/core/store/src/config.rs @@ -112,10 +112,10 @@ pub struct StateSnapshotConfig { pub enum StateSnapshotType { /// Consider this as the default "disabled" option. We need to have snapshotting enabled for resharding /// State snapshots involve filesystem operations and costly IO operations. - #[default] ForReshardingOnly, /// This is the "enabled" option where we create a snapshot at the beginning of every epoch. /// Needed if a node wants to be able to respond to state part requests. + #[default] EveryEpoch, } From 08065ffaabd14adc262dcabf1aafccfda5e22612 Mon Sep 17 00:00:00 2001 From: Aleksandr Logunov Date: Fri, 20 Sep 2024 17:40:20 +0400 Subject: [PATCH 02/49] feat: lifetimes in ProtocolSchema types (#12120) Remove lifetimes from types generated by ProtocolSchema tooling. Needed for #12031. --- core/primitives/src/sharding.rs | 2 +- .../schema-checker-macro/src/lib.rs | 146 ++++++++++++++---- .../res/protocol_schema.toml | 1 + 3 files changed, 119 insertions(+), 30 deletions(-) diff --git a/core/primitives/src/sharding.rs b/core/primitives/src/sharding.rs index 089635f6aff..a9a1c7fa6e9 100644 --- a/core/primitives/src/sharding.rs +++ b/core/primitives/src/sharding.rs @@ -933,7 +933,7 @@ impl EncodedShardChunkBody { } } -#[derive(BorshSerialize, Debug, Clone)] +#[derive(BorshSerialize, Debug, Clone, ProtocolSchema)] pub struct ReceiptList<'a>(pub ShardId, pub &'a [Receipt]); #[derive(BorshSerialize, BorshDeserialize, ProtocolSchema)] diff --git a/core/schema-checker/schema-checker-macro/src/lib.rs b/core/schema-checker/schema-checker-macro/src/lib.rs index b7a33dd0568..3c3774c0345 100644 --- a/core/schema-checker/schema-checker-macro/src/lib.rs +++ b/core/schema-checker/schema-checker-macro/src/lib.rs @@ -9,14 +9,25 @@ pub fn protocol_schema(input: TokenStream) -> TokenStream { mod helper { use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; - use quote::quote; - use syn::{parse_macro_input, Data, DeriveInput, Fields, FieldsNamed, FieldsUnnamed, Variant}; + use quote::{format_ident, quote}; + use syn::{ + parse_macro_input, Data, DeriveInput, Field, Fields, FieldsNamed, FieldsUnnamed, + GenericArgument, GenericParam, Generics, Index, Path, PathArguments, PathSegment, Type, + TypePath, Variant, + }; pub fn protocol_schema_impl(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; - let info_name = quote::format_ident!("{}_INFO", name); - let type_id = quote! { std::any::TypeId::of::<#name>() }; + let info_name = format_ident!("{}_INFO", name); + let generics = &input.generics; + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + // Create a version of ty_generics without lifetimes for TypeId + let ty_generics_without_lifetimes = remove_lifetimes(generics); + + let type_id = quote! { std::any::TypeId::of::<#name #ty_generics_without_lifetimes>() }; let info = match &input.data { Data::Struct(data_struct) => { let fields = extract_struct_fields(&data_struct.fields); @@ -49,7 +60,7 @@ mod helper { #info_name } - impl near_schema_checker_lib::ProtocolSchema for #name { + impl #impl_generics near_schema_checker_lib::ProtocolSchema for #name #ty_generics #where_clause { fn ensure_registration() {} } }; @@ -95,10 +106,10 @@ mod helper { /// Extracts type ids from the type and **all** its underlying generic /// parameters, recursively. /// For example, for `Vec>` it will return `[Vec, Vec, u32]`. - fn extract_type_ids_from_type(ty: &syn::Type) -> Vec { + fn extract_type_ids_from_type(ty: &Type) -> Vec { let mut result = vec![quote! { std::any::TypeId::of::<#ty>() }]; let type_path = match ty { - syn::Type::Path(type_path) => type_path, + Type::Path(type_path) => type_path, _ => return result, }; @@ -109,7 +120,7 @@ mod helper { // Not urgent because protocol structs are expected to be simple. let generic_params = &type_path.path.segments.last().unwrap().arguments; let params = match generic_params { - syn::PathArguments::AngleBracketed(params) => params, + PathArguments::AngleBracketed(params) => params, _ => return result, }; @@ -117,7 +128,7 @@ mod helper { .args .iter() .map(|arg| { - if let syn::GenericArgument::Type(ty) = arg { + if let GenericArgument::Type(ty) = arg { extract_type_ids_from_type(ty) } else { vec![] @@ -129,44 +140,103 @@ mod helper { result } - fn extract_type_info(ty: &syn::Type) -> TokenStream2 { - let type_path = match ty { - syn::Type::Path(type_path) => type_path, - syn::Type::Array(array) => { + fn extract_type_info(ty: &Type) -> TokenStream2 { + match ty { + Type::Path(type_path) => { + let type_name = &type_path.path.segments.last().unwrap().ident; + let type_without_lifetimes = remove_lifetimes_from_type(type_path); + let type_ids = extract_type_ids_from_type(&type_without_lifetimes); + let type_ids_count = type_ids.len(); + + quote! { + { + const TYPE_IDS_COUNT: usize = #type_ids_count; + const fn create_array() -> [std::any::TypeId; TYPE_IDS_COUNT] { + [#(#type_ids),*] + } + (stringify!(#type_name), &create_array()) + } + } + } + Type::Reference(type_ref) => { + let elem = &type_ref.elem; + extract_type_info(elem) + } + Type::Array(array) => { let elem = &array.elem; let len = &array.len; - return quote! { + quote! { { const fn create_array() -> [std::any::TypeId; 1] { [std::any::TypeId::of::<#elem>()] } (stringify!([#elem; #len]), &create_array()) } - }; + } } _ => { println!("Unsupported type: {:?}", ty); - return quote! { (stringify!(#ty), &[std::any::TypeId::of::<#ty>()]) }; + quote! { (stringify!(#ty), &[std::any::TypeId::of::<#ty>()]) } } - }; + } + } + + fn remove_lifetimes_from_type(type_path: &TypePath) -> Type { + let segments = type_path.path.segments.iter().map(|segment| { + let mut new_segment = + PathSegment { ident: segment.ident.clone(), arguments: PathArguments::None }; - let type_name = quote::format_ident!("{}", type_path.path.segments.last().unwrap().ident); - let type_ids = extract_type_ids_from_type(ty); - let type_ids_count = type_ids.len(); + if let PathArguments::AngleBracketed(args) = &segment.arguments { + let new_args: Vec<_> = args + .args + .iter() + .filter_map(|arg| match arg { + GenericArgument::Type(ty) => { + Some(GenericArgument::Type(remove_lifetimes_from_type_recursive(ty))) + } + GenericArgument::Const(c) => Some(GenericArgument::Const(c.clone())), + _ => None, + }) + .collect(); - quote! { - { - const TYPE_IDS_COUNT: usize = #type_ids_count; - const fn create_array() -> [std::any::TypeId; TYPE_IDS_COUNT] { - [#(#type_ids),*] + if !new_args.is_empty() { + new_segment.arguments = + PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments { + colon2_token: args.colon2_token, + lt_token: args.lt_token, + args: new_args.into_iter().collect(), + gt_token: args.gt_token, + }); } - (stringify!(#type_name), &create_array()) } + + new_segment + }); + + Type::Path(TypePath { + qself: type_path.qself.clone(), + path: Path { + leading_colon: type_path.path.leading_colon, + segments: segments.collect(), + }, + }) + } + + fn remove_lifetimes_from_type_recursive(ty: &Type) -> Type { + match ty { + Type::Path(type_path) => remove_lifetimes_from_type(type_path), + Type::Reference(type_ref) => Type::Reference(syn::TypeReference { + and_token: type_ref.and_token, + lifetime: None, + mutability: type_ref.mutability, + elem: Box::new(remove_lifetimes_from_type_recursive(&type_ref.elem)), + }), + _ => ty.clone(), } } fn extract_from_named_fields( - named: &syn::punctuated::Punctuated, + named: &syn::punctuated::Punctuated, ) -> impl Iterator + '_ { named.iter().map(|f| { let name = &f.ident; @@ -177,15 +247,33 @@ mod helper { } fn extract_from_unnamed_fields( - unnamed: &syn::punctuated::Punctuated, + unnamed: &syn::punctuated::Punctuated, ) -> impl Iterator + '_ { unnamed.iter().enumerate().map(|(i, f)| { - let index = syn::Index::from(i); + let index = Index::from(i); let ty = &f.ty; let type_info = extract_type_info(ty); quote! { (stringify!(#index), #type_info) } }) } + + fn remove_lifetimes(generics: &Generics) -> proc_macro2::TokenStream { + let params: Vec<_> = generics + .params + .iter() + .filter_map(|param| match param { + GenericParam::Type(type_param) => Some(quote! { #type_param }), + GenericParam::Const(const_param) => Some(quote! { #const_param }), + GenericParam::Lifetime(_) => None, + }) + .collect(); + + if !params.is_empty() { + quote! { <#(#params),*> } + } else { + quote! {} + } + } } #[cfg(not(all(enable_const_type_id, feature = "protocol_schema")))] diff --git a/tools/protocol-schema-check/res/protocol_schema.toml b/tools/protocol-schema-check/res/protocol_schema.toml index 28f18c91977..0eb924a54e4 100644 --- a/tools/protocol-schema-check/res/protocol_schema.toml +++ b/tools/protocol-schema-check/res/protocol_schema.toml @@ -169,6 +169,7 @@ RawTrieNodeWithSize = 1474149765 ReasonForBan = 792112981 Receipt = 2916802703 ReceiptEnum = 3157292228 +ReceiptList = 273687817 ReceiptProof = 1019992812 ReceiptProofResponse = 4034805727 ReceiptV0 = 3604411866 From c0c172fcecd76e36e63c7a64f8e35feca5943bc1 Mon Sep 17 00:00:00 2001 From: Tayfun Elmas Date: Fri, 20 Sep 2024 18:28:46 +0300 Subject: [PATCH 03/49] test: Add new test cases for chunk validator kickouts when endorsements are missed (#12117) We update the existing testloop file `chunk_validator_kickout.rs` to add two new test cases. The test exercise the path where endorsements from a particular validator are all dropped. If the validator is chunk-validator only, it is kicked out, otherwise it is not. Note that we do not remove the existing test cases where the chunks validated by a validator are dropped (so they are counted as missing endorsements), since I think these cases are still useful to exercise. --- .../stateless_validation/chunk_endorsement.rs | 8 + integration-tests/src/test_loop/builder.rs | 15 +- .../tests/chunk_validator_kickout.rs | 141 +++++++++++++----- .../src/test_loop/utils/network.rs | 14 ++ 4 files changed, 137 insertions(+), 41 deletions(-) diff --git a/core/primitives/src/stateless_validation/chunk_endorsement.rs b/core/primitives/src/stateless_validation/chunk_endorsement.rs index 07f0f6f3088..e6fca3a37da 100644 --- a/core/primitives/src/stateless_validation/chunk_endorsement.rs +++ b/core/primitives/src/stateless_validation/chunk_endorsement.rs @@ -42,6 +42,14 @@ impl ChunkEndorsement { let data = borsh::to_vec(&inner).unwrap(); signature.verify(&data, public_key) } + + /// Returns the account ID of the chunk validator that generated this endorsement. + pub fn validator_account(&self) -> &AccountId { + match self { + ChunkEndorsement::V1(v1) => &v1.account_id, + ChunkEndorsement::V2(v2) => &v2.metadata.account_id, + } + } } #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize, ProtocolSchema)] diff --git a/integration-tests/src/test_loop/builder.rs b/integration-tests/src/test_loop/builder.rs index d1c13571e40..28b17f388dd 100644 --- a/integration-tests/src/test_loop/builder.rs +++ b/integration-tests/src/test_loop/builder.rs @@ -39,7 +39,7 @@ use nearcore::state_sync::StateSyncDumper; use tempfile::TempDir; use super::env::{ClientToShardsManagerSender, TestData, TestLoopChunksStorage, TestLoopEnv}; -use super::utils::network::partial_encoded_chunks_dropper; +use super::utils::network::{chunk_endorsement_dropper, partial_encoded_chunks_dropper}; pub(crate) struct TestLoopBuilder { test_loop: TestLoopV2, @@ -60,6 +60,8 @@ pub(crate) struct TestLoopBuilder { chunks_storage: Arc>, /// Whether test loop should drop all chunks validated by the given account. drop_chunks_validated_by: Option, + /// Whether test loop should drop all endorsements from the given account. + drop_endorsements_from: Option, /// Number of latest epochs to keep before garbage collecting associated data. gc_num_epochs_to_keep: Option, /// The store of runtime configurations to be passed into runtime adapters. @@ -81,6 +83,7 @@ impl TestLoopBuilder { archival_clients: HashSet::new(), chunks_storage: Default::default(), drop_chunks_validated_by: None, + drop_endorsements_from: None, gc_num_epochs_to_keep: None, runtime_config_store: None, config_modifier: None, @@ -124,6 +127,11 @@ impl TestLoopBuilder { self } + pub(crate) fn drop_endorsements_from(mut self, account_id: &str) -> Self { + self.drop_endorsements_from = Some(account_id.parse().unwrap()); + self + } + pub(crate) fn gc_num_epochs_to_keep(mut self, num_epochs: u64) -> Self { self.gc_num_epochs_to_keep = Some(num_epochs); self @@ -519,6 +527,11 @@ impl TestLoopBuilder { )); } + if let Some(account_id) = &self.drop_endorsements_from { + peer_manager_actor + .register_override_handler(chunk_endorsement_dropper(account_id.clone())); + } + self.test_loop.register_actor_for_index( idx, peer_manager_actor, diff --git a/integration-tests/src/test_loop/tests/chunk_validator_kickout.rs b/integration-tests/src/test_loop/tests/chunk_validator_kickout.rs index a72e7eeeefd..e74b4c7a4cc 100644 --- a/integration-tests/src/test_loop/tests/chunk_validator_kickout.rs +++ b/integration-tests/src/test_loop/tests/chunk_validator_kickout.rs @@ -8,29 +8,68 @@ use near_async::time::Duration; use near_chain_configs::test_genesis::TestGenesisBuilder; use near_o11y::testonly::init_test_logger; use near_primitives::types::AccountId; -use std::string::ToString; -fn run_test_chunk_validator_kickout(select_chunk_validator_only: bool) { - init_test_logger(); - let builder = TestLoopBuilder::new(); +const NUM_ACCOUNTS: usize = 8; +const NUM_PRODUCER_ACCOUNTS: usize = 6; + +fn create_accounts() -> Vec { + (0..NUM_ACCOUNTS).map(|i| format!("account{}", i).parse().unwrap()).collect::>() +} + +enum TestCase { + /// Drop chunks validated by the account. + DropChunksValidatedBy(AccountId), + /// Drop endorsements from the account. + DropEndorsementsFrom(AccountId), +} + +impl TestCase { + fn selected_account(&self) -> &AccountId { + match self { + TestCase::DropChunksValidatedBy(account_id) => account_id, + TestCase::DropEndorsementsFrom(account_id) => account_id, + } + } +} +fn run_test_chunk_validator_kickout(accounts: Vec, test_case: TestCase) { + init_test_logger(); let initial_balance = 10000 * ONE_NEAR; let epoch_length = 10; - let accounts = - (0..8).map(|i| format!("account{}", i).parse().unwrap()).collect::>(); let clients = accounts.iter().cloned().collect_vec(); let accounts_str = accounts.iter().map(|a| a.as_str()).collect_vec(); - let (block_and_chunk_producers, chunk_validators_only) = accounts_str.split_at(6); - - // Select the account to kick out. - // Only chunk validator-only node can be kicked out for low endorsement - // stats. - let account_id = if select_chunk_validator_only { - chunk_validators_only[0] - } else { - block_and_chunk_producers[3] + let (block_and_chunk_producers, chunk_validators_only) = + accounts_str.split_at(NUM_PRODUCER_ACCOUNTS); + + let builder = match &test_case { + TestCase::DropChunksValidatedBy(account_id) => TestLoopBuilder::new() + // Drop only chunks validated by `account_id`. + // By how our endorsement stats are computed, this will count as this + // validator validating zero chunks. + .drop_chunks_validated_by(account_id.as_str()), + TestCase::DropEndorsementsFrom(account_id) => TestLoopBuilder::new() + // Drop only endorsements for chunks validated by `account_id`. + .drop_endorsements_from(account_id.as_str()), + }; + + let num_validator_mandates_per_shard = match &test_case { + // Target giving one mandate to each chunk validator, which results in + // every chunk validator validating only one shard in most cases. + // As a result, when we drop the chunk, we also zero-out all the endorsements + // of the corresponding chunk validator. + TestCase::DropChunksValidatedBy(_) => 1, + // Target giving a large number of mandates to each chunk validator, so that if we drop all the + // endorsements from one of the validators, this will not result in missing any chunks. + TestCase::DropEndorsementsFrom(_) => 16, }; - let expect_kickout = select_chunk_validator_only; + + // Only chunk validator-only node can be kicked out for low endorsement stats. + let account_to_kickout = + if chunk_validators_only.contains(&test_case.selected_account().as_str()) { + Some(test_case.selected_account()) + } else { + None + }; let mut genesis_builder = TestGenesisBuilder::new(); genesis_builder @@ -41,45 +80,47 @@ fn run_test_chunk_validator_kickout(select_chunk_validator_only: bool) { .validators_desired_roles(block_and_chunk_producers, chunk_validators_only) // Set up config to kick out only chunk validators for low performance. .kickouts_for_chunk_validators_only() - // Target giving one mandate to each chunk validator, which results in - // every chunk validator validating only one shard in most cases. - .target_validator_mandates_per_shard(1); + .target_validator_mandates_per_shard(num_validator_mandates_per_shard); for account in &accounts { genesis_builder.add_user_account_simple(account.clone(), initial_balance); } let genesis = genesis_builder.build(); - let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = builder - .genesis(genesis) - .clients(clients) - // Drop only chunks validated by `account_id`. - // By how our endorsement stats are computed, this will count as this - // validator validating zero chunks. - .drop_chunks_validated_by(account_id) - .build(); + let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = + builder.genesis(genesis).clients(clients).build(); // Run chain until our targeted chunk validator is (not) kicked out. let client_handle = node_datas[0].client_sender.actor_handle(); let initial_validators = get_epoch_all_validators(&test_loop.data.get(&client_handle).client); - assert_eq!(initial_validators.len(), 8); - assert!(initial_validators.contains(&account_id.to_string())); + assert_eq!(initial_validators.len(), NUM_ACCOUNTS); + assert!(initial_validators.contains(&test_case.selected_account().to_string())); let success_condition = |test_loop_data: &mut TestLoopData| -> bool { let client = &test_loop_data.get(&client_handle).client; - let validators = get_epoch_all_validators(client); let tip = client.chain.head().unwrap(); + + // Check the number of missed chunks for each test case. + let block = client.chain.get_block(&tip.last_block_hash).unwrap(); + let num_missed_chunks = block.header().chunk_mask().iter().filter(|c| !**c).count(); + match &test_case { + TestCase::DropChunksValidatedBy(_) => assert!(num_missed_chunks <= 1, + "At most one chunk must be missed when dropping chunks validated by the selected account"), + TestCase::DropEndorsementsFrom(_) => assert_eq!(num_missed_chunks, 0, + "No chunk must be missed when dropping endorsements from the selected account"), + } + + let validators = get_epoch_all_validators(client); let epoch_height = client.epoch_manager.get_epoch_height_from_prev_block(&tip.prev_block_hash).unwrap(); - - if expect_kickout { + if let Some(account_id) = &account_to_kickout { assert!(epoch_height < 4); - return if validators.len() == 7 { + return if validators.len() == NUM_ACCOUNTS - 1 { assert!(!validators.contains(&account_id.to_string())); true } else { false }; } else { - assert_eq!(validators.len(), 8, "No kickouts are expected"); + assert_eq!(validators.len(), NUM_ACCOUNTS, "No kickouts are expected"); epoch_height >= 4 } }; @@ -94,14 +135,34 @@ fn run_test_chunk_validator_kickout(select_chunk_validator_only: bool) { .shutdown_and_drain_remaining_events(Duration::seconds(20)); } -/// Checks that chunk validator with low endorsement stats is kicked out. +/// Checks that chunk validator with low endorsement stats is kicked out when the chunks it would validate are all dropped. +#[test] +fn test_chunk_validator_kicked_out_when_chunks_dropped() { + let accounts = create_accounts(); + let test_case = TestCase::DropChunksValidatedBy(accounts[NUM_PRODUCER_ACCOUNTS + 1].clone()); + run_test_chunk_validator_kickout(accounts, test_case); +} + +/// Checks that block producer with low chunk endorsement stats is not kicked out when the chunks it would validate are all dropped. +#[test] +fn test_block_producer_not_kicked_out_when_chunks_dropped() { + let accounts = create_accounts(); + let test_case = TestCase::DropChunksValidatedBy(accounts[NUM_PRODUCER_ACCOUNTS - 1].clone()); + run_test_chunk_validator_kickout(accounts, test_case); +} + +/// Checks that chunk validator with low endorsement stats is kicked out when the endorsements it generates are all dropped. #[test] -fn test_chunk_validator_kicked_out() { - run_test_chunk_validator_kickout(true); +fn test_chunk_validator_kicked_out_when_endorsements_dropped() { + let accounts = create_accounts(); + let test_case = TestCase::DropEndorsementsFrom(accounts[NUM_PRODUCER_ACCOUNTS + 1].clone()); + run_test_chunk_validator_kickout(accounts, test_case); } -/// Checks that block producer with low chunk endorsement stats is not kicked out. +/// Checks that block producer with low chunk endorsement stats is not kicked out when the endorsements it generates are all dropped. #[test] -fn test_block_producer_not_kicked_out() { - run_test_chunk_validator_kickout(false); +fn test_block_producer_not_kicked_out_when_endorsements_dropped() { + let accounts = create_accounts(); + let test_case = TestCase::DropEndorsementsFrom(accounts[NUM_PRODUCER_ACCOUNTS - 1].clone()); + run_test_chunk_validator_kickout(accounts, test_case); } diff --git a/integration-tests/src/test_loop/utils/network.rs b/integration-tests/src/test_loop/utils/network.rs index 0b689c09b4c..6181045bb90 100644 --- a/integration-tests/src/test_loop/utils/network.rs +++ b/integration-tests/src/test_loop/utils/network.rs @@ -75,3 +75,17 @@ pub fn partial_encoded_chunks_dropper( return None; }) } + +/// Handler to drop all network messages containing chunk endorsements sent from a given chunk-validator account. +pub fn chunk_endorsement_dropper( + validator: AccountId, +) -> Box Option> { + Box::new(move |request| { + if let NetworkRequests::ChunkEndorsement(_target, endorsement) = &request { + if endorsement.validator_account() == &validator { + return None; + } + } + Some(request) + }) +} From a18810a05b4a463cc89641b3d26cda972868180e Mon Sep 17 00:00:00 2001 From: Saketh Are Date: Fri, 20 Sep 2024 13:56:37 -0400 Subject: [PATCH 04/49] feat(network): overhaul state part request (#12110) In this PR we implement the ability to request state parts from arbitrary peers in the network via routed messages. Previously, state parts were requested via a PeerMessage which can only be sent to directly connected peers of the node. Because the responses to these requests are large and non-time-sensitive, it is undesirable to send them over the tier1/tier2 connections used for other operations of the protocol. Hence we also introduce a new connection pool tier3 used for the sole purpose of transmitting large one-time payloads. A separate PR will follow which overhauls the state sync actor in accordance with these changes. The end-to-end behavior has been built and tested in #12095. --- chain/client/src/sync/state.rs | 64 ++++++--- chain/client/src/tests/catching_up.rs | 11 +- .../src/network_protocol/borsh_conv.rs | 3 + chain/network/src/network_protocol/mod.rs | 3 + .../src/network_protocol/network.proto | 16 +-- .../proto_conv/peer_message.rs | 4 + .../src/network_protocol/state_sync.rs | 23 +++ chain/network/src/peer/peer_actor.rs | 42 +++++- .../src/peer_manager/connection/mod.rs | 4 + .../src/peer_manager/network_state/mod.rs | 67 +++++++-- .../src/peer_manager/network_state/routing.rs | 5 + .../src/peer_manager/peer_manager_actor.rs | 133 +++++++++++++++++- .../src/peer_manager/tests/connection_pool.rs | 3 +- .../src/rate_limits/messages_limits.rs | 2 + chain/network/src/snapshot_hosts/mod.rs | 1 - chain/network/src/tcp.rs | 5 + chain/network/src/types.rs | 28 +++- .../res/protocol_schema.toml | 7 +- 18 files changed, 362 insertions(+), 59 deletions(-) diff --git a/chain/client/src/sync/state.rs b/chain/client/src/sync/state.rs index 0344ab89d5e..16cece2a726 100644 --- a/chain/client/src/sync/state.rs +++ b/chain/client/src/sync/state.rs @@ -675,24 +675,42 @@ impl StateSync { for ((part_id, download), target) in parts_to_fetch(new_shard_sync_download).zip(possible_targets_sampler) { - sent_request_part( - self.clock.clone(), - target.clone(), - part_id, - shard_id, - sync_hash, - last_part_id_requested, - requested_target, - self.timeout, - ); - request_part_from_peers( - part_id, - target, - download, - shard_id, - sync_hash, - &self.network_adapter, - ); + // The request sent to the network adapater needs to include the sync_prev_prev_hash + // so that a peer hosting the correct snapshot can be selected. + let prev_header = chain + .get_block_header(&sync_hash) + .map(|header| chain.get_block_header(&header.prev_hash())); + + match prev_header { + Ok(Ok(prev_header)) => { + let sync_prev_prev_hash = prev_header.prev_hash(); + sent_request_part( + self.clock.clone(), + target.clone(), + part_id, + shard_id, + sync_hash, + last_part_id_requested, + requested_target, + self.timeout, + ); + request_part_from_peers( + part_id, + target, + download, + shard_id, + sync_hash, + *sync_prev_prev_hash, + &self.network_adapter, + ); + } + Ok(Err(err)) => { + tracing::error!(target: "sync", %shard_id, %sync_hash, ?err, "could not get prev header"); + } + Err(err) => { + tracing::error!(target: "sync", %shard_id, %sync_hash, ?err, "could not get header"); + } + } } } StateSyncInner::External { chain_id, semaphore, external } => { @@ -1304,18 +1322,24 @@ fn request_part_from_peers( download: &mut DownloadStatus, shard_id: ShardId, sync_hash: CryptoHash, + sync_prev_prev_hash: CryptoHash, network_adapter: &PeerManagerAdapter, ) { download.run_me.store(false, Ordering::SeqCst); download.state_requests_count += 1; - download.last_target = Some(peer_id.clone()); + download.last_target = Some(peer_id); let run_me = download.run_me.clone(); near_performance_metrics::actix::spawn( "StateSync", network_adapter .send_async(PeerManagerMessageRequest::NetworkRequests( - NetworkRequests::StateRequestPart { shard_id, sync_hash, part_id, peer_id }, + NetworkRequests::StateRequestPart { + shard_id, + sync_hash, + sync_prev_prev_hash, + part_id, + }, )) .then(move |result| { // TODO: possible optimization - in the current code, even if one of the targets it not present in the network graph diff --git a/chain/client/src/tests/catching_up.rs b/chain/client/src/tests/catching_up.rs index 19a5ced219a..f49db6989ec 100644 --- a/chain/client/src/tests/catching_up.rs +++ b/chain/client/src/tests/catching_up.rs @@ -101,8 +101,9 @@ enum ReceiptsSyncPhases { pub struct StateRequestStruct { pub shard_id: u64, pub sync_hash: CryptoHash, + pub sync_prev_prev_hash: Option, pub part_id: Option, - pub peer_id: PeerId, + pub peer_id: Option, } /// Sanity checks that the incoming and outgoing receipts are properly sent and received @@ -268,8 +269,9 @@ fn test_catchup_receipts_sync_common(wait_till: u64, send: u64, sync_hold: bool) let srs = StateRequestStruct { shard_id: *shard_id, sync_hash: *sync_hash, + sync_prev_prev_hash: None, part_id: None, - peer_id: peer_id.clone(), + peer_id: Some(peer_id.clone()), }; if !seen_hashes_with_state .contains(&hash_func(&borsh::to_vec(&srs).unwrap())) @@ -283,16 +285,17 @@ fn test_catchup_receipts_sync_common(wait_till: u64, send: u64, sync_hold: bool) if let NetworkRequests::StateRequestPart { shard_id, sync_hash, + sync_prev_prev_hash, part_id, - peer_id, } = msg { if sync_hold { let srs = StateRequestStruct { shard_id: *shard_id, sync_hash: *sync_hash, + sync_prev_prev_hash: Some(*sync_prev_prev_hash), part_id: Some(*part_id), - peer_id: peer_id.clone(), + peer_id: None, }; if !seen_hashes_with_state .contains(&hash_func(&borsh::to_vec(&srs).unwrap())) diff --git a/chain/network/src/network_protocol/borsh_conv.rs b/chain/network/src/network_protocol/borsh_conv.rs index 4ef69a0dc58..cb899f8f896 100644 --- a/chain/network/src/network_protocol/borsh_conv.rs +++ b/chain/network/src/network_protocol/borsh_conv.rs @@ -216,6 +216,9 @@ impl From<&mem::PeerMessage> for net::PeerMessage { panic!("Tier1Handshake is not supported in Borsh encoding") } mem::PeerMessage::Tier2Handshake(h) => net::PeerMessage::Handshake((&h).into()), + mem::PeerMessage::Tier3Handshake(_) => { + panic!("Tier3Handshake is not supported in Borsh encoding") + } mem::PeerMessage::HandshakeFailure(pi, hfr) => { net::PeerMessage::HandshakeFailure(pi, (&hfr).into()) } diff --git a/chain/network/src/network_protocol/mod.rs b/chain/network/src/network_protocol/mod.rs index 6c584219c70..a2a2c28b0e2 100644 --- a/chain/network/src/network_protocol/mod.rs +++ b/chain/network/src/network_protocol/mod.rs @@ -411,6 +411,7 @@ pub struct Disconnect { pub enum PeerMessage { Tier1Handshake(Handshake), Tier2Handshake(Handshake), + Tier3Handshake(Handshake), HandshakeFailure(PeerInfo, HandshakeFailureReason), /// When a failed nonce is used by some peer, this message is sent back as evidence. LastEdge(Edge), @@ -552,6 +553,7 @@ pub enum RoutedMessageBody { VersionedChunkEndorsement(ChunkEndorsement), EpochSyncRequest, EpochSyncResponse(CompressedEpochSyncProof), + StatePartRequest(StatePartRequest), } impl RoutedMessageBody { @@ -645,6 +647,7 @@ impl fmt::Debug for RoutedMessageBody { RoutedMessageBody::EpochSyncResponse(_) => { write!(f, "EpochSyncResponse") } + RoutedMessageBody::StatePartRequest(_) => write!(f, "StatePartRequest"), } } } diff --git a/chain/network/src/network_protocol/network.proto b/chain/network/src/network_protocol/network.proto index f60bd202312..cc95644d4d5 100644 --- a/chain/network/src/network_protocol/network.proto +++ b/chain/network/src/network_protocol/network.proto @@ -458,17 +458,15 @@ message PeerMessage { TraceContext trace_context = 26; oneof message_type { - // Handshakes for TIER1 and TIER2 networks are considered separate, - // so that a node binary which doesn't support TIER1 connection won't - // be even able to PARSE the handshake. This way we avoid accidental - // connections, such that one end thinks it is a TIER2 connection and the - // other thinks it is a TIER1 connection. As currently both TIER1 and TIER2 - // connections are handled by the same PeerActor, both fields use the same - // underlying message type. If we ever decide to separate the handshake - // implementations, we can copy the Handshake message type defition and - // make it evolve differently for TIER1 and TIER2. + // Handshakes for different network tiers explicitly use different PeerMessage variants. + // This way we avoid accidental connections, such that one end thinks it is a TIER2 connection + // and the other thinks it is a TIER1 connection. Currently the same PeerActor handles + // all types of connections, hence the contents are identical for all types of connections. + // If we ever decide to separate the handshake implementations, we can copy the Handshake message + // type definition and make it evolve differently for different tiers. Handshake tier1_handshake = 27; Handshake tier2_handshake = 4; + Handshake tier3_handshake = 33; HandshakeFailure handshake_failure = 5; LastEdge last_edge = 6; diff --git a/chain/network/src/network_protocol/proto_conv/peer_message.rs b/chain/network/src/network_protocol/proto_conv/peer_message.rs index 38b4250b15a..b73a66d7966 100644 --- a/chain/network/src/network_protocol/proto_conv/peer_message.rs +++ b/chain/network/src/network_protocol/proto_conv/peer_message.rs @@ -234,6 +234,7 @@ impl From<&PeerMessage> for proto::PeerMessage { message_type: Some(match x { PeerMessage::Tier1Handshake(h) => ProtoMT::Tier1Handshake(h.into()), PeerMessage::Tier2Handshake(h) => ProtoMT::Tier2Handshake(h.into()), + PeerMessage::Tier3Handshake(h) => ProtoMT::Tier3Handshake(h.into()), PeerMessage::HandshakeFailure(pi, hfr) => { ProtoMT::HandshakeFailure((pi, hfr).into()) } @@ -398,6 +399,9 @@ impl TryFrom<&proto::PeerMessage> for PeerMessage { ProtoMT::Tier2Handshake(h) => { PeerMessage::Tier2Handshake(h.try_into().map_err(Self::Error::Handshake)?) } + ProtoMT::Tier3Handshake(h) => { + PeerMessage::Tier3Handshake(h.try_into().map_err(Self::Error::Handshake)?) + } ProtoMT::HandshakeFailure(hf) => { let (pi, hfr) = hf.try_into().map_err(Self::Error::HandshakeFailure)?; PeerMessage::HandshakeFailure(pi, hfr) diff --git a/chain/network/src/network_protocol/state_sync.rs b/chain/network/src/network_protocol/state_sync.rs index d30170b83d5..c500893f3f4 100644 --- a/chain/network/src/network_protocol/state_sync.rs +++ b/chain/network/src/network_protocol/state_sync.rs @@ -107,3 +107,26 @@ pub enum SnapshotHostInfoVerificationError { )] TooManyShards(usize), } + +/// Message used to request a state part. +/// +#[derive( + Clone, + Debug, + Eq, + PartialEq, + Hash, + borsh::BorshSerialize, + borsh::BorshDeserialize, + ProtocolSchema, +)] +pub struct StatePartRequest { + /// Requested shard id + pub shard_id: ShardId, + /// Hash of the requested snapshot's state root + pub sync_hash: CryptoHash, + /// Requested part id + pub part_id: u64, + /// Public address of the node making the request + pub addr: std::net::SocketAddr, +} diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index 4ff60267190..1e585d70b94 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -284,6 +284,18 @@ impl PeerActor { .start_outbound(peer_id.clone()) .map_err(ClosingReason::OutboundNotAllowed)? } + tcp::Tier::T3 => { + // Loop connections are allowed only on T1; see comment above + if peer_id == &network_state.config.node_id() { + return Err(ClosingReason::OutboundNotAllowed( + connection::PoolError::UnexpectedLoopConnection, + )); + } + network_state + .tier3 + .start_outbound(peer_id.clone()) + .map_err(ClosingReason::OutboundNotAllowed)? + } }, handshake_spec: HandshakeSpec { partial_edge_info: network_state.propose_edge(&clock, peer_id, None), @@ -293,10 +305,12 @@ impl PeerActor { }, }, }; - // Override force_encoding for outbound Tier1 connections, - // since Tier1Handshake is supported only with proto encoding. + // Override force_encoding for outbound Tier1 and Tier3 connections; + // Tier1Handshake and Tier3Handshake are supported only with proto encoding. let force_encoding = match &stream.type_ { - tcp::StreamType::Outbound { tier, .. } if tier == &tcp::Tier::T1 => { + tcp::StreamType::Outbound { tier, .. } + if tier == &tcp::Tier::T1 || tier == &tcp::Tier::T3 => + { Some(Encoding::Proto) } _ => force_encoding, @@ -480,6 +494,7 @@ impl PeerActor { let msg = match spec.tier { tcp::Tier::T1 => PeerMessage::Tier1Handshake(handshake), tcp::Tier::T2 => PeerMessage::Tier2Handshake(handshake), + tcp::Tier::T3 => PeerMessage::Tier3Handshake(handshake), }; self.send_message_or_log(&msg); } @@ -939,6 +954,9 @@ impl PeerActor { (PeerStatus::Connecting { .. }, PeerMessage::Tier2Handshake(msg)) => { self.process_handshake(ctx, tcp::Tier::T2, msg) } + (PeerStatus::Connecting { .. }, PeerMessage::Tier3Handshake(msg)) => { + self.process_handshake(ctx, tcp::Tier::T3, msg) + } (_, msg) => { tracing::warn!(target:"network","unexpected message during handshake: {}",msg) } @@ -1140,7 +1158,9 @@ impl PeerActor { self.stop(ctx, ClosingReason::DisconnectMessage); } - PeerMessage::Tier1Handshake(_) | PeerMessage::Tier2Handshake(_) => { + PeerMessage::Tier1Handshake(_) + | PeerMessage::Tier2Handshake(_) + | PeerMessage::Tier3Handshake(_) => { // Received handshake after already have seen handshake from this peer. tracing::debug!(target: "network", "Duplicate handshake from {}", self.peer_info); } @@ -1182,8 +1202,20 @@ impl PeerActor { self.stop(ctx, ClosingReason::Ban(ReasonForBan::Abusive)); } - // Add received peers to the peer store let node_id = self.network_state.config.node_id(); + + // Record our own IP address as observed by the peer. + if self.network_state.my_public_addr.read().is_none() { + if let Some(my_peer_info) = + direct_peers.iter().find(|peer_info| peer_info.id == node_id) + { + if let Some(addr) = my_peer_info.addr { + let mut my_public_addr = self.network_state.my_public_addr.write(); + *my_public_addr = Some(addr); + } + } + } + // Add received indirect peers to the peer store self.network_state.peer_store.add_indirect_peers( &self.clock, peers.into_iter().filter(|peer_info| peer_info.id != node_id), diff --git a/chain/network/src/peer_manager/connection/mod.rs b/chain/network/src/peer_manager/connection/mod.rs index ea9f7edccab..2035a673da5 100644 --- a/chain/network/src/peer_manager/connection/mod.rs +++ b/chain/network/src/peer_manager/connection/mod.rs @@ -36,8 +36,12 @@ impl tcp::Tier { match msg { PeerMessage::Tier1Handshake(_) => self == tcp::Tier::T1, PeerMessage::Tier2Handshake(_) => self == tcp::Tier::T2, + PeerMessage::Tier3Handshake(_) => self == tcp::Tier::T3, PeerMessage::HandshakeFailure(_, _) => true, PeerMessage::LastEdge(_) => true, + PeerMessage::VersionedStateResponse(_) => { + self == tcp::Tier::T2 || self == tcp::Tier::T3 + } PeerMessage::Routed(msg) => self.is_allowed_routed(&msg.body), _ => self == tcp::Tier::T2, } diff --git a/chain/network/src/peer_manager/network_state/mod.rs b/chain/network/src/peer_manager/network_state/mod.rs index 06c1bad9ffe..739b94e8101 100644 --- a/chain/network/src/peer_manager/network_state/mod.rs +++ b/chain/network/src/peer_manager/network_state/mod.rs @@ -28,7 +28,9 @@ use crate::state_witness::{ use crate::stats::metrics; use crate::store; use crate::tcp; -use crate::types::{ChainInfo, PeerType, ReasonForBan}; +use crate::types::{ + ChainInfo, PeerType, ReasonForBan, StatePartRequestBody, Tier3Request, Tier3RequestBody, +}; use anyhow::Context; use arc_swap::ArcSwap; use near_async::messaging::{CanSend, SendAsync, Sender}; @@ -38,7 +40,8 @@ use near_primitives::hash::CryptoHash; use near_primitives::network::PeerId; use near_primitives::stateless_validation::chunk_endorsement::ChunkEndorsement; use near_primitives::types::AccountId; -use parking_lot::Mutex; +use parking_lot::{Mutex, RwLock}; +use std::collections::VecDeque; use std::net::SocketAddr; use std::num::NonZeroUsize; use std::sync::atomic::AtomicUsize; @@ -115,8 +118,11 @@ pub(crate) struct NetworkState { /// Connected peers (inbound and outbound) with their full peer information. pub tier2: connection::Pool, pub tier1: connection::Pool, + pub tier3: connection::Pool, /// Semaphore limiting inflight inbound handshakes. pub inbound_handshake_permits: Arc, + /// The public IP of this node; available after connecting to any one peer. + pub my_public_addr: Arc>>, /// Peer store that provides read/write access to peers. pub peer_store: peer_store::PeerStore, /// Information about state snapshots hosted by network peers. @@ -143,6 +149,9 @@ pub(crate) struct NetworkState { /// TODO(gprusak): consider removing it altogether. pub tier1_route_back: Mutex, + /// Queue of received requests to which a response should be made over TIER3. + pub tier3_requests: Mutex>, + /// Shared counter across all PeerActors, which counts number of `RoutedMessageBody::ForwardTx` /// messages sincce last block. pub txns_since_last_block: AtomicUsize, @@ -194,7 +203,9 @@ impl NetworkState { chain_info: Default::default(), tier2: connection::Pool::new(config.node_id()), tier1: connection::Pool::new(config.node_id()), + tier3: connection::Pool::new(config.node_id()), inbound_handshake_permits: Arc::new(tokio::sync::Semaphore::new(LIMIT_PENDING_PEERS)), + my_public_addr: Arc::new(RwLock::new(None)), peer_store, snapshot_hosts: Arc::new(SnapshotHostsCache::new(config.snapshot_hosts.clone())), connection_store: connection_store::ConnectionStore::new(store.clone()).unwrap(), @@ -203,6 +214,7 @@ impl NetworkState { account_announcements: Arc::new(AnnounceAccountCache::new(store)), tier2_route_back: Mutex::new(RouteBackCache::default()), tier1_route_back: Mutex::new(RouteBackCache::default()), + tier3_requests: Mutex::new(VecDeque::::new()), recent_routed_messages: Mutex::new(lru::LruCache::new( NonZeroUsize::new(RECENT_ROUTED_MESSAGES_CACHE_SIZE).unwrap(), )), @@ -349,6 +361,18 @@ impl NetworkState { // Write to the peer store this.peer_store.peer_connected(&clock, peer_info); } + tcp::Tier::T3 => { + if conn.peer_type == PeerType::Inbound { + // TODO(saketh): When a peer initiates a TIER3 connection it should be + // responding to a request sent previously by the local node. If we + // maintain some state about pending requests it would be possible to add + // an additional layer of security here and reject unexpected connections. + } + if !edge.verify() { + return Err(RegisterPeerError::InvalidEdge); + } + this.tier3.insert_ready(conn).map_err(RegisterPeerError::PoolError)?; + } } Ok(()) }).await.unwrap() @@ -369,14 +393,19 @@ impl NetworkState { let clock = clock.clone(); let conn = conn.clone(); self.spawn(async move { - let peer_id = conn.peer_info.id.clone(); - if conn.tier == tcp::Tier::T1 { - // There is no banning or routing table for TIER1. - // Just remove the connection from the network_state. - this.tier1.remove(&conn); + match conn.tier { + tcp::Tier::T1 => this.tier1.remove(&conn), + tcp::Tier::T2 => this.tier2.remove(&conn), + tcp::Tier::T3 => this.tier3.remove(&conn), + } + + // The rest of this function has to do with banning or routing, + // which are applicable only for TIER2. + if conn.tier != tcp::Tier::T2 { return; } - this.tier2.remove(&conn); + + let peer_id = conn.peer_info.id.clone(); // If the last edge we have with this peer represent a connection addition, create the edge // update that represents the connection removal. @@ -558,6 +587,17 @@ impl NetworkState { } } } + tcp::Tier::T3 => { + let peer_id = match &msg.target { + PeerIdOrHash::Hash(_) => { + // There is no route back cache for TIER3 as all connections are direct + debug_assert!(false); + return false; + } + PeerIdOrHash::PeerId(peer_id) => peer_id.clone(), + }; + return self.tier3.send_message(peer_id, Arc::new(PeerMessage::Routed(msg))); + } } } @@ -743,6 +783,17 @@ impl NetworkState { self.client.send(EpochSyncResponseMessage { from_peer: peer_id, proof }); None } + RoutedMessageBody::StatePartRequest(request) => { + self.tier3_requests.lock().push_back(Tier3Request { + peer_info: PeerInfo { id: peer_id, addr: Some(request.addr), account_id: None }, + body: Tier3RequestBody::StatePart(StatePartRequestBody { + shard_id: request.shard_id, + sync_hash: request.sync_hash, + part_id: request.part_id, + }), + }); + None + } body => { tracing::error!(target: "network", "Peer received unexpected message type: {:?}", body); None diff --git a/chain/network/src/peer_manager/network_state/routing.rs b/chain/network/src/peer_manager/network_state/routing.rs index 0fe045fcdad..ccbf28c7f3f 100644 --- a/chain/network/src/peer_manager/network_state/routing.rs +++ b/chain/network/src/peer_manager/network_state/routing.rs @@ -210,9 +210,14 @@ impl NetworkState { tracing::trace!(target: "network", route_back = ?msg.clone(), "Received peer message that requires response"); let from = &conn.peer_info.id; + match conn.tier { tcp::Tier::T1 => self.tier1_route_back.lock().insert(&clock, msg.hash(), from.clone()), tcp::Tier::T2 => self.tier2_route_back.lock().insert(&clock, msg.hash(), from.clone()), + tcp::Tier::T3 => { + // TIER3 connections are direct by design; no routing is performed + debug_assert!(false) + } } } diff --git a/chain/network/src/peer_manager/peer_manager_actor.rs b/chain/network/src/peer_manager/peer_manager_actor.rs index 6e04e871203..30deb4aa529 100644 --- a/chain/network/src/peer_manager/peer_manager_actor.rs +++ b/chain/network/src/peer_manager/peer_manager_actor.rs @@ -1,10 +1,11 @@ -use crate::client::{ClientSenderForNetwork, SetNetworkInfo}; +use crate::client::{ClientSenderForNetwork, SetNetworkInfo, StateRequestPart}; use crate::config; use crate::debug::{DebugStatus, GetDebugStatus}; use crate::network_protocol; use crate::network_protocol::SyncSnapshotHosts; use crate::network_protocol::{ Disconnect, Edge, PeerIdOrHash, PeerMessage, Ping, Pong, RawRoutedMessage, RoutedMessageBody, + StatePartRequest, }; use crate::peer::peer_actor::PeerActor; use crate::peer_manager::connection; @@ -18,7 +19,7 @@ use crate::tcp; use crate::types::{ ConnectedPeerInfo, HighestHeightPeerInfo, KnownProducer, NetworkInfo, NetworkRequests, NetworkResponses, PeerInfo, PeerManagerMessageRequest, PeerManagerMessageResponse, PeerType, - SetChainInfo, SnapshotHostInfo, + SetChainInfo, SnapshotHostInfo, StatePartRequestBody, Tier3RequestBody, }; use ::time::ext::InstantExt as _; use actix::fut::future::wrap_future; @@ -87,6 +88,11 @@ pub(crate) const UPDATE_CONNECTION_STORE_INTERVAL: time::Duration = time::Durati /// How often to poll the NetworkState for closed connections we'd like to re-establish. pub(crate) const POLL_CONNECTION_STORE_INTERVAL: time::Duration = time::Duration::minutes(1); +/// How often we check for and process pending Tier3 requests +const PROCESS_TIER3_REQUESTS_INTERVAL: time::Duration = time::Duration::seconds(1); +/// The length of time that a Tier3 connection is allowed to idle before it is stopped +const TIER3_IDLE_TIMEOUT: time::Duration = time::Duration::seconds(15); + /// Actor that manages peers connections. pub struct PeerManagerActor { pub(crate) clock: time::Clock, @@ -338,6 +344,62 @@ impl PeerManagerActor { } } }); + // Periodically process pending Tier3 requests. + arbiter.spawn({ + let clock = clock.clone(); + let state = state.clone(); + let arbiter = arbiter.clone(); + let mut interval = time::Interval::new(clock.now(), PROCESS_TIER3_REQUESTS_INTERVAL); + async move { + loop { + interval.tick(&clock).await; + + if let Some(request) = state.tier3_requests.lock().pop_front() { + arbiter.spawn({ + let clock = clock.clone(); + let state = state.clone(); + async move { + let tier3_response = match request.body { + Tier3RequestBody::StatePart(StatePartRequestBody { shard_id, sync_hash, part_id }) => { + match state.client.send_async(StateRequestPart { shard_id, sync_hash, part_id }).await { + Ok(Some(client_response)) => { + PeerMessage::VersionedStateResponse(*client_response.0) + } + Ok(None) => { + tracing::debug!(target: "network", "client declined to respond to {:?}", request); + return; + } + Err(err) => { + tracing::error!(target: "network", ?err, "client failed to respond to {:?}", request); + return; + } + } + } + }; + + if !state.tier3.load().ready.contains_key(&request.peer_info.id) { + let result = async { + let stream = tcp::Stream::connect( + &request.peer_info, + tcp::Tier::T3, + &state.config.socket_options + ).await.context("tcp::Stream::connect()")?; + PeerActor::spawn_and_handshake(clock.clone(),stream,None,state.clone()).await.context("PeerActor::spawn()")?; + anyhow::Ok(()) + }.await; + + if let Err(ref err) = result { + tracing::info!(target: "network", err = format!("{:#}", err), "failed to connect to {}", request.peer_info); + } + } + + state.tier3.send_message(request.peer_info.id, Arc::new(tier3_response)); + } + }); + } + } + } + }); } }); Ok(Self::start_in_arbiter(&arbiter, move |_ctx| Self { @@ -553,6 +615,29 @@ impl PeerManagerActor { } } + /// TIER3 connections are established ad-hoc to transmit individual large messages. + /// Here we terminate these "single-purpose" connections after an idle timeout. + /// + /// When a TIER3 connection is established the intended message is already prepared in-memory, + /// so there is no concern of the timeout falling in between the handshake and the payload. + /// + /// A finer detail is that as long as a TIER3 connection remains open it can be reused to + /// transmit additional TIER3 payloads intended for the same peer. In such cases the message + /// can be lost if the timeout is reached precisely while it is in flight. For simplicity we + /// accept this risk; network requests are understood as unreliable and the requesting node has + /// retry logic anyway. TODO(saketh): consider if we can improve this in a simple way. + fn stop_tier3_idle_connections(&self) { + let now = self.clock.now(); + let _ = self + .state + .tier3 + .load() + .ready + .values() + .filter(|p| now - p.last_time_received_message.load() > TIER3_IDLE_TIMEOUT) + .map(|p| p.stop(None)); + } + /// Periodically monitor list of peers and: /// - request new peers from connected peers, /// - bootstrap outbound connections from known peers, @@ -621,6 +706,9 @@ impl PeerManagerActor { // If there are too many active connections try to remove some connections self.maybe_stop_active_connection(); + // Close Tier3 connections which have been idle for too long + self.stop_tier3_idle_connections(); + // Find peers that are not reliable (too much behind) - and make sure that we're not routing messages through them. let unreliable_peers = self.unreliable_peers(); metrics::PEER_UNRELIABLE.set(unreliable_peers.len() as i64); @@ -788,11 +876,42 @@ impl PeerManagerActor { NetworkResponses::RouteNotFound } } - NetworkRequests::StateRequestPart { shard_id, sync_hash, part_id, peer_id } => { - if self.state.tier2.send_message( - peer_id, - Arc::new(PeerMessage::StateRequestPart(shard_id, sync_hash, part_id)), - ) { + NetworkRequests::StateRequestPart { + shard_id, + sync_hash, + sync_prev_prev_hash, + part_id, + } => { + let mut success = false; + + // The node needs to include its own public address in the request + // so that the reponse can be sent over Tier3 + if let Some(addr) = *self.state.my_public_addr.read() { + if let Some(peer_id) = self.state.snapshot_hosts.select_host_for_part( + &sync_prev_prev_hash, + shard_id, + part_id, + ) { + success = + self.state.send_message_to_peer( + &self.clock, + tcp::Tier::T2, + self.state.sign_message( + &self.clock, + RawRoutedMessage { + target: PeerIdOrHash::PeerId(peer_id), + body: RoutedMessageBody::StatePartRequest( + StatePartRequest { shard_id, sync_hash, part_id, addr }, + ), + }, + ), + ); + } else { + tracing::debug!(target: "network", "no hosts available for {shard_id}, {sync_prev_prev_hash}"); + } + } + + if success { NetworkResponses::NoResponse } else { NetworkResponses::RouteNotFound diff --git a/chain/network/src/peer_manager/tests/connection_pool.rs b/chain/network/src/peer_manager/tests/connection_pool.rs index 79e00807f20..15da55a57aa 100644 --- a/chain/network/src/peer_manager/tests/connection_pool.rs +++ b/chain/network/src/peer_manager/tests/connection_pool.rs @@ -273,7 +273,7 @@ async fn invalid_edge() { ]; for (name, edge) in &testcases { - for tier in [tcp::Tier::T1, tcp::Tier::T2] { + for tier in [tcp::Tier::T1, tcp::Tier::T2, tcp::Tier::T3] { tracing::info!(target:"test","{name} {tier:?}"); let stream = tcp::Stream::connect(&pm.peer_info(), tier, &SocketOptions::default()) .await @@ -303,6 +303,7 @@ async fn invalid_edge() { let handshake = match tier { tcp::Tier::T1 => PeerMessage::Tier1Handshake(handshake), tcp::Tier::T2 => PeerMessage::Tier2Handshake(handshake), + tcp::Tier::T3 => PeerMessage::Tier3Handshake(handshake), }; stream.write(&handshake).await; let reason = events diff --git a/chain/network/src/rate_limits/messages_limits.rs b/chain/network/src/rate_limits/messages_limits.rs index 08d2d8ea40f..54638a829c3 100644 --- a/chain/network/src/rate_limits/messages_limits.rs +++ b/chain/network/src/rate_limits/messages_limits.rs @@ -220,6 +220,7 @@ fn get_key_and_token_cost(message: &PeerMessage) -> Option<(RateLimitedPeerMessa RoutedMessageBody::VersionedChunkEndorsement(_) => Some((ChunkEndorsement, 1)), RoutedMessageBody::EpochSyncRequest => None, RoutedMessageBody::EpochSyncResponse(_) => None, + RoutedMessageBody::StatePartRequest(_) => None, // TODO RoutedMessageBody::Ping(_) | RoutedMessageBody::Pong(_) | RoutedMessageBody::_UnusedChunkStateWitness @@ -239,6 +240,7 @@ fn get_key_and_token_cost(message: &PeerMessage) -> Option<(RateLimitedPeerMessa PeerMessage::VersionedStateResponse(_) => Some((VersionedStateResponse, 1)), PeerMessage::Tier1Handshake(_) | PeerMessage::Tier2Handshake(_) + | PeerMessage::Tier3Handshake(_) | PeerMessage::HandshakeFailure(_, _) | PeerMessage::LastEdge(_) | PeerMessage::Disconnect(_) diff --git a/chain/network/src/snapshot_hosts/mod.rs b/chain/network/src/snapshot_hosts/mod.rs index ca430900ac1..2a636b4e9ff 100644 --- a/chain/network/src/snapshot_hosts/mod.rs +++ b/chain/network/src/snapshot_hosts/mod.rs @@ -313,7 +313,6 @@ impl SnapshotHostsCache { } /// Given a state part request, selects a peer host to which the request should be sent. - #[allow(dead_code)] pub fn select_host_for_part( &self, sync_hash: &CryptoHash, diff --git a/chain/network/src/tcp.rs b/chain/network/src/tcp.rs index 06ba01a5033..5e5e78a7a42 100644 --- a/chain/network/src/tcp.rs +++ b/chain/network/src/tcp.rs @@ -26,6 +26,11 @@ pub enum Tier { /// consensus messages. Also, Tier1 peer discovery actually happens on Tier2 network, i.e. /// Tier2 network is necessary to bootstrap Tier1 connections. T2, + /// Tier3 connections are created ad hoc to directly transfer large messages, e.g. state parts. + /// Requests for state parts are routed over Tier2. A node receiving such a request initiates a + /// direct Tier3 connections to send the response. By sending large responses over dedicated + /// connections we avoid delaying other messages and we minimize network bandwidth usage. + T3, } #[derive(Clone, Debug)] diff --git a/chain/network/src/types.rs b/chain/network/src/types.rs index 3ddcbbcc067..4b005e24c2b 100644 --- a/chain/network/src/types.rs +++ b/chain/network/src/types.rs @@ -245,7 +245,12 @@ pub enum NetworkRequests { /// Request state header for given shard at given state root. StateRequestHeader { shard_id: ShardId, sync_hash: CryptoHash, peer_id: PeerId }, /// Request state part for given shard at given state root. - StateRequestPart { shard_id: ShardId, sync_hash: CryptoHash, part_id: u64, peer_id: PeerId }, + StateRequestPart { + shard_id: ShardId, + sync_hash: CryptoHash, + sync_prev_prev_hash: CryptoHash, + part_id: u64, + }, /// Ban given peer. BanPeer { peer_id: PeerId, ban_reason: ReasonForBan }, /// Announce account @@ -498,3 +503,24 @@ pub struct AccountIdOrPeerTrackingShard { /// Only send messages to peers whose latest chain height is no less `min_height` pub min_height: BlockHeight, } + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// An inbound request to which a response should be sent over Tier3 +pub struct Tier3Request { + /// Target peer to send the response to + pub peer_info: PeerInfo, + /// Contents of the request + pub body: Tier3RequestBody, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Tier3RequestBody { + StatePart(StatePartRequestBody), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct StatePartRequestBody { + pub shard_id: ShardId, + pub sync_hash: CryptoHash, + pub part_id: u64, +} diff --git a/tools/protocol-schema-check/res/protocol_schema.toml b/tools/protocol-schema-check/res/protocol_schema.toml index 0eb924a54e4..ab871b63095 100644 --- a/tools/protocol-schema-check/res/protocol_schema.toml +++ b/tools/protocol-schema-check/res/protocol_schema.toml @@ -153,7 +153,7 @@ PeerChainInfoV2 = 2686179044 PeerId = 2447445523 PeerIdOrHash = 4080492546 PeerInfo = 3831734408 -PeerMessage = 1912504821 +PeerMessage = 2881871188 Ping = 2783493472 Pong = 3159638327 PrepareError = 4009037507 @@ -177,8 +177,8 @@ ReceiptV1 = 2994842769 ReceiptValidationError = 551721215 ReceivedData = 3601438283 RootProof = 3135729669 -RoutedMessage = 334669112 -RoutedMessageBody = 237812276 +RoutedMessage = 3434968924 +RoutedMessageBody = 4241045537 RoutingTableUpdate = 2987752645 Secp256K1PublicKey = 4117078281 Secp256K1Signature = 3687154735 @@ -212,6 +212,7 @@ StakeAction = 2002027105 StateChangeCause = 1569242014 StateHeaderKey = 1385533899 StatePartKey = 3498655211 +StatePartRequest = 4194196967 StateResponseInfo = 2184941925 StateResponseInfoV1 = 1435664823 StateResponseInfoV2 = 1784931382 From d6d6e4782f7d57a7b7acc2894e1668355012ff74 Mon Sep 17 00:00:00 2001 From: robin-near <111538878+robin-near@users.noreply.github.com> Date: Fri, 20 Sep 2024 12:29:19 -0700 Subject: [PATCH 05/49] [Epoch Sync] Support bootstrapping a node from another epoch-synced node. (#12100) * Store EpochSyncProof locally after as part of bootstrapping the node. * Use the stored EpochSyncProof to derive a new EpochSyncProof in order to handle EpochSyncRequests. * Add a new testloop test to check that one can bootstrap a new node via epoch sync from a node that was bootstrapped with epoch sync itself * Add new testloop tests to check that the EpochSyncProof derived based on an older EpochSyncProof is exactly equal to an EpochSync derived from genesis. --- chain/chain/src/garbage_collection.rs | 1 + chain/client/src/sync/epoch.rs | 66 +++++- core/async/src/test_loop.rs | 2 +- core/store/src/columns.rs | 11 +- integration-tests/src/test_loop/builder.rs | 6 + .../src/test_loop/tests/epoch_sync.rs | 199 +++++++++++++++--- 6 files changed, 246 insertions(+), 39 deletions(-) diff --git a/chain/chain/src/garbage_collection.rs b/chain/chain/src/garbage_collection.rs index 599119a112a..00da87b8ffe 100644 --- a/chain/chain/src/garbage_collection.rs +++ b/chain/chain/src/garbage_collection.rs @@ -991,6 +991,7 @@ impl<'a> ChainStoreUpdate<'a> { | DBCol::FlatStateChanges | DBCol::FlatStateDeltaMetadata | DBCol::FlatStorageStatus + | DBCol::EpochSyncProof | DBCol::Misc | DBCol::_ReceiptIdToShardId => unreachable!(), diff --git a/chain/client/src/sync/epoch.rs b/chain/client/src/sync/epoch.rs index 6f9dee93ae9..89f01e51999 100644 --- a/chain/client/src/sync/epoch.rs +++ b/chain/client/src/sync/epoch.rs @@ -53,7 +53,7 @@ impl EpochSync { /// Derives an epoch sync proof for a recent epoch, that can be directly used to bootstrap /// a new node or bring a far-behind node to a recent epoch. #[instrument(skip(store))] - fn derive_epoch_sync_proof(store: Store) -> Result { + pub fn derive_epoch_sync_proof(store: Store) -> Result { // Epoch sync initializes a new node with the first block of some epoch; we call that // epoch the "target epoch". In the context of talking about the proof or the newly // bootstrapped node, it is also called the "current epoch". @@ -111,17 +111,51 @@ impl EpochSync { .get_ser::(DBCol::EpochInfo, next_epoch.0.as_bytes())? .ok_or_else(|| Error::EpochOutOfBounds(next_epoch))?; - // TODO: don't always generate from genesis - let all_past_epochs = Self::get_all_past_epochs( + let genesis_epoch_info = store + .get_ser::(DBCol::EpochInfo, EpochId::default().0.as_bytes())? + .ok_or_else(|| Error::EpochOutOfBounds(EpochId::default()))?; + + // If we have an existing (possibly and likely outdated) EpochSyncProof stored on disk, + // the last epoch we have a proof for is the "previous epoch" included in that EpochSyncProof. + // Otherwise, the last epoch we have a "proof" for is the genesis epoch. + let existing_epoch_sync_proof = + store.get_ser::(DBCol::EpochSyncProof, &[])?; + let last_epoch_we_have_proof_for = existing_epoch_sync_proof + .as_ref() + .and_then(|existing_proof| { + existing_proof + .past_epochs + .last() + .map(|last_epoch| *last_epoch.last_final_block_header.epoch_id()) + }) + .unwrap_or_else(EpochId::default); + let last_epoch_height_we_have_proof_for = existing_epoch_sync_proof + .as_ref() + .map(|existing_proof| existing_proof.last_epoch.epoch_info.epoch_height()) + .unwrap_or_else(|| genesis_epoch_info.epoch_height()); + + // If the proof we stored is for the same epoch as current or older, then just return that. + if current_epoch_info.epoch_height() <= last_epoch_height_we_have_proof_for { + if let Some(existing_proof) = existing_epoch_sync_proof { + return Ok(existing_proof); + } + // Corner case for if the current epoch is genesis somehow. + return Err(Error::Other("Need at least three epochs to epoch sync".to_string())); + } + + let all_past_epochs_since_last_proof = Self::get_past_epoch_proofs_in_range( &store, - EpochId::default(), + last_epoch_we_have_proof_for, next_epoch, &final_block_header_in_current_epoch, )?; - if all_past_epochs.is_empty() { - return Err(Error::Other("Need at least three epochs to epoch sync".to_string())); + if all_past_epochs_since_last_proof.is_empty() { + return Err(Error::Other( + "Programming error: past epochs should not be empty".to_string(), + )); } - let prev_epoch = *all_past_epochs.last().unwrap().last_final_block_header.epoch_id(); + let prev_epoch = + *all_past_epochs_since_last_proof.last().unwrap().last_final_block_header.epoch_id(); let prev_epoch_info = store .get_ser::(DBCol::EpochInfo, prev_epoch.0.as_bytes())? .ok_or_else(|| Error::EpochOutOfBounds(prev_epoch))?; @@ -191,8 +225,14 @@ impl EpochSync { Error::Other("Could not find merkle proof for first block".to_string()) })?; + let all_past_epochs_including_old_proof = existing_epoch_sync_proof + .map(|proof| proof.past_epochs) + .unwrap_or_else(Vec::new) + .into_iter() + .chain(all_past_epochs_since_last_proof.into_iter()) + .collect(); let proof = EpochSyncProof { - past_epochs: all_past_epochs, + past_epochs: all_past_epochs_including_old_proof, last_epoch: EpochSyncProofLastEpochData { epoch_info: prev_epoch_info, next_epoch_info: current_epoch_info, @@ -220,7 +260,7 @@ impl EpochSync { /// (both exclusive). `current_epoch_any_header` is any block header in the current epoch, /// which is the epoch before `next_epoch`. #[instrument(skip(store, current_epoch_any_header))] - fn get_all_past_epochs( + fn get_past_epoch_proofs_in_range( store: &Store, after_epoch: EpochId, next_epoch: EpochId, @@ -405,16 +445,20 @@ impl EpochSync { // TODO(#11932): Verify the proof. - let last_header = proof.current_epoch.first_block_header_in_epoch; let mut store_update = chain.chain_store.store().store_update(); + // Store the EpochSyncProof, so that this node can derive a more recent EpochSyncProof + // to faciliate epoch sync of other nodes. + store_update.set_ser(DBCol::EpochSyncProof, &[], &proof)?; + + let last_header = proof.current_epoch.first_block_header_in_epoch; let mut update = chain.mut_chain_store().store_update(); update.save_block_header_no_update_tree(last_header.clone())?; update.save_block_header_no_update_tree( proof.current_epoch.last_block_header_in_prev_epoch, )?; update.save_block_header_no_update_tree( - proof.current_epoch.second_last_block_header_in_prev_epoch, + proof.current_epoch.second_last_block_header_in_prev_epoch.clone(), )?; tracing::info!( "last final block of last past epoch: {:?}", diff --git a/core/async/src/test_loop.rs b/core/async/src/test_loop.rs index 624ccdf2130..17ec2b51c32 100644 --- a/core/async/src/test_loop.rs +++ b/core/async/src/test_loop.rs @@ -98,7 +98,7 @@ pub struct TestLoopV2 { /// The next ID to assign to an event we receive. next_event_index: usize, /// The current virtual time. - current_time: Duration, + pub current_time: Duration, /// Fake clock that always returns the virtual time. clock: near_time::FakeClock, /// Shutdown flag. When this flag is true, delayed action runners will no diff --git a/core/store/src/columns.rs b/core/store/src/columns.rs index c585aa4eaaa..48b2a7f34fb 100644 --- a/core/store/src/columns.rs +++ b/core/store/src/columns.rs @@ -293,6 +293,13 @@ pub enum DBCol { /// Witnesses with the lowest index are garbage collected first. /// u64 -> LatestWitnessesKey LatestWitnessesByIndex, + /// A valid epoch sync proof that proves the transition from the genesis to some epoch, + /// beyond which we keep all headers in this node. Nodes bootstrapped via Epoch Sync will + /// have this column, which allows it to compute a more recent EpochSyncProof using block + /// headers collected after the stored EpochSyncProof. + /// - *Rows*: only one key with 0 bytes. + /// - *Column type*: `EpochSyncProof` + EpochSyncProof, } /// Defines different logical parts of a db key. @@ -494,7 +501,8 @@ impl DBCol { | DBCol::FlatState | DBCol::FlatStateChanges | DBCol::FlatStateDeltaMetadata - | DBCol::FlatStorageStatus => false, + | DBCol::FlatStorageStatus + | DBCol::EpochSyncProof => false, } } @@ -566,6 +574,7 @@ impl DBCol { DBCol::StateTransitionData => &[DBKeyType::BlockHash, DBKeyType::ShardId], DBCol::LatestChunkStateWitnesses => &[DBKeyType::LatestWitnessesKey], DBCol::LatestWitnessesByIndex => &[DBKeyType::LatestWitnessIndex], + DBCol::EpochSyncProof => &[DBKeyType::Empty], } } } diff --git a/integration-tests/src/test_loop/builder.rs b/integration-tests/src/test_loop/builder.rs index 28b17f388dd..e086d883ded 100644 --- a/integration-tests/src/test_loop/builder.rs +++ b/integration-tests/src/test_loop/builder.rs @@ -115,6 +115,12 @@ impl TestLoopBuilder { self } + /// Like stores_override, but all cold stores are None. + pub fn stores_override_hot_only(mut self, stores: Vec) -> Self { + self.stores_override = Some(stores.into_iter().map(|store| (store, None)).collect()); + self + } + /// Set the accounts whose clients should be configured as archival nodes in the test loop. /// These accounts should be a subset of the accounts provided to the `clients` method. pub(crate) fn archival_clients(mut self, clients: HashSet) -> Self { diff --git a/integration-tests/src/test_loop/tests/epoch_sync.rs b/integration-tests/src/test_loop/tests/epoch_sync.rs index 2df87f04c98..e94edfc17cc 100644 --- a/integration-tests/src/test_loop/tests/epoch_sync.rs +++ b/integration-tests/src/test_loop/tests/epoch_sync.rs @@ -1,36 +1,43 @@ use itertools::Itertools; use near_async::time::Duration; use near_chain_configs::test_genesis::TestGenesisBuilder; +use near_chain_configs::{Genesis, GenesisConfig}; use near_client::test_utils::test_loop::ClientQueries; use near_o11y::testonly::init_test_logger; use near_primitives::types::AccountId; +use near_store::{DBCol, Store}; +use tempfile::TempDir; use crate::test_loop::builder::TestLoopBuilder; use crate::test_loop::env::TestLoopEnv; use crate::test_loop::utils::transactions::execute_money_transfers; use crate::test_loop::utils::ONE_NEAR; use near_async::messaging::CanSend; -use near_chain::ChainStoreAccess; +use near_chain::{ChainStore, ChainStoreAccess}; +use near_client::sync::epoch::EpochSync; use near_client::SetNetworkInfo; use near_network::types::{HighestHeightPeerInfo, NetworkInfo, PeerInfo}; use near_primitives::block::GenesisId; +use near_primitives::epoch_sync::EpochSyncProof; +use near_primitives::hash::CryptoHash; use near_store::test_utils::create_test_store; use std::cell::RefCell; use std::rc::Rc; -const NUM_CLIENTS: usize = 4; +struct TestNetworkSetup { + tempdir: TempDir, + genesis: Genesis, + accounts: Vec, + stores: Vec, +} -// Test that a new node that only has genesis can use whatever method available -// to sync up to the current state of the network. -#[test] -fn test_epoch_sync_from_genesis() { - init_test_logger(); +fn setup_initial_blockchain(num_clients: usize) -> TestNetworkSetup { let builder = TestLoopBuilder::new(); let initial_balance = 10000 * ONE_NEAR; let accounts = (0..100).map(|i| format!("account{}", i).parse().unwrap()).collect::>(); - let clients = accounts.iter().take(NUM_CLIENTS).cloned().collect_vec(); + let clients = accounts.iter().take(num_clients).cloned().collect_vec(); let mut genesis_builder = TestGenesisBuilder::new(); genesis_builder @@ -79,7 +86,7 @@ fn test_epoch_sync_from_genesis() { // Make a new TestLoopEnv, adding a new node to the network, and check that it can properly sync. let mut stores = Vec::new(); for data in &node_datas { - stores.push(( + stores.push( test_loop .data .get(&data.client_sender.actor_handle()) @@ -88,10 +95,8 @@ fn test_epoch_sync_from_genesis() { .chain_store .store() .clone(), - None, - )); + ); } - stores.push((create_test_store(), None)); // new node starts empty. // Properly shut down the previous TestLoopEnv. // We must preserve the tempdir, since state dumps are stored there, @@ -99,14 +104,20 @@ fn test_epoch_sync_from_genesis() { let tempdir = TestLoopEnv { test_loop, datas: node_datas, tempdir } .shutdown_and_drain_remaining_events(Duration::seconds(5)); - tracing::info!("Starting new TestLoopEnv with new node"); + TestNetworkSetup { tempdir, genesis, accounts, stores } +} - let clients = accounts.iter().take(NUM_CLIENTS + 1).cloned().collect_vec(); +fn bootstrap_node_via_epoch_sync(setup: TestNetworkSetup, source_node: usize) -> TestNetworkSetup { + tracing::info!("Starting new TestLoopEnv with new node"); + let TestNetworkSetup { genesis, accounts, mut stores, tempdir } = setup; + let num_existing_clients = stores.len(); + let clients = accounts.iter().take(num_existing_clients + 1).cloned().collect_vec(); + stores.push(create_test_store()); // new node starts empty. let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = TestLoopBuilder::new() .genesis(genesis.clone()) .clients(clients) - .stores_override(stores) + .stores_override_hot_only(stores) .test_loop_data_dir(tempdir) .config_modifier(|config, _| { // Enable epoch sync, and make the horizon small enough to trigger it. @@ -125,20 +136,24 @@ fn test_epoch_sync_from_genesis() { // the networking layer is completely mocked out. So in order to allow the new node to sync, we // need to manually propagate the network info to the new node. In this case we'll tell the new // node that node 0 is available to sync from. - let chain0 = &test_loop.data.get(&node_datas[0].client_sender.actor_handle()).client.chain; + let source_chain = + &test_loop.data.get(&node_datas[source_node].client_sender.actor_handle()).client.chain; let peer_info = HighestHeightPeerInfo { archival: false, - genesis_id: GenesisId { chain_id: genesis.config.chain_id, hash: *chain0.genesis().hash() }, - highest_block_hash: chain0.head().unwrap().last_block_hash, - highest_block_height: chain0.head().unwrap().height, + genesis_id: GenesisId { + chain_id: genesis.config.chain_id.clone(), + hash: *source_chain.genesis().hash(), + }, + highest_block_hash: source_chain.head().unwrap().last_block_hash, + highest_block_height: source_chain.head().unwrap().height, tracked_shards: vec![], peer_info: PeerInfo { - account_id: Some(accounts[0].clone()), + account_id: Some(accounts[source_node].clone()), addr: None, - id: node_datas[0].peer_id.clone(), + id: node_datas[source_node].peer_id.clone(), }, }; - node_datas[NUM_CLIENTS].client_sender.send(SetNetworkInfo(NetworkInfo { + node_datas[num_existing_clients].client_sender.send(SetNetworkInfo(NetworkInfo { connected_peers: Vec::new(), highest_height_peers: vec![peer_info], // only this field matters. known_producers: vec![], @@ -193,10 +208,6 @@ fn test_epoch_sync_from_genesis() { }, Duration::seconds(30), ); - - TestLoopEnv { test_loop, datas: node_datas, tempdir } - .shutdown_and_drain_remaining_events(Duration::seconds(5)); - assert_eq!( sync_status_history.borrow().as_slice(), &[ @@ -221,4 +232,140 @@ fn test_epoch_sync_from_genesis() { .map(|s| s.to_string()) .collect::>() ); + + let mut stores = Vec::new(); + for data in &node_datas { + stores.push( + test_loop + .data + .get(&data.client_sender.actor_handle()) + .client + .chain + .chain_store + .store() + .clone(), + ); + } + + let tempdir = TestLoopEnv { test_loop, datas: node_datas, tempdir } + .shutdown_and_drain_remaining_events(Duration::seconds(5)); + + TestNetworkSetup { tempdir, genesis, accounts, stores } +} + +// Test that a new node that only has genesis can use Epoch Sync to bring itself +// up to date. +#[test] +fn test_epoch_sync_from_genesis() { + init_test_logger(); + let setup = setup_initial_blockchain(4); + bootstrap_node_via_epoch_sync(setup, 0); +} + +// Tests that after epoch syncing, we can use the new node to bootstrap another +// node via epoch sync. +#[test] +fn test_epoch_sync_from_another_epoch_synced_node() { + init_test_logger(); + let setup = setup_initial_blockchain(4); + let setup = bootstrap_node_via_epoch_sync(setup, 0); + bootstrap_node_via_epoch_sync(setup, 4); +} + +impl TestNetworkSetup { + fn derive_epoch_sync_proof(&self, node_index: usize) -> EpochSyncProof { + let store = self.stores[node_index].clone(); + EpochSync::derive_epoch_sync_proof(store).unwrap() + } + + fn chain_final_head_height(&self, node_index: usize) -> u64 { + let store = self.stores[node_index].clone(); + let chain_store = ChainStore::new(store, self.genesis.config.genesis_height, false); + chain_store.final_head().unwrap().height + } + + fn assert_epoch_sync_proof_existence_on_disk(&self, node_index: usize, exists: bool) { + let store = self.stores[node_index].clone(); + let proof = store.get_ser::(DBCol::EpochSyncProof, &[]).unwrap(); + assert_eq!(proof.is_some(), exists); + } + + fn assert_header_existence(&self, node_index: usize, height: u64, exists: bool) { + let store = self.stores[node_index].clone(); + let header = + store.get_ser::(DBCol::BlockHeight, &height.to_le_bytes()).unwrap(); + assert_eq!(header.is_some(), exists); + } +} + +/// Performs some basic checks for the epoch sync proof; does not check the proof's correctness. +fn sanity_check_epoch_sync_proof( + proof: &EpochSyncProof, + final_head_height: u64, + genesis_config: &GenesisConfig, +) { + let epoch_height_of_final_block = + (final_head_height - genesis_config.genesis_height - 1) / genesis_config.epoch_length + 1; + let expected_current_epoch_height = epoch_height_of_final_block - 1; + assert_eq!( + proof.current_epoch.first_block_info_in_epoch.height(), + genesis_config.genesis_height + + (expected_current_epoch_height - 1) * genesis_config.epoch_length + + 1 + ); + assert_eq!( + proof.last_epoch.first_block_in_epoch.height(), + genesis_config.genesis_height + + (expected_current_epoch_height - 2) * genesis_config.epoch_length + + 1 + ); + + // EpochSyncProof starts with epoch height 2 because the first height is proven by + // genesis. + let mut epoch_height = 2; + for past_epoch in &proof.past_epochs { + assert_eq!( + past_epoch.last_final_block_header.height(), + genesis_config.genesis_height + epoch_height * genesis_config.epoch_length - 2 + ); + epoch_height += 1; + } + assert_eq!(epoch_height, expected_current_epoch_height); +} + +#[test] +fn test_initial_epoch_sync_proof_sanity() { + init_test_logger(); + let setup = setup_initial_blockchain(4); + let proof = setup.derive_epoch_sync_proof(0); + let final_head_height = setup.chain_final_head_height(0); + sanity_check_epoch_sync_proof(&proof, final_head_height, &setup.genesis.config); + // Requesting the proof should not have persisted the proof on disk. This is intentional; + // it is to reduce the statefulness of the system so that we may modify the way the proof + // is presented in the future (for e.g. bug fixes) without a DB migration. + setup.assert_epoch_sync_proof_existence_on_disk(0, false); +} + +#[test] +fn test_epoch_sync_proof_sanity_from_epoch_synced_node() { + init_test_logger(); + let setup = setup_initial_blockchain(4); + let setup = bootstrap_node_via_epoch_sync(setup, 0); + let old_proof = setup.derive_epoch_sync_proof(0); + let new_proof = setup.derive_epoch_sync_proof(4); + let final_head_height_old = setup.chain_final_head_height(0); + let final_head_height_new = setup.chain_final_head_height(4); + sanity_check_epoch_sync_proof(&new_proof, final_head_height_new, &setup.genesis.config); + // Test loop shutdown mechanism should not have left any new block messages unhandled, + // so the nodes should be at the same height in the end. + assert_eq!(final_head_height_old, final_head_height_new); + assert_eq!(old_proof, new_proof); + + // On the original node we should have no proof but all headers. + setup.assert_epoch_sync_proof_existence_on_disk(0, false); + setup.assert_header_existence(0, setup.genesis.config.genesis_height + 1, true); + + // On the new node we should have a proof but missing headers for the old epochs. + setup.assert_epoch_sync_proof_existence_on_disk(4, true); + setup.assert_header_existence(4, setup.genesis.config.genesis_height + 1, false); } From e516989d068906444614b48f107a289fe594509a Mon Sep 17 00:00:00 2001 From: Simonas Kazlauskas Date: Mon, 23 Sep 2024 16:59:50 +0300 Subject: [PATCH 06/49] Pipelining (#12015) This reverts commit 4e93e4680bfcc8e8e445d30bd3106188e18eb234. This reverts commit 86cd45b1a6f58a190fb7ac518eb7ee309f7874a4. To address the issues seen that led to the functionality being reverted in the first place I had to add some mechanisms to query The State in a side-effect-free (pure) manner and to also revert some changes from the original change-set where we the new code would *not* produce the side-effects that were expected by the other nodes. --- core/store/src/contract.rs | 52 +++ core/store/src/lib.rs | 38 +- core/store/src/trie/mem/loading.rs | 2 +- core/store/src/trie/mod.rs | 116 ++++-- core/store/src/trie/receipts_column_helper.rs | 22 +- core/store/src/trie/resharding_v2.rs | 2 +- core/store/src/trie/update.rs | 64 ++-- .../client/features/fix_storage_usage.rs | 2 +- .../src/tests/client/features/nearvm.rs | 12 +- .../src/near_vm_runner/runner.rs | 7 +- runtime/near-vm-runner/src/wasmer2_runner.rs | 11 +- runtime/near-vm-runner/src/wasmer_runner.rs | 10 +- runtime/runtime/src/actions.rs | 91 ++--- runtime/runtime/src/balance_checker.rs | 4 +- runtime/runtime/src/congestion_control.rs | 14 +- runtime/runtime/src/ext.rs | 23 +- runtime/runtime/src/lib.rs | 271 ++++++++++++-- runtime/runtime/src/metrics.rs | 74 +++- runtime/runtime/src/pipelining.rs | 341 ++++++++++++++++++ runtime/runtime/src/state_viewer/mod.rs | 43 ++- 20 files changed, 967 insertions(+), 232 deletions(-) create mode 100644 core/store/src/contract.rs create mode 100644 runtime/runtime/src/pipelining.rs diff --git a/core/store/src/contract.rs b/core/store/src/contract.rs new file mode 100644 index 00000000000..b0f5ffca0a5 --- /dev/null +++ b/core/store/src/contract.rs @@ -0,0 +1,52 @@ +use crate::TrieStorage; +use near_primitives::hash::CryptoHash; +use near_vm_runner::ContractCode; +use std::collections::BTreeMap; +use std::sync::{Arc, Mutex}; + +/// Reads contract code from the trie by its hash. +/// +/// Cloning is cheap. +#[derive(Clone)] +pub struct ContractStorage { + storage: Arc, + + /// During an apply of a single chunk contracts may be deployed through the + /// `Action::DeployContract`. + /// + /// Unfortunately `TrieStorage` does not have a way to write to the underlying storage, and the + /// `TrieUpdate` will only write the contract once the whole transaction is committed at the + /// end of the chunk's apply. + /// + /// As a temporary work-around while we're still involving `Trie` for `ContractCode` storage, + /// we'll keep a list of such deployed contracts here. Once the contracts are no longer part of + /// The State this field should be removed, and the `Storage::store` function should be + /// adjusted to write out the contract into the relevant part of the database immediately + /// (without going through transactional storage operations and such). + uncommitted_deploys: Arc>>, +} + +impl ContractStorage { + pub fn new(storage: Arc) -> Self { + Self { storage, uncommitted_deploys: Default::default() } + } + + pub fn get(&self, code_hash: CryptoHash) -> Option { + { + let guard = self.uncommitted_deploys.lock().expect("no panics"); + if let Some(v) = guard.get(&code_hash) { + return Some(ContractCode::new(v.code().to_vec(), Some(code_hash))); + } + } + + match self.storage.retrieve_raw_bytes(&code_hash) { + Ok(raw_code) => Some(ContractCode::new(raw_code.to_vec(), Some(code_hash))), + Err(_) => None, + } + } + + pub fn store(&self, code: ContractCode) { + let mut guard = self.uncommitted_deploys.lock().expect("no panics"); + guard.insert(*code.hash(), code); + } +} diff --git a/core/store/src/lib.rs b/core/store/src/lib.rs index 415869d695f..e4444a504d9 100644 --- a/core/store/src/lib.rs +++ b/core/store/src/lib.rs @@ -46,6 +46,7 @@ use strum; pub mod cold_storage; mod columns; pub mod config; +pub mod contract; pub mod db; pub mod flat; pub mod genesis; @@ -761,6 +762,22 @@ pub fn get( } } +/// [`get`] without incurring side effects. +pub fn get_pure( + trie: &dyn TrieAccess, + key: &TrieKey, +) -> Result, StorageError> { + match trie.get_no_side_effects(key)? { + None => Ok(None), + Some(data) => match T::try_from_slice(&data) { + Err(_err) => { + Err(StorageError::StorageInconsistentState("Failed to deserialize".to_string())) + } + Ok(value) => Ok(Some(value)), + }, + } +} + /// Writes an object into Trie. pub fn set(state_update: &mut TrieUpdate, key: TrieKey, value: &T) { let data = borsh::to_vec(&value).expect("Borsh serializer is not expected to ever fail"); @@ -970,15 +987,6 @@ pub fn set_code(state_update: &mut TrieUpdate, account_id: AccountId, code: &Con state_update.set(TrieKey::ContractCode { account_id }, code.code().to_vec()); } -pub fn get_code( - trie: &dyn TrieAccess, - account_id: &AccountId, - code_hash: Option, -) -> Result, StorageError> { - let key = TrieKey::ContractCode { account_id: account_id.clone() }; - trie.get(&key).map(|opt| opt.map(|code| ContractCode::new(code, code_hash))) -} - /// Removes account, code and all access keys associated to it. pub fn remove_account( state_update: &mut TrieUpdate, @@ -1134,6 +1142,18 @@ impl ContractRuntimeCache for StoreContractRuntimeCache { } } +/// Get the contract WASM code from The State. +/// +/// Executing all the usual storage access side-effects. +pub fn get_code( + trie: &dyn TrieAccess, + account_id: &AccountId, + code_hash: Option, +) -> Result, StorageError> { + let key = TrieKey::ContractCode { account_id: account_id.clone() }; + trie.get(&key).map(|opt| opt.map(|code| ContractCode::new(code, code_hash))) +} + #[cfg(test)] mod tests { use near_primitives::hash::CryptoHash; diff --git a/core/store/src/trie/mem/loading.rs b/core/store/src/trie/mem/loading.rs index 347ed2b3ea7..882392b1552 100644 --- a/core/store/src/trie/mem/loading.rs +++ b/core/store/src/trie/mem/loading.rs @@ -271,7 +271,7 @@ mod tests { // Check that the accessed nodes are consistent with those from disk. for (node_hash, serialized_node) in nodes_accessed { let expected_serialized_node = - trie.internal_retrieve_trie_node(&node_hash, false).unwrap(); + trie.internal_retrieve_trie_node(&node_hash, false, true).unwrap(); assert_eq!(expected_serialized_node, serialized_node); } } diff --git a/core/store/src/trie/mod.rs b/core/store/src/trie/mod.rs index 21ca9fa1dc7..97d3ca76b34 100644 --- a/core/store/src/trie/mod.rs +++ b/core/store/src/trie/mod.rs @@ -370,6 +370,9 @@ pub trait TrieAccess { /// argument. fn get(&self, key: &TrieKey) -> Result>, StorageError>; + /// Retrieves value with given key without incurring any side-effects. + fn get_no_side_effects(&self, key: &TrieKey) -> Result>, StorageError>; + /// Check if the key is present. /// /// Equivalent to `Self::get(k)?.is_some()`, but avoids reading out the value. @@ -817,16 +820,19 @@ impl Trie { &self, hash: &CryptoHash, use_accounting_cache: bool, + side_effects: bool, ) -> Result, StorageError> { - let result = if use_accounting_cache { + let result = if side_effects && use_accounting_cache { self.accounting_cache .borrow_mut() .retrieve_raw_bytes_with_accounting(hash, &*self.storage)? } else { self.storage.retrieve_raw_bytes(hash)? }; - if let Some(recorder) = &self.recorder { - recorder.borrow_mut().record(hash, result.clone()); + if side_effects { + if let Some(recorder) = &self.recorder { + recorder.borrow_mut().record(hash, result.clone()); + } } Ok(result) } @@ -887,7 +893,7 @@ impl Trie { ) -> Result<(), StorageError> { match value { ValueHandle::HashAndSize(value) => { - self.internal_retrieve_trie_node(&value.hash, true)?; + self.internal_retrieve_trie_node(&value.hash, true, true)?; memory.refcount_changes.subtract(value.hash, 1); } ValueHandle::InMemory(_) => { @@ -1088,7 +1094,7 @@ impl Trie { } *limit -= 1; - let (bytes, raw_node, mem_usage) = match self.retrieve_raw_node(hash, true) { + let (bytes, raw_node, mem_usage) = match self.retrieve_raw_node(hash, true, true) { Ok(Some((bytes, raw_node))) => (bytes, raw_node.node, raw_node.memory_usage), Ok(None) => return writeln!(f, "{spaces}EmptyNode"), Err(err) => return writeln!(f, "{spaces}error {err}"), @@ -1194,11 +1200,12 @@ impl Trie { &self, hash: &CryptoHash, use_accounting_cache: bool, + side_effects: bool, ) -> Result, RawTrieNodeWithSize)>, StorageError> { if hash == &Self::EMPTY_ROOT { return Ok(None); } - let bytes = self.internal_retrieve_trie_node(hash, use_accounting_cache)?; + let bytes = self.internal_retrieve_trie_node(hash, use_accounting_cache, side_effects)?; let node = RawTrieNodeWithSize::try_from_slice(&bytes).map_err(|err| { StorageError::StorageInconsistentState(format!("Failed to decode node {hash}: {err}")) })?; @@ -1212,7 +1219,7 @@ impl Trie { &self, hash: &CryptoHash, ) -> Result { - let bytes = self.internal_retrieve_trie_node(hash, true)?; + let bytes = self.internal_retrieve_trie_node(hash, true, true)?; match RawTrieNodeWithSize::try_from_slice(&bytes) { Ok(_) => Ok(NodeOrValue::Node), Err(_) => Ok(NodeOrValue::Value(bytes)), @@ -1224,7 +1231,7 @@ impl Trie { memory: &mut NodesStorage, hash: &CryptoHash, ) -> Result { - match self.retrieve_raw_node(hash, true)? { + match self.retrieve_raw_node(hash, true, true)? { None => Ok(memory.store(TrieNodeWithSize::empty())), Some((_, node)) => { let result = memory.store(TrieNodeWithSize::from_raw(node)); @@ -1244,14 +1251,14 @@ impl Trie { &self, hash: &CryptoHash, ) -> Result<(Option>, TrieNodeWithSize), StorageError> { - match self.retrieve_raw_node(hash, true)? { + match self.retrieve_raw_node(hash, true, true)? { None => Ok((None, TrieNodeWithSize::empty())), Some((bytes, node)) => Ok((Some(bytes), TrieNodeWithSize::from_raw(node))), } } pub fn retrieve_root_node(&self) -> Result { - match self.retrieve_raw_node(&self.root, true)? { + match self.retrieve_raw_node(&self.root, true, true)? { None => Ok(StateRootNode::empty()), Some((bytes, node)) => { Ok(StateRootNode { data: bytes, memory_usage: node.memory_usage }) @@ -1277,16 +1284,17 @@ impl Trie { fn lookup_from_flat_storage( &self, key: &[u8], + side_effects: bool, ) -> Result, StorageError> { let flat_storage_chunk_view = self.flat_storage_chunk_view.as_ref().unwrap(); let value = flat_storage_chunk_view.get_value(key)?; - if self.recorder.is_some() { + if side_effects && self.recorder.is_some() { // If recording, we need to look up in the trie as well to record the trie nodes, // as they are needed to prove the value. Also, it's important that this lookup // is done even if the key was not found, because intermediate trie nodes may be // needed to prove the non-existence of the key. let value_ref_from_trie = - self.lookup_from_state_column(NibbleSlice::new(key), false)?; + self.lookup_from_state_column(NibbleSlice::new(key), false, side_effects)?; debug_assert_eq!( &value_ref_from_trie, &value.as_ref().map(|value| value.to_value_ref()) @@ -1305,10 +1313,15 @@ impl Trie { &self, mut key: NibbleSlice<'_>, charge_gas_for_trie_node_access: bool, + side_effects: bool, ) -> Result, StorageError> { let mut hash = self.root; loop { - let node = match self.retrieve_raw_node(&hash, charge_gas_for_trie_node_access)? { + let node = match self.retrieve_raw_node( + &hash, + charge_gas_for_trie_node_access, + side_effects, + )? { None => return Ok(None), Some((_bytes, node)) => node.node, }; @@ -1372,26 +1385,33 @@ impl Trie { &self, key: &[u8], charge_gas_for_trie_node_access: bool, + side_effects: bool, map_result: impl FnOnce(ValueView<'_>) -> R, ) -> Result, StorageError> { if self.root == Self::EMPTY_ROOT { return Ok(None); } - let mut accessed_nodes = Vec::new(); + let lock = self.memtries.as_ref().unwrap().read().unwrap(); - let mem_value = lock.lookup(&self.root, key, Some(&mut accessed_nodes))?; - if charge_gas_for_trie_node_access { - for (node_hash, serialized_node) in &accessed_nodes { - self.accounting_cache - .borrow_mut() - .retroactively_account(*node_hash, serialized_node.clone()); + let mem_value = if side_effects { + let mut accessed_nodes = Vec::new(); + let mem_value = lock.lookup(&self.root, key, Some(&mut accessed_nodes))?; + if charge_gas_for_trie_node_access { + for (node_hash, serialized_node) in &accessed_nodes { + self.accounting_cache + .borrow_mut() + .retroactively_account(*node_hash, serialized_node.clone()); + } } - } - if let Some(recorder) = &self.recorder { - for (node_hash, serialized_node) in accessed_nodes { - recorder.borrow_mut().record(&node_hash, serialized_node); + if let Some(recorder) = &self.recorder { + for (node_hash, serialized_node) in accessed_nodes { + recorder.borrow_mut().record(&node_hash, serialized_node); + } } - } + mem_value + } else { + lock.lookup(&self.root, key, None)? + }; Ok(mem_value.map(map_result)) } @@ -1425,7 +1445,7 @@ impl Trie { // The rest of the logic is very similar to the standard lookup() function, except // we return the raw node and don't expect to hit a leaf. - let mut node = self.retrieve_raw_node(&self.root, true)?; + let mut node = self.retrieve_raw_node(&self.root, true, true)?; while !key.is_empty() { match node { Some((_, raw_node)) => match raw_node.node { @@ -1437,7 +1457,7 @@ impl Trie { let child = children[key.at(0)]; match child { Some(child) => { - node = self.retrieve_raw_node(&child, true)?; + node = self.retrieve_raw_node(&child, true, true)?; key = key.mid(1); } None => return Ok(None), @@ -1446,7 +1466,7 @@ impl Trie { RawTrieNode::Extension(existing_key, child) => { let existing_key = NibbleSlice::from_encoded(&existing_key).0; if key.starts_with(&existing_key) { - node = self.retrieve_raw_node(&child, true)?; + node = self.retrieve_raw_node(&child, true, true)?; key = key.mid(existing_key.len()); } else { return Ok(None); @@ -1465,7 +1485,7 @@ impl Trie { /// Returns the raw bytes corresponding to a ValueRef that came from a node with /// value (either Leaf or BranchWithValue). pub fn retrieve_value(&self, hash: &CryptoHash) -> Result, StorageError> { - let bytes = self.internal_retrieve_trie_node(hash, true)?; + let bytes = self.internal_retrieve_trie_node(hash, true, true)?; Ok(bytes.to_vec()) } @@ -1486,7 +1506,7 @@ impl Trie { mode == KeyLookupMode::Trie || self.charge_gas_for_trie_node_access; if self.memtries.is_some() { return Ok(self - .lookup_from_memory(key, charge_gas_for_trie_node_access, |_| ())? + .lookup_from_memory(key, charge_gas_for_trie_node_access, true, |_| ())? .is_some()); } @@ -1500,14 +1520,14 @@ impl Trie { // is done even if the key was not found, because intermediate trie nodes may be // needed to prove the non-existence of the key. let value_ref_from_trie = - self.lookup_from_state_column(NibbleSlice::new(key), false)?; + self.lookup_from_state_column(NibbleSlice::new(key), false, true)?; debug_assert_eq!(&value_ref_from_trie.is_some(), &value); } return Ok(value); } Ok(self - .lookup_from_state_column(NibbleSlice::new(key), charge_gas_for_trie_node_access)? + .lookup_from_state_column(NibbleSlice::new(key), charge_gas_for_trie_node_access, true)? .is_some()) } @@ -1530,14 +1550,18 @@ impl Trie { let charge_gas_for_trie_node_access = mode == KeyLookupMode::Trie || self.charge_gas_for_trie_node_access; if self.memtries.is_some() { - self.lookup_from_memory(key, charge_gas_for_trie_node_access, |v| { + self.lookup_from_memory(key, charge_gas_for_trie_node_access, true, |v| { v.to_optimized_value_ref() }) } else if mode == KeyLookupMode::FlatStorage && self.flat_storage_chunk_view.is_some() { - self.lookup_from_flat_storage(key) + self.lookup_from_flat_storage(key, true) } else { Ok(self - .lookup_from_state_column(NibbleSlice::new(key), charge_gas_for_trie_node_access)? + .lookup_from_state_column( + NibbleSlice::new(key), + charge_gas_for_trie_node_access, + true, + )? .map(OptimizedValueRef::Ref)) } } @@ -1720,6 +1744,28 @@ impl TrieAccess for Trie { Trie::get(self, &key.to_vec()) } + fn get_no_side_effects(&self, key: &TrieKey) -> Result>, StorageError> { + let key = key.to_vec(); + let node = if self.memtries.is_some() { + self.lookup_from_memory(&key, false, false, |v| v.to_optimized_value_ref())? + } else if self.flat_storage_chunk_view.is_some() { + self.lookup_from_flat_storage(&key, false)? + } else { + self.lookup_from_state_column(NibbleSlice::new(&key), false, false)? + .map(OptimizedValueRef::Ref) + }; + match node { + Some(optimized_ref) => Ok(Some(match &optimized_ref { + OptimizedValueRef::Ref(value_ref) => { + let bytes = self.internal_retrieve_trie_node(&value_ref.hash, false, false)?; + bytes.to_vec() + } + OptimizedValueRef::AvailableValue(ValueAccessToken { value }) => value.clone(), + })), + None => Ok(None), + } + } + fn contains_key(&self, key: &TrieKey) -> Result { Trie::contains_key(&self, &key.to_vec()) } diff --git a/core/store/src/trie/receipts_column_helper.rs b/core/store/src/trie/receipts_column_helper.rs index 6cc8b884135..66f645037da 100644 --- a/core/store/src/trie/receipts_column_helper.rs +++ b/core/store/src/trie/receipts_column_helper.rs @@ -1,4 +1,4 @@ -use crate::{get, set, TrieAccess, TrieUpdate}; +use crate::{get, get_pure, set, TrieAccess, TrieUpdate}; use near_primitives::errors::{IntegerOverflowError, StorageError}; use near_primitives::receipt::{BufferedReceiptIndices, Receipt, TrieQueueIndices}; use near_primitives::trie_key::TrieKey; @@ -12,6 +12,7 @@ pub struct ReceiptIterator<'a> { indices: std::ops::Range, trie_queue: &'a dyn TrieQueue, trie: &'a dyn TrieAccess, + side_effects: bool, } /// Type safe access to delayed receipts queue stored in the state. Only use one @@ -144,15 +145,18 @@ pub trait TrieQueue { self.indices().len() } - fn iter<'a>(&'a self, trie: &'a dyn TrieAccess) -> ReceiptIterator<'a> + fn iter<'a>(&'a self, trie: &'a dyn TrieAccess, side_effects: bool) -> ReceiptIterator<'a> where Self: Sized, { - self.debug_check_unchanged(trie); + if side_effects { + self.debug_check_unchanged(trie); + } ReceiptIterator { indices: self.indices().first_index..self.indices().next_available_index, trie_queue: self, trie, + side_effects, } } @@ -256,7 +260,9 @@ impl<'a> Iterator for ReceiptIterator<'a> { fn next(&mut self) -> Option { let index = self.indices.next()?; let key = self.trie_queue.trie_key(index); - let result = match get(self.trie, &key) { + let value = + if self.side_effects { get(self.trie, &key) } else { get_pure(self.trie, &key) }; + let result = match value { Err(e) => Err(e), Ok(None) => Err(StorageError::StorageInconsistentState( "Receipt referenced by index should be in the state".to_owned(), @@ -271,7 +277,9 @@ impl<'a> DoubleEndedIterator for ReceiptIterator<'a> { fn next_back(&mut self) -> Option { let index = self.indices.next_back()?; let key = self.trie_queue.trie_key(index); - let result = match get(self.trie, &key) { + let value = + if self.side_effects { get(self.trie, &key) } else { get_pure(self.trie, &key) }; + let result = match value { Err(e) => Err(e), Ok(None) => Err(StorageError::StorageInconsistentState( "Receipt referenced by index should be in the state".to_owned(), @@ -405,7 +413,7 @@ mod tests { queue.push(trie, receipt).expect("pushing must not fail"); } let iterated_receipts: Vec = - queue.iter(trie).collect::>().expect("iterating should not fail"); + queue.iter(trie, true).collect::>().expect("iterating should not fail"); // check 1: receipts should be in queue and contained in the iterator assert_eq!(input_receipts, iterated_receipts, "receipts were not recorded in queue"); @@ -421,7 +429,7 @@ mod tests { ) { // check 2: assert newly loaded queue still contains the receipts let iterated_receipts: Vec = - queue.iter(trie).collect::>().expect("iterating should not fail"); + queue.iter(trie, true).collect::>().expect("iterating should not fail"); assert_eq!(input_receipts, iterated_receipts, "receipts were not persisted correctly"); // check 3: pop receipts from queue and check if all are returned in the right order diff --git a/core/store/src/trie/resharding_v2.rs b/core/store/src/trie/resharding_v2.rs index 60b2f6581d5..e0455893768 100644 --- a/core/store/src/trie/resharding_v2.rs +++ b/core/store/src/trie/resharding_v2.rs @@ -1,7 +1,7 @@ use crate::flat::FlatStateChanges; use crate::{ get, get_delayed_receipt_indices, get_promise_yield_indices, set, ShardTries, StoreUpdate, - Trie, TrieUpdate, + Trie, TrieAccess as _, TrieUpdate, }; use borsh::BorshDeserialize; use bytesize::ByteSize; diff --git a/core/store/src/trie/update.rs b/core/store/src/trie/update.rs index d5632e0a5b0..14d6eb7f279 100644 --- a/core/store/src/trie/update.rs +++ b/core/store/src/trie/update.rs @@ -1,41 +1,19 @@ pub use self::iterator::TrieUpdateIterator; use super::accounting_cache::TrieAccountingCacheSwitch; use super::{OptimizedValueRef, Trie, TrieWithReadLock}; +use crate::contract::ContractStorage; use crate::trie::{KeyLookupMode, TrieChanges}; -use crate::{StorageError, TrieStorage}; +use crate::StorageError; use near_primitives::hash::CryptoHash; use near_primitives::trie_key::TrieKey; use near_primitives::types::{ AccountId, RawStateChange, RawStateChanges, RawStateChangesWithTrieKey, StateChangeCause, StateRoot, TrieCacheMode, }; -use near_vm_runner::ContractCode; use std::collections::BTreeMap; -use std::sync::Arc; mod iterator; -/// Reads contract code from the trie by its hash. -/// Currently, uses `TrieStorage`. Consider implementing separate logic for -/// requesting and compiling contracts, as any contract code read and -/// compilation is a major bottleneck during chunk execution. -struct ContractStorage { - storage: Arc, -} - -impl ContractStorage { - fn new(storage: Arc) -> Self { - Self { storage } - } - - pub fn get(&self, code_hash: CryptoHash) -> Option { - match self.storage.retrieve_raw_bytes(&code_hash) { - Ok(raw_code) => Some(ContractCode::new(raw_code.to_vec(), Some(code_hash))), - Err(_) => None, - } - } -} - /// Key-value update. Contains a TrieKey and a value. pub struct TrieKeyValueUpdate { pub trie_key: TrieKey, @@ -49,7 +27,7 @@ pub type TrieUpdates = BTreeMap, TrieKeyValueUpdate>; /// TODO (#7327): rename to StateUpdate pub struct TrieUpdate { pub trie: Trie, - contract_storage: ContractStorage, + pub contract_storage: ContractStorage, committed: RawStateChanges, prospective: TrieUpdates, } @@ -124,18 +102,6 @@ impl TrieUpdate { self.trie.contains_key(&key) } - pub fn get(&self, key: &TrieKey) -> Result>, StorageError> { - let key = key.to_vec(); - if let Some(key_value) = self.prospective.get(&key) { - return Ok(key_value.value.as_ref().map(>::clone)); - } else if let Some(changes_with_trie_key) = self.committed.get(&key) { - if let Some(RawStateChange { data, .. }) = changes_with_trie_key.changes.last() { - return Ok(data.as_ref().map(>::clone)); - } - } - self.trie.get(&key) - } - /// Gets code from trie updates or directly from contract storage, /// bypassing the trie. pub fn get_code( @@ -275,11 +241,31 @@ impl TrieUpdate { } TrieCacheModeGuard(previous, switch) } + + fn get_from_updates( + &self, + key: &TrieKey, + fallback: impl FnOnce(&[u8]) -> Result>, StorageError>, + ) -> Result>, StorageError> { + let key = key.to_vec(); + if let Some(key_value) = self.prospective.get(&key) { + return Ok(key_value.value.as_ref().map(>::clone)); + } else if let Some(changes_with_trie_key) = self.committed.get(&key) { + if let Some(RawStateChange { data, .. }) = changes_with_trie_key.changes.last() { + return Ok(data.as_ref().map(>::clone)); + } + } + fallback(&key) + } } impl crate::TrieAccess for TrieUpdate { fn get(&self, key: &TrieKey) -> Result>, StorageError> { - TrieUpdate::get(self, key) + self.get_from_updates(key, |k| self.trie.get(k)) + } + + fn get_no_side_effects(&self, key: &TrieKey) -> Result>, StorageError> { + self.get_from_updates(key, |_| self.trie.get_no_side_effects(&key)) } fn contains_key(&self, key: &TrieKey) -> Result { @@ -298,7 +284,7 @@ impl Drop for TrieCacheModeGuard { mod tests { use super::*; use crate::test_utils::TestTriesBuilder; - use crate::ShardUId; + use crate::{ShardUId, TrieAccess as _}; use near_primitives::hash::CryptoHash; const SHARD_VERSION: u32 = 1; const COMPLEX_SHARD_UID: ShardUId = ShardUId { version: SHARD_VERSION, shard_id: 0 }; diff --git a/integration-tests/src/tests/client/features/fix_storage_usage.rs b/integration-tests/src/tests/client/features/fix_storage_usage.rs index 729c57a83fb..2a67fe9f2e9 100644 --- a/integration-tests/src/tests/client/features/fix_storage_usage.rs +++ b/integration-tests/src/tests/client/features/fix_storage_usage.rs @@ -5,7 +5,7 @@ use near_client::test_utils::TestEnv; use near_o11y::testonly::init_test_logger; use near_primitives::version::ProtocolFeature; use near_primitives::{trie_key::TrieKey, types::AccountId}; -use near_store::{ShardUId, TrieUpdate}; +use near_store::{ShardUId, TrieAccess, TrieUpdate}; use nearcore::test_utils::TestEnvNightshadeSetupExt; use crate::tests::client::process_blocks::set_block_protocol_version; diff --git a/integration-tests/src/tests/client/features/nearvm.rs b/integration-tests/src/tests/client/features/nearvm.rs index 93e2f4ae74c..5b24ab4b1c9 100644 --- a/integration-tests/src/tests/client/features/nearvm.rs +++ b/integration-tests/src/tests/client/features/nearvm.rs @@ -96,6 +96,14 @@ fn test_nearvm_upgrade() { capture.drain() }; - assert!(logs_at_old_version.iter().any(|l| l.contains(&"vm_kind=Wasmer2"))); - assert!(dbg!(logs_at_new_version).iter().any(|l| l.contains(&"vm_kind=NearVm"))); + assert!( + logs_at_old_version.iter().any(|l| l.contains(&"Wasmer2VM::run_method")), + "{:#?}", + logs_at_old_version + ); + assert!( + logs_at_new_version.iter().any(|l| l.contains(&"NearVM::run_method")), + "{:#?}", + logs_at_new_version + ); } diff --git a/runtime/near-vm-runner/src/near_vm_runner/runner.rs b/runtime/near-vm-runner/src/near_vm_runner/runner.rs index 7430c50f074..c2e2bbdd658 100644 --- a/runtime/near-vm-runner/src/near_vm_runner/runner.rs +++ b/runtime/near-vm-runner/src/near_vm_runner/runner.rs @@ -331,7 +331,7 @@ impl NearVM { mut import: NearVmImports<'_, '_, '_>, entrypoint: FunctionIndex, ) -> Result, VMRunnerError> { - let _span = tracing::debug_span!(target: "vm", "run_method").entered(); + let _span = tracing::debug_span!(target: "vm", "NearVM::run_method").entered(); // FastGasCounter in Nearcore must be reinterpret_cast-able to the one in NearVm. assert_eq!( @@ -349,7 +349,8 @@ impl NearVM { let gas = import.vmlogic.gas_counter().fast_counter_raw_ptr(); unsafe { let instance = { - let _span = tracing::debug_span!(target: "vm", "run_method/instantiate").entered(); + let _span = + tracing::debug_span!(target: "vm", "NearVM::run_method/instantiate").entered(); // An important caveat is that the `'static` lifetime here refers to the lifetime // of `VMLogic` reference to which is retained by the `InstanceHandle` we create. // However this `InstanceHandle` only lives during the execution of this body, so @@ -396,7 +397,7 @@ impl NearVM { handle }; if let Some(function) = instance.function_by_index(entrypoint) { - let _span = tracing::debug_span!(target: "vm", "run_method/call").entered(); + let _span = tracing::debug_span!(target: "vm", "NearVM::run_method/call").entered(); // Signature for the entry point should be `() -> ()`. This is only a sanity check // – this should've been already checked by `get_entrypoint_index`. let signature = artifact diff --git a/runtime/near-vm-runner/src/wasmer2_runner.rs b/runtime/near-vm-runner/src/wasmer2_runner.rs index 7957f55a18f..f7f9cafef78 100644 --- a/runtime/near-vm-runner/src/wasmer2_runner.rs +++ b/runtime/near-vm-runner/src/wasmer2_runner.rs @@ -400,7 +400,7 @@ impl Wasmer2VM { mut import: Wasmer2Imports<'_, '_, '_>, entrypoint: FunctionIndex, ) -> Result, VMRunnerError> { - let _span = tracing::debug_span!(target: "vm", "run_method").entered(); + let _span = tracing::debug_span!(target: "vm", "Wasmer2VM::run_method").entered(); // FastGasCounter in Nearcore and Wasmer must match in layout. assert_eq!(size_of::(), size_of::()); @@ -419,7 +419,8 @@ impl Wasmer2VM { let gas = import.vmlogic.gas_counter().fast_counter_raw_ptr(); unsafe { let instance = { - let _span = tracing::debug_span!(target: "vm", "run_method/instantiate").entered(); + let _span = tracing::debug_span!(target: "vm", "Wasmer2VM::run_method/instantiate") + .entered(); // An important caveat is that the `'static` lifetime here refers to the lifetime // of `VMLogic` reference to which is retained by the `InstanceHandle` we create. // However this `InstanceHandle` only lives during the execution of this body, so @@ -467,7 +468,8 @@ impl Wasmer2VM { handle }; if let Some(function) = instance.function_by_index(entrypoint) { - let _span = tracing::debug_span!(target: "vm", "run_method/call").entered(); + let _span = + tracing::debug_span!(target: "vm", "Wasmer2VM::run_method/call").entered(); // Signature for the entry point should be `() -> ()`. This is only a sanity check // – this should've been already checked by `get_entrypoint_index`. let signature = artifact @@ -502,7 +504,8 @@ impl Wasmer2VM { { let _span = - tracing::debug_span!(target: "vm", "run_method/drop_instance").entered(); + tracing::debug_span!(target: "vm", "Wasmer2VM::run_method/drop_instance") + .entered(); drop(instance) } } diff --git a/runtime/near-vm-runner/src/wasmer_runner.rs b/runtime/near-vm-runner/src/wasmer_runner.rs index f2680baf815..38172f2917e 100644 --- a/runtime/near-vm-runner/src/wasmer_runner.rs +++ b/runtime/near-vm-runner/src/wasmer_runner.rs @@ -262,10 +262,11 @@ fn run_method( import: &ImportObject, method_name: &str, ) -> Result, VMRunnerError> { - let _span = tracing::debug_span!(target: "vm", "run_method").entered(); + let _span = tracing::debug_span!(target: "vm", "Wasmer0VM::run_method").entered(); let instance = { - let _span = tracing::debug_span!(target: "vm", "run_method/instantiate").entered(); + let _span = + tracing::debug_span!(target: "vm", "Wasmer0VM::run_method/instantiate").entered(); match module.instantiate(import) { Ok(instance) => instance, Err(err) => { @@ -276,7 +277,7 @@ fn run_method( }; { - let _span = tracing::debug_span!(target: "vm", "run_method/call").entered(); + let _span = tracing::debug_span!(target: "vm", "Wasmer0VM::run_method/call").entered(); if let Err(err) = instance.call(method_name, &[]) { let guest_aborted = err.into_vm_error()?; return Ok(Err(guest_aborted)); @@ -284,7 +285,8 @@ fn run_method( } { - let _span = tracing::debug_span!(target: "vm", "run_method/drop_instance").entered(); + let _span = + tracing::debug_span!(target: "vm", "Wasmer0VM::run_method/drop_instance").entered(); drop(instance) } diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index ca9a0620903..678ab116d67 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -2,7 +2,7 @@ use crate::config::{ safe_add_compute, safe_add_gas, total_prepaid_exec_fees, total_prepaid_gas, total_prepaid_send_fees, }; -use crate::ext::{ExternalError, RuntimeContractExt, RuntimeExt}; +use crate::ext::{ExternalError, RuntimeExt}; use crate::receipt_manager::ReceiptManager; use crate::{metrics, ActionResult, ApplyState}; use near_crypto::PublicKey; @@ -38,8 +38,8 @@ use near_vm_runner::logic::errors::{ CompilationError, FunctionCallError, InconsistentStateError, VMRunnerError, }; use near_vm_runner::logic::{VMContext, VMOutcome}; -use near_vm_runner::ContractCode; use near_vm_runner::{precompile_contract, PreparedContract}; +use near_vm_runner::{ContractCode, ContractRuntimeCache}; use near_wallet_contract::{wallet_contract, wallet_contract_magic_bytes}; use std::sync::Arc; @@ -161,42 +161,6 @@ pub(crate) fn execute_function_call( Ok(outcome) } -pub(crate) fn prepare_function_call( - state_update: &TrieUpdate, - apply_state: &ApplyState, - account: &Account, - account_id: &AccountId, - function_call: &FunctionCallAction, - config: &RuntimeConfig, - view_config: Option, -) -> Box { - let max_gas_burnt = match view_config { - Some(ViewConfig { max_gas_burnt }) => max_gas_burnt, - None => config.wasm_config.limit_config.max_gas_burnt, - }; - let gas_counter = near_vm_runner::logic::GasCounter::new( - config.wasm_config.ext_costs.clone(), - max_gas_burnt, - config.wasm_config.regular_op_cost, - function_call.gas, - view_config.is_some(), - ); - let code_ext = RuntimeContractExt { - trie_update: state_update, - account_id, - account, - current_protocol_version: apply_state.current_protocol_version, - }; - let contract = near_vm_runner::prepare( - &code_ext, - Arc::clone(&config.wasm_config), - apply_state.cache.as_deref(), - gas_counter, - &function_call.method_name, - ); - contract -} - pub(crate) fn action_function_call( state_update: &mut TrieUpdate, apply_state: &ApplyState, @@ -644,7 +608,8 @@ pub(crate) fn action_deploy_contract( account: &mut Account, account_id: &AccountId, deploy_contract: &DeployContractAction, - apply_state: &ApplyState, + config: Arc, + cache: Option<&dyn ContractRuntimeCache>, ) -> Result<(), StorageError> { let _span = tracing::debug_span!(target: "runtime", "action_deploy_contract").entered(); let code = ContractCode::new(deploy_contract.code.clone(), None); @@ -660,16 +625,20 @@ pub(crate) fn action_deploy_contract( })?, ); account.set_code_hash(*code.hash()); + // Legacy: populate the mapping from `AccountId => sha256(code)` thus making contracts part of + // The State. For the time being we are also relying on the `TrieUpdate` to actually write the + // contracts into the storage as part of the commit routine, however no code should be relying + // that the contracts are written to The State. set_code(state_update, account_id.clone(), &code); - // Precompile the contract and store result (compiled code or error) in the database. - // Note, that contract compilation costs are already accounted in deploy cost using - // special logic in estimator (see get_runtime_config() function). - precompile_contract( - &code, - Arc::clone(&apply_state.config.wasm_config), - apply_state.cache.as_deref(), - ) - .ok(); + // Precompile the contract and store result (compiled code or error) in the contract runtime + // cache. + // Note, that contract compilation costs are already accounted in deploy cost using special + // logic in estimator (see get_runtime_config() function). + precompile_contract(&code, config, cache).ok(); + // Inform the `store::contract::Storage` about the new deploy (so that the `get` method can + // return the contract before the contract is written out to the underlying storage as part of + // the `TrieUpdate` commit.) + state_update.contract_storage.store(code); Ok(()) } @@ -1189,10 +1158,8 @@ mod tests { use near_primitives::action::delegate::NonDelegateAction; use near_primitives::congestion_info::BlockCongestionInfo; use near_primitives::errors::InvalidAccessKeyError; - use near_primitives::hash::hash; use near_primitives::runtime::migration_data::MigrationFlags; use near_primitives::transaction::CreateAccountAction; - use near_primitives::trie_key::TrieKey; use near_primitives::types::{EpochId, StateChangeCause}; use near_primitives_core::version::PROTOCOL_VERSION; use near_store::set_account; @@ -1354,11 +1321,25 @@ mod tests { let mut state_update = tries.new_trie_update(ShardUId::single_shard(), CryptoHash::default()); let account_id = "alice".parse::().unwrap(); - let trie_key = TrieKey::ContractCode { account_id: account_id.clone() }; - let empty_contract = [0; 10_000].to_vec(); - let contract_hash = hash(&empty_contract); - state_update.set(trie_key, empty_contract); - test_delete_large_account(&account_id, &contract_hash, storage_usage, &mut state_update) + let deploy_action = DeployContractAction { code: [0; 10_000].to_vec() }; + let mut account = + Account::new(100, 0, 0, CryptoHash::default(), storage_usage, PROTOCOL_VERSION); + let apply_state = create_apply_state(0); + let res = action_deploy_contract( + &mut state_update, + &mut account, + &account_id, + &deploy_action, + Arc::clone(&apply_state.config.wasm_config), + None, + ); + assert!(res.is_ok()); + test_delete_large_account( + &account_id, + &account.code_hash(), + storage_usage, + &mut state_update, + ) } #[test] diff --git a/runtime/runtime/src/balance_checker.rs b/runtime/runtime/src/balance_checker.rs index 0fed196bb3e..132562ae258 100644 --- a/runtime/runtime/src/balance_checker.rs +++ b/runtime/runtime/src/balance_checker.rs @@ -151,7 +151,7 @@ fn buffered_receipts( // in which case the final index can be 0 and the initial index larger. if let Some(num_forwarded) = after.first_index.checked_sub(before.first_index) { // The first n receipts were forwarded. - for receipt in initial_buffer.iter(initial_state).take(num_forwarded as usize) { + for receipt in initial_buffer.iter(initial_state, true).take(num_forwarded as usize) { forwarded_receipts.push(receipt?) } } @@ -159,7 +159,7 @@ fn buffered_receipts( after.next_available_index.checked_sub(before.next_available_index) { // The last n receipts are new. ("rev" to take from the back) - for receipt in final_buffer.iter(final_state).rev().take(num_buffered as usize) { + for receipt in final_buffer.iter(final_state, true).rev().take(num_buffered as usize) { new_buffered_receipts.push(receipt?); } } diff --git a/runtime/runtime/src/congestion_control.rs b/runtime/runtime/src/congestion_control.rs index cafa9886222..032eb871d1d 100644 --- a/runtime/runtime/src/congestion_control.rs +++ b/runtime/runtime/src/congestion_control.rs @@ -9,7 +9,7 @@ use near_primitives::receipt::{Receipt, ReceiptEnum}; use near_primitives::types::{EpochInfoProvider, Gas, ShardId}; use near_primitives::version::ProtocolFeature; use near_store::trie::receipts_column_helper::{ - DelayedReceiptQueue, ShardsOutgoingReceiptBuffer, TrieQueue, + DelayedReceiptQueue, ReceiptIterator, ShardsOutgoingReceiptBuffer, TrieQueue, }; use near_store::{StorageError, TrieAccess, TrieUpdate}; use near_vm_runner::logic::ProtocolVersion; @@ -194,7 +194,9 @@ impl ReceiptSinkV2<'_> { apply_state: &ApplyState, ) -> Result<(), RuntimeError> { let mut num_forwarded = 0; - for receipt_result in self.outgoing_buffers.to_shard(shard_id).iter(&state_update.trie) { + for receipt_result in + self.outgoing_buffers.to_shard(shard_id).iter(&state_update.trie, true) + { let receipt = receipt_result?; let bytes = receipt_size(&receipt)?; let gas = receipt_congestion_gas(&receipt, &apply_state.config)?; @@ -369,7 +371,7 @@ pub fn bootstrap_congestion_info( let mut buffered_receipts_gas: u128 = 0; let delayed_receipt_queue = &DelayedReceiptQueue::load(trie)?; - for receipt_result in delayed_receipt_queue.iter(trie) { + for receipt_result in delayed_receipt_queue.iter(trie, true) { let receipt = receipt_result?; let gas = receipt_congestion_gas(&receipt, config).map_err(int_overflow_to_storage_err)?; delayed_receipts_gas = @@ -381,7 +383,7 @@ pub fn bootstrap_congestion_info( let mut outgoing_buffers = ShardsOutgoingReceiptBuffer::load(trie)?; for shard in outgoing_buffers.shards() { - for receipt_result in outgoing_buffers.to_shard(shard).iter(trie) { + for receipt_result in outgoing_buffers.to_shard(shard).iter(trie, true) { let receipt = receipt_result?; let gas = receipt_congestion_gas(&receipt, config).map_err(int_overflow_to_storage_err)?; @@ -445,6 +447,10 @@ impl DelayedReceiptQueueWrapper { Ok(receipt) } + pub(crate) fn peek_iter<'a>(&'a self, trie_update: &'a TrieUpdate) -> ReceiptIterator<'a> { + self.queue.iter(trie_update, false) + } + pub(crate) fn len(&self) -> u64 { self.queue.len() } diff --git a/runtime/runtime/src/ext.rs b/runtime/runtime/src/ext.rs index dee2e18fd54..d45b170753e 100644 --- a/runtime/runtime/src/ext.rs +++ b/runtime/runtime/src/ext.rs @@ -6,9 +6,10 @@ use near_primitives::checked_feature; use near_primitives::errors::{EpochError, StorageError}; use near_primitives::hash::CryptoHash; use near_primitives::trie_key::{trie_key_parsers, TrieKey}; -use near_primitives::types::{AccountId, Balance, EpochId, EpochInfoProvider, Gas, TrieCacheMode}; +use near_primitives::types::{AccountId, Balance, EpochId, EpochInfoProvider, Gas}; use near_primitives::utils::create_receipt_id_from_action_hash; use near_primitives::version::ProtocolVersion; +use near_store::contract::ContractStorage; use near_store::{has_promise_yield_receipt, KeyLookupMode, TrieUpdate, TrieUpdateValuePtr}; use near_vm_runner::logic::errors::{AnyError, VMLogicError}; use near_vm_runner::logic::types::ReceiptIndex; @@ -362,9 +363,9 @@ impl<'a> External for RuntimeExt<'a> { } pub(crate) struct RuntimeContractExt<'a> { - pub(crate) trie_update: &'a TrieUpdate, + pub(crate) storage: ContractStorage, pub(crate) account_id: &'a AccountId, - pub(crate) account: &'a Account, + pub(crate) code_hash: CryptoHash, pub(crate) current_protocol_version: ProtocolVersion, } @@ -377,19 +378,17 @@ impl<'a> Contract for RuntimeContractExt<'a> { { // There are old eth implicit accounts without magic bytes in the code hash. // Result can be None and it's a valid option. See https://github.com/near/nearcore/pull/11606 - if let Some(wallet_contract) = wallet_contract(self.account.code_hash()) { + if let Some(wallet_contract) = wallet_contract(self.code_hash) { return *wallet_contract.hash(); } } - self.account.code_hash() + self.code_hash } fn get_code(&self) -> Option> { let account_id = self.account_id; - let code_hash = self.account.code_hash(); let version = self.current_protocol_version; - if checked_feature!("stable", EthImplicitAccounts, version) && account_id.get_account_type() == AccountType::EthImplicitAccount { @@ -398,16 +397,10 @@ impl<'a> Contract for RuntimeContractExt<'a> { // description of #11606) may have something else deployed to them. Only return // something here if the accounts have a wallet contract hash. Otherwise use the // regular path to grab the deployed contract. - if let Some(wc) = wallet_contract(code_hash) { + if let Some(wc) = wallet_contract(self.code_hash) { return Some(wc); } } - - let mode = match checked_feature!("stable", ChunkNodesCache, version) { - true => Some(TrieCacheMode::CachingShard), - false => None, - }; - let _guard = self.trie_update.with_trie_cache_mode(mode); - self.trie_update.get_code(self.account_id.clone(), code_hash).map(Arc::new) + self.storage.get(self.code_hash).map(Arc::new) } } diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index f22e2ec8b68..a581c6db6d0 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -52,10 +52,11 @@ use near_primitives::version::{ProtocolFeature, ProtocolVersion}; use near_primitives_core::apply::ApplyChunkReason; use near_store::trie::receipts_column_helper::DelayedReceiptQueue; use near_store::{ - get, get_account, get_postponed_receipt, get_promise_yield_receipt, get_received_data, - has_received_data, remove_account, remove_postponed_receipt, remove_promise_yield_receipt, set, - set_access_key, set_account, set_code, set_postponed_receipt, set_promise_yield_receipt, - set_received_data, PartialStorage, StorageError, Trie, TrieAccess, TrieChanges, TrieUpdate, + get, get_account, get_postponed_receipt, get_promise_yield_receipt, get_pure, + get_received_data, has_received_data, remove_account, remove_postponed_receipt, + remove_promise_yield_receipt, set, set_access_key, set_account, set_code, + set_postponed_receipt, set_promise_yield_receipt, set_received_data, PartialStorage, + StorageError, Trie, TrieAccess, TrieChanges, TrieUpdate, }; use near_vm_runner::logic::types::PromiseResult; use near_vm_runner::logic::ReturnData; @@ -63,6 +64,7 @@ pub use near_vm_runner::with_ext_cost_counter; use near_vm_runner::ContractCode; use near_vm_runner::ContractRuntimeCache; use near_vm_runner::ProfileDataV3; +use pipelining::ReceiptPreparationPipeline; use std::cmp::max; use std::collections::{HashMap, HashSet, VecDeque}; use std::sync::Arc; @@ -76,6 +78,7 @@ mod congestion_control; mod conversions; pub mod ext; mod metrics; +mod pipelining; mod prefetch; pub mod receipt_manager; pub mod state_viewer; @@ -367,6 +370,7 @@ impl Runtime { action: &Action, state_update: &mut TrieUpdate, apply_state: &ApplyState, + preparation_pipeline: &ReceiptPreparationPipeline, account: &mut Option, actor_id: &mut AccountId, receipt: &Receipt, @@ -432,18 +436,16 @@ impl Runtime { account.as_mut().expect(EXPECT_ACCOUNT_EXISTS), account_id, deploy_contract, - apply_state, + Arc::clone(&apply_state.config.wasm_config), + apply_state.cache.as_deref(), )?; } Action::FunctionCall(function_call) => { let account = account.as_mut().expect(EXPECT_ACCOUNT_EXISTS); - let contract = prepare_function_call( - state_update, - apply_state, - account, - account_id, - function_call, - &apply_state.config, + let contract = preparation_pipeline.get_contract( + receipt, + account.code_hash(), + action_index, None, ); let is_last_action = action_index + 1 == actions.len(); @@ -558,6 +560,7 @@ impl Runtime { &self, state_update: &mut TrieUpdate, apply_state: &ApplyState, + preparation_pipeline: &ReceiptPreparationPipeline, receipt: &Receipt, receipt_sink: &mut ReceiptSink, validator_proposals: &mut Vec, @@ -628,6 +631,7 @@ impl Runtime { action, state_update, apply_state, + preparation_pipeline, &mut account, &mut actor_id, receipt, @@ -975,14 +979,19 @@ impl Runtime { fn process_receipt( &self, - state_update: &mut TrieUpdate, - apply_state: &ApplyState, + processing_state: &mut ApplyProcessingReceiptState, receipt: &Receipt, receipt_sink: &mut ReceiptSink, validator_proposals: &mut Vec, - stats: &mut ApplyStats, - epoch_info_provider: &(dyn EpochInfoProvider), ) -> Result, RuntimeError> { + let ApplyProcessingReceiptState { + ref mut state_update, + apply_state, + epoch_info_provider, + ref pipeline_manager, + ref mut stats, + .. + } = *processing_state; let account_id = receipt.receiver_id(); match receipt.receipt() { ReceiptEnum::Data(ref data_receipt) => { @@ -1045,6 +1054,7 @@ impl Runtime { .apply_action_receipt( state_update, apply_state, + pipeline_manager, &ready_receipt, receipt_sink, validator_proposals, @@ -1099,6 +1109,7 @@ impl Runtime { .apply_action_receipt( state_update, apply_state, + pipeline_manager, receipt, receipt_sink, validator_proposals, @@ -1150,6 +1161,7 @@ impl Runtime { .apply_action_receipt( state_update, apply_state, + pipeline_manager, &yield_receipt, receipt_sink, validator_proposals, @@ -1575,24 +1587,22 @@ impl Runtime { compute_usage = tracing::field::Empty, ) .entered(); - let total = &mut processing_state.total; let state_update = &mut processing_state.state_update; let node_counter_before = state_update.trie().get_trie_nodes_count(); let recorded_storage_size_before = state_update.trie().recorded_storage_size(); let storage_proof_size_upper_bound_before = state_update.trie().recorded_storage_size_upper_bound(); let result = self.process_receipt( - state_update, - processing_state.apply_state, + processing_state, receipt, &mut receipt_sink, &mut validator_proposals, - &mut processing_state.stats, - processing_state.epoch_info_provider, ); + + let total = &mut processing_state.total; + let state_update = &mut processing_state.state_update; let node_counter_after = state_update.trie().get_trie_nodes_count(); tracing::trace!(target: "runtime", ?node_counter_before, ?node_counter_after); - let recorded_storage_diff = state_update .trie() .recorded_storage_size() @@ -1637,6 +1647,11 @@ impl Runtime { Ok(()) } + #[instrument(target = "runtime", level = "debug", "process_local_receipts", skip_all, fields( + num_receipts = processing_state.local_receipts.len(), + gas_burnt = tracing::field::Empty, + compute_usage = tracing::field::Empty, + ))] fn process_local_receipts<'a>( &self, mut processing_state: &mut ApplyProcessingReceiptState<'a>, @@ -1646,14 +1661,25 @@ impl Runtime { validator_proposals: &mut Vec, ) -> Result<(), RuntimeError> { let local_processing_start = std::time::Instant::now(); + let local_receipts = std::mem::take(&mut processing_state.local_receipts); let local_receipt_count = processing_state.local_receipts.len(); if let Some(prefetcher) = &mut processing_state.prefetcher { // Prefetcher is allowed to fail - let (front, back) = processing_state.local_receipts.as_slices(); + let (front, back) = local_receipts.as_slices(); _ = prefetcher.prefetch_receipts_data(front); _ = prefetcher.prefetch_receipts_data(back); } - while let Some(receipt) = processing_state.next_local_receipt() { + + let mut prep_lookahead_iter = local_receipts.iter(); + // Advance the preparation by one step (stagger it) so that we're preparing one interesting + // receipt in advance. + let mut next_schedule_after = schedule_contract_preparation( + &mut processing_state.pipeline_manager, + &processing_state.state_update, + &mut prep_lookahead_iter, + ); + + for receipt in local_receipts.iter() { if processing_state.total.compute >= compute_limit || proof_size_limit.is_some_and(|limit| { processing_state.state_update.trie.recorded_storage_size_upper_bound() > limit @@ -1665,6 +1691,19 @@ impl Runtime { &processing_state.apply_state.config, )?; } else { + if let Some(nsi) = &mut next_schedule_after { + *nsi = nsi.saturating_sub(1); + if *nsi == 0 { + // We're about to process a receipt that has been submitted for + // preparation, so lets submit the next one in anticipation that it might + // be processed too (it might also be not if we run out of gas/compute.) + next_schedule_after = schedule_contract_preparation( + &mut processing_state.pipeline_manager, + &processing_state.state_update, + &mut prep_lookahead_iter, + ); + } + } // NOTE: We don't need to validate the local receipt, because it's just validated in // the `verify_and_charge_transaction`. self.process_receipt_with_metrics( @@ -1675,6 +1714,10 @@ impl Runtime { )? } } + + let span = tracing::Span::current(); + span.record("gas_burnt", processing_state.total.gas); + span.record("compute_usage", processing_state.total.compute); processing_state.metrics.local_receipts_done( local_receipt_count as u64, local_processing_start.elapsed(), @@ -1684,6 +1727,11 @@ impl Runtime { Ok(()) } + #[instrument(target = "runtime", level = "debug", "process_delayed_receipts", skip_all, fields( + num_receipts = processing_state.delayed_receipts.len(), + gas_burnt = tracing::field::Empty, + compute_usage = tracing::field::Empty, + ))] fn process_delayed_receipts<'a>( &self, mut processing_state: &mut ApplyProcessingReceiptState<'a>, @@ -1696,6 +1744,17 @@ impl Runtime { let protocol_version = processing_state.protocol_version; let mut delayed_receipt_count = 0; let mut processed_delayed_receipts = vec![]; + + let mut prep_lookahead_iter = processing_state + .delayed_receipts + .peek_iter(&processing_state.state_update) + .map_while(Result::ok); + let mut next_schedule_after = schedule_contract_preparation( + &mut processing_state.pipeline_manager, + &processing_state.state_update, + &mut prep_lookahead_iter, + ); + while processing_state.delayed_receipts.len() > 0 { if processing_state.total.compute >= compute_limit || proof_size_limit.is_some_and(|limit| { @@ -1704,11 +1763,26 @@ impl Runtime { { break; } + delayed_receipt_count += 1; let receipt = processing_state .delayed_receipts .pop(&mut processing_state.state_update, &processing_state.apply_state.config)? .expect("queue is not empty"); + if let Some(nsi) = &mut next_schedule_after { + *nsi = nsi.saturating_sub(1); + if *nsi == 0 { + let mut prep_lookahead_iter = processing_state + .delayed_receipts + .peek_iter(&processing_state.state_update) + .map_while(Result::ok); + next_schedule_after = schedule_contract_preparation( + &mut processing_state.pipeline_manager, + &processing_state.state_update, + &mut prep_lookahead_iter, + ); + } + } if let Some(prefetcher) = &mut processing_state.prefetcher { // Prefetcher is allowed to fail @@ -1736,6 +1810,9 @@ impl Runtime { )?; processed_delayed_receipts.push(receipt); } + let span = tracing::Span::current(); + span.record("gas_burnt", processing_state.total.gas); + span.record("compute_usage", processing_state.total.compute); processing_state.metrics.delayed_receipts_done( delayed_receipt_count, delayed_processing_start.elapsed(), @@ -1746,6 +1823,11 @@ impl Runtime { Ok(processed_delayed_receipts) } + #[instrument(target = "runtime", level = "debug", "process_incoming_receipts", skip_all, fields( + num_receipts = processing_state.incoming_receipts.len(), + gas_burnt = tracing::field::Empty, + compute_usage = tracing::field::Empty, + ))] fn process_incoming_receipts<'a>( &self, mut processing_state: &mut ApplyProcessingReceiptState<'a>, @@ -1760,6 +1842,16 @@ impl Runtime { // Prefetcher is allowed to fail _ = prefetcher.prefetch_receipts_data(&processing_state.incoming_receipts); } + + let mut prep_lookahead_iter = processing_state.incoming_receipts.iter(); + // Advance the preparation by one step (stagger it) so that we're preparing one interesting + // receipt in advance. + let mut next_schedule_after = schedule_contract_preparation( + &mut processing_state.pipeline_manager, + &processing_state.state_update, + &mut prep_lookahead_iter, + ); + for receipt in processing_state.incoming_receipts.iter() { // Validating new incoming no matter whether we have available gas or not. We don't // want to store invalid receipts in state as delayed. @@ -1780,6 +1872,20 @@ impl Runtime { &processing_state.apply_state.config, )?; } else { + if let Some(nsi) = &mut next_schedule_after { + *nsi = nsi.saturating_sub(1); + if *nsi == 0 { + // We're about to process a receipt that has been submitted for + // preparation, so lets submit the next one in anticipation that it might + // be processed too (it might also be not if we run out of gas/compute.) + next_schedule_after = schedule_contract_preparation( + &mut processing_state.pipeline_manager, + &processing_state.state_update, + &mut prep_lookahead_iter, + ); + } + } + self.process_receipt_with_metrics( &receipt, &mut processing_state, @@ -1788,6 +1894,9 @@ impl Runtime { )?; } } + let span = tracing::Span::current(); + span.record("gas_burnt", processing_state.total.gas); + span.record("compute_usage", processing_state.total.compute); processing_state.metrics.incoming_receipts_done( processing_state.incoming_receipts.len() as u64, incoming_processing_start.elapsed(), @@ -2289,7 +2398,14 @@ impl<'a> ApplyProcessingState<'a> { incoming_receipts: &'a [Receipt], delayed_receipts: DelayedReceiptQueueWrapper, ) -> ApplyProcessingReceiptState<'a> { + let pipeline_manager = pipelining::ReceiptPreparationPipeline::new( + Arc::clone(&self.apply_state.config), + self.apply_state.cache.as_ref().map(|v| v.handle()), + self.apply_state.current_protocol_version, + self.state_update.contract_storage.clone(), + ); ApplyProcessingReceiptState { + pipeline_manager, protocol_version: self.protocol_version, apply_state: self.apply_state, prefetcher: self.prefetcher, @@ -2323,19 +2439,111 @@ struct ApplyProcessingReceiptState<'a> { local_receipts: VecDeque, incoming_receipts: &'a [Receipt], delayed_receipts: DelayedReceiptQueueWrapper, + pipeline_manager: pipelining::ReceiptPreparationPipeline, +} + +trait MaybeRefReceipt { + fn as_ref(&self) -> &Receipt; } -impl<'a> ApplyProcessingReceiptState<'a> { - /// Obtain the next receipt that should be executed. - fn next_local_receipt(&mut self) -> Option { - self.local_receipts.pop_front() +impl MaybeRefReceipt for Receipt { + fn as_ref(&self) -> &Receipt { + self } } +impl<'a> MaybeRefReceipt for &'a Receipt { + fn as_ref(&self) -> &Receipt { + *self + } +} + +/// Schedule a one receipt for contract preparation. +/// +/// The caller should call this method again after the returned number of receipts from `iterator` +/// are processed. +fn schedule_contract_preparation<'b, R: MaybeRefReceipt>( + pipeline_manager: &mut pipelining::ReceiptPreparationPipeline, + state_update: &TrieUpdate, + mut iterator: impl Iterator, +) -> Option { + let scheduled_receipt_offset = iterator.position(|peek| { + let peek = peek.as_ref(); + let account_id = peek.receiver_id(); + let key = TrieKey::Account { account_id: account_id.clone() }; + let receiver = get_pure::(state_update, &key); + let Ok(Some(receiver)) = receiver else { + // Most likely reason this can happen is because the receipt is for an account that + // does not yet exist. This is a routine occurrence as accounts are created by sending + // some NEAR to a name that's about to be created. + return false; + }; + + // We need to inspect each receipt recursively in case these are data receipts, thus a + // function. + fn handle_receipt( + mgr: &mut ReceiptPreparationPipeline, + state_update: &TrieUpdate, + receiver: &Account, + account_id: &AccountId, + receipt: &Receipt, + ) -> bool { + match receipt.receipt() { + ReceiptEnum::Action(_) | ReceiptEnum::PromiseYield(_) => { + // This returns `true` if work may have been scheduled (thus we currently + // prepare actions in at most 2 "interesting" receipts in parallel due to + // staggering.) + mgr.submit(receipt, &receiver, None) + } + ReceiptEnum::Data(dr) => { + let key = TrieKey::PostponedReceiptId { + receiver_id: account_id.clone(), + data_id: dr.data_id, + }; + let Ok(Some(rid)) = get_pure::(state_update, &key) else { + return false; + }; + let key = TrieKey::PendingDataCount { + receiver_id: account_id.clone(), + receipt_id: rid, + }; + let Ok(Some(data_count)) = get_pure::(state_update, &key) else { + return false; + }; + if data_count > 1 { + return false; + } + let key = TrieKey::PostponedReceipt { + receiver_id: account_id.clone(), + receipt_id: rid, + }; + let Ok(Some(pr)) = get_pure::(state_update, &key) else { + return false; + }; + return handle_receipt(mgr, state_update, receiver, account_id, &pr); + } + ReceiptEnum::PromiseResume(dr) => { + let key = TrieKey::PromiseYieldReceipt { + receiver_id: account_id.clone(), + data_id: dr.data_id, + }; + let Ok(Some(yr)) = get_pure::(state_update, &key) else { + return false; + }; + return handle_receipt(mgr, state_update, receiver, account_id, &yr); + } + } + } + handle_receipt(pipeline_manager, state_update, &receiver, account_id, peek) + })?; + Some(scheduled_receipt_offset.saturating_add(1)) +} + /// Interface provided for gas cost estimations. pub mod estimator { use super::{ReceiptSink, Runtime}; use crate::congestion_control::ReceiptSinkV2; + use crate::pipelining::ReceiptPreparationPipeline; use crate::{ApplyState, ApplyStats}; use near_primitives::congestion_info::CongestionInfo; use near_primitives::errors::RuntimeError; @@ -2367,9 +2575,16 @@ pub mod estimator { outgoing_buffers: ShardsOutgoingReceiptBuffer::load(&state_update.trie)?, outgoing_receipts, }); + let empty_pipeline = ReceiptPreparationPipeline::new( + std::sync::Arc::clone(&apply_state.config), + apply_state.cache.as_ref().map(|c| c.handle()), + apply_state.current_protocol_version, + state_update.contract_storage.clone(), + ); Runtime {}.apply_action_receipt( state_update, apply_state, + &empty_pipeline, receipt, &mut receipt_sink, validator_proposals, diff --git a/runtime/runtime/src/metrics.rs b/runtime/runtime/src/metrics.rs index ae46d57b8f3..182e8304bc1 100644 --- a/runtime/runtime/src/metrics.rs +++ b/runtime/runtime/src/metrics.rs @@ -1,10 +1,10 @@ use crate::congestion_control::ReceiptSink; use crate::ApplyState; use near_o11y::metrics::{ - exponential_buckets, linear_buckets, try_create_counter_vec, try_create_gauge_vec, - try_create_histogram_vec, try_create_int_counter, try_create_int_counter_vec, - try_create_int_gauge_vec, CounterVec, GaugeVec, HistogramVec, IntCounter, IntCounterVec, - IntGaugeVec, + exponential_buckets, linear_buckets, try_create_counter, try_create_counter_vec, + try_create_gauge_vec, try_create_histogram_vec, try_create_int_counter, + try_create_int_counter_vec, try_create_int_gauge_vec, Counter, CounterVec, GaugeVec, + HistogramVec, IntCounter, IntCounterVec, IntGaugeVec, }; use near_parameters::config::CongestionControlConfig; use near_primitives::congestion_info::CongestionInfo; @@ -442,6 +442,72 @@ pub(crate) static CHUNK_RECEIPTS_LIMITED_BY: LazyLock = LazyLock: .unwrap() }); +pub(crate) static PIPELINING_ACTIONS_SUBMITTED: LazyLock = LazyLock::new(|| { + try_create_int_counter( + "near_pipelininig_actions_submitted_count", + "Number of actions submitted to the pipeline for preparation.", + ) + .unwrap() +}); + +pub(crate) static PIPELINING_ACTIONS_PREPARED_IN_MAIN_THREAD: LazyLock = + LazyLock::new(|| { + try_create_int_counter( + "near_pipelininig_actions_prepared_in_main_thread_count", + "Number of actions prepared in the main thread, rather than the pipeline.", + ) + .unwrap() + }); + +pub(crate) static PIPELINING_ACTIONS_NOT_SUBMITTED: LazyLock = LazyLock::new(|| { + try_create_int_counter( + "near_pipelininig_actions_not_submitted_count", + "Number of actions prepared in the main thread, because they were never submitted.", + ) + .unwrap() +}); + +pub(crate) static PIPELINING_ACTIONS_FOUND_PREPARED: LazyLock = LazyLock::new(|| { + try_create_int_counter( + "near_pipelininig_actions_found_prepared_count", + "Number of actions that were found prepared by the time they were needed.", + ) + .unwrap() +}); + +pub(crate) static PIPELINING_ACTIONS_WAITING_TIME: LazyLock = LazyLock::new(|| { + try_create_counter( + "near_pipelininig_waiting_seconds_total", + "Time spent waiting for the task results to be ready.", + ) + .unwrap() +}); + +pub(crate) static PIPELINING_ACTIONS_MAIN_THREAD_WORKING_TIME: LazyLock = + LazyLock::new(|| { + try_create_counter( + "near_pipelininig_main_thread_seconds_total", + "Time spent preparing contracts on the main thread (for whatever reason.)", + ) + .unwrap() + }); + +pub(crate) static PIPELINING_ACTIONS_TASK_DELAY_TIME: LazyLock = LazyLock::new(|| { + try_create_counter( + "near_pipelininig_delay_seconds_total", + "Time spent waiting for the preparation task to be scheduled on thread pool.", + ) + .unwrap() +}); + +pub(crate) static PIPELINING_ACTIONS_TASK_WORKING_TIME: LazyLock = LazyLock::new(|| { + try_create_counter( + "near_pipelininig_working_seconds_total", + "Time spent working to produce the results for work scheduled on the pipeline.", + ) + .unwrap() +}); + /// Buckets used for burned gas in receipts. /// /// The maximum possible is 1300 Tgas for a full chunk. diff --git a/runtime/runtime/src/pipelining.rs b/runtime/runtime/src/pipelining.rs new file mode 100644 index 00000000000..13fa8d2ed68 --- /dev/null +++ b/runtime/runtime/src/pipelining.rs @@ -0,0 +1,341 @@ +use crate::ext::RuntimeContractExt; +use crate::metrics::{ + PIPELINING_ACTIONS_FOUND_PREPARED, PIPELINING_ACTIONS_MAIN_THREAD_WORKING_TIME, + PIPELINING_ACTIONS_NOT_SUBMITTED, PIPELINING_ACTIONS_PREPARED_IN_MAIN_THREAD, + PIPELINING_ACTIONS_SUBMITTED, PIPELINING_ACTIONS_TASK_DELAY_TIME, + PIPELINING_ACTIONS_TASK_WORKING_TIME, PIPELINING_ACTIONS_WAITING_TIME, +}; +use near_parameters::RuntimeConfig; +use near_primitives::account::Account; +use near_primitives::action::Action; +use near_primitives::config::ViewConfig; +use near_primitives::hash::CryptoHash; +use near_primitives::receipt::{Receipt, ReceiptEnum}; +use near_primitives::types::{AccountId, Gas}; +use near_store::contract::ContractStorage; +use near_vm_runner::logic::{GasCounter, ProtocolVersion}; +use near_vm_runner::{ContractRuntimeCache, PreparedContract}; +use std::collections::{BTreeMap, BTreeSet}; +use std::sync::{Arc, Condvar, Mutex}; +use std::time::Instant; + +pub(crate) struct ReceiptPreparationPipeline { + /// Mapping from a Receipt's ID to a parallel "task" to prepare the receipt's data. + /// + /// The task itself may be run in the current thread, a separate thread or forced in any other + /// way. + map: BTreeMap>, + + /// List of Receipt receiver IDs that must not be prepared for this chunk. + /// + /// This solves an issue wherein the pipelining implementation only has access to the committed + /// storage (read: data as a result of applying the previous chunk,) and not the state that has + /// been built up as a result of processing the current chunk. One notable thing that may have + /// occurred there is a contract deployment. Once that happens, we can no longer get the + /// "current" contract code for the account. + /// + /// However, even if we had access to the transaction of the current chunk and were able to + /// access the new code, there's a risk of a race between when the deployment is executed + /// and when a parallel preparation may occur, leading back to needing to hold prefetching of + /// that account's contracts until the deployment is executed. + /// + /// As deployments are a relatively rare event, it is probably just fine to entirely disable + /// pipelining for the account in question for that particular block. This field implements + /// exactly that. + /// + /// In the future, however, it may make sense to either move the responsibility of executing + /// deployment actions to this pipelining thingy OR, even better, modify the protocol such that + /// contract deployments in block N only take effect in the block N+1 as that, among other + /// things, would give the runtime more time to compile the contract. + block_accounts: BTreeSet, + + /// The Runtime config for these pipelining requests. + config: Arc, + + /// The contract cache. + contract_cache: Option>, + + /// Protocol version for this chunk. + protocol_version: u32, + + /// Storage for WASM code. + storage: ContractStorage, +} + +#[derive(PartialEq, Eq, PartialOrd, Ord)] +struct PrepareTaskKey { + receipt_id: CryptoHash, + action_index: usize, +} + +struct PrepareTask { + status: Mutex, + condvar: Condvar, +} + +enum PrepareTaskStatus { + Pending, + Working, + Prepared(Box), + Finished, +} + +impl ReceiptPreparationPipeline { + pub(crate) fn new( + config: Arc, + contract_cache: Option>, + protocol_version: u32, + storage: ContractStorage, + ) -> Self { + Self { + map: Default::default(), + block_accounts: Default::default(), + config, + contract_cache, + protocol_version, + storage, + } + } + + /// Submit a receipt to the "pipeline" for preparation of likely eventual execution. + /// + /// Note that not all receipts submitted here must be actually handled in some way. That said, + /// while it is perfectly fine to not use the results of submitted work (e.g. because a + /// applying a chunk ran out of gas or compute cost,) this work would eventually get lost, so + /// for the most part it is best to submit work with limited look-ahead. + /// + /// Returns `true` if the receipt is interesting and that pipelining has acted on it in some + /// way. Currently `true` is returned for any receipts containing `Action::DeployContract` (in + /// which case no further processing for the receiver account will be done), and + /// `Action::FunctionCall` (provided the account has not been blocked.) + pub(crate) fn submit( + &mut self, + receipt: &Receipt, + account: &Account, + view_config: Option, + ) -> bool { + let account_id = receipt.receiver_id(); + if self.block_accounts.contains(account_id) { + return false; + } + let actions = match receipt.receipt() { + ReceiptEnum::Action(a) | ReceiptEnum::PromiseYield(a) => &a.actions, + ReceiptEnum::Data(_) | ReceiptEnum::PromiseResume(_) => return false, + }; + let mut any_function_calls = false; + for (action_index, action) in actions.iter().enumerate() { + let account_id = account_id.clone(); + match action { + Action::DeployContract(_) => { + // FIXME: instead of blocking these accounts, move the handling of + // deploy action into here, so that the necessary data dependencies can be + // established. + return self.block_accounts.insert(account_id); + } + Action::FunctionCall(function_call) => { + let key = PrepareTaskKey { receipt_id: receipt.get_hash(), action_index }; + let gas_counter = self.gas_counter(view_config.as_ref(), function_call.gas); + let entry = match self.map.entry(key) { + std::collections::btree_map::Entry::Vacant(v) => v, + // Already been submitted. + // TODO: Warning? + std::collections::btree_map::Entry::Occupied(_) => continue, + }; + let config = Arc::clone(&self.config.wasm_config); + let cache = self.contract_cache.as_ref().map(|c| c.handle()); + let storage = self.storage.clone(); + let protocol_version = self.protocol_version; + let code_hash = account.code_hash(); + let created = Instant::now(); + let method_name = function_call.method_name.clone(); + let status = Mutex::new(PrepareTaskStatus::Pending); + let task = Arc::new(PrepareTask { status, condvar: Condvar::new() }); + entry.insert(Arc::clone(&task)); + PIPELINING_ACTIONS_SUBMITTED.inc_by(1); + rayon::spawn_fifo(move || { + let task_status = { + let mut status = task.status.lock().expect("mutex lock"); + std::mem::replace(&mut *status, PrepareTaskStatus::Working) + }; + let PrepareTaskStatus::Pending = task_status else { + return; + }; + PIPELINING_ACTIONS_TASK_DELAY_TIME.inc_by(created.elapsed().as_secs_f64()); + let start = Instant::now(); + let contract = prepare_function_call( + &storage, + cache.as_deref(), + protocol_version, + config, + gas_counter, + code_hash, + &account_id, + &method_name, + ); + + let mut status = task.status.lock().expect("mutex lock"); + *status = PrepareTaskStatus::Prepared(contract); + PIPELINING_ACTIONS_TASK_WORKING_TIME.inc_by(start.elapsed().as_secs_f64()); + task.condvar.notify_all(); + }); + any_function_calls = true; + } + // No need to handle this receipt as it only generates other new receipts. + Action::Delegate(_) => {} + // No handling for these. + Action::CreateAccount(_) + | Action::Transfer(_) + | Action::Stake(_) + | Action::AddKey(_) + | Action::DeleteKey(_) + | Action::DeleteAccount(_) => {} + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + Action::NonrefundableStorageTransfer(_) => {} + } + } + return any_function_calls; + } + + /// Obtain the prepared contract for the provided receipt. + /// + /// If the contract is currently being prepared this function will block waiting for the + /// preparation to complete. + /// + /// If the preparation hasn't been started yet (either because it hasn't been scheduled for any + /// reason, or because the pipeline didn't make it in time), this function will prepare the + /// contract in the calling thread. + pub(crate) fn get_contract( + &self, + receipt: &Receipt, + code_hash: CryptoHash, + action_index: usize, + view_config: Option, + ) -> Box { + let account_id = receipt.receiver_id(); + let action = match receipt.receipt() { + ReceiptEnum::Action(r) | ReceiptEnum::PromiseYield(r) => r + .actions + .get(action_index) + .expect("indexing receipt actions by an action_index failed!"), + ReceiptEnum::Data(_) | ReceiptEnum::PromiseResume(_) => { + panic!("attempting to get_contract with a non-action receipt!?") + } + }; + let Action::FunctionCall(function_call) = action else { + panic!("referenced receipt action is not a function call!"); + }; + let key = PrepareTaskKey { receipt_id: receipt.get_hash(), action_index }; + let Some(task) = self.map.get(&key) else { + let start = Instant::now(); + let gas_counter = self.gas_counter(view_config.as_ref(), function_call.gas); + if !self.block_accounts.contains(account_id) { + tracing::debug!( + target: "runtime::pipelining", + message="function call task was not submitted for preparation", + receipt=%receipt.get_hash(), + action_index, + ); + } + let result = prepare_function_call( + &self.storage, + self.contract_cache.as_deref(), + self.protocol_version, + Arc::clone(&self.config.wasm_config), + gas_counter, + code_hash, + &account_id, + &function_call.method_name, + ); + PIPELINING_ACTIONS_NOT_SUBMITTED.inc_by(1); + PIPELINING_ACTIONS_MAIN_THREAD_WORKING_TIME.inc_by(start.elapsed().as_secs_f64()); + return result; + }; + let mut status_guard = task.status.lock().unwrap(); + loop { + let current = std::mem::replace(&mut *status_guard, PrepareTaskStatus::Working); + match current { + PrepareTaskStatus::Pending => { + *status_guard = PrepareTaskStatus::Finished; + drop(status_guard); + let start = Instant::now(); + tracing::trace!( + target: "runtime::pipelining", + message="function call preparation on the main thread", + receipt=%receipt.get_hash(), + action_index + ); + let gas_counter = self.gas_counter(view_config.as_ref(), function_call.gas); + let cache = self.contract_cache.as_ref().map(|c| c.handle()); + let method_name = function_call.method_name.clone(); + let contract = prepare_function_call( + &self.storage, + cache.as_deref(), + self.protocol_version, + Arc::clone(&self.config.wasm_config), + gas_counter, + code_hash, + &account_id, + &method_name, + ); + PIPELINING_ACTIONS_PREPARED_IN_MAIN_THREAD.inc_by(1); + PIPELINING_ACTIONS_MAIN_THREAD_WORKING_TIME + .inc_by(start.elapsed().as_secs_f64()); + return contract; + } + PrepareTaskStatus::Working => { + let start = Instant::now(); + status_guard = task.condvar.wait(status_guard).unwrap(); + PIPELINING_ACTIONS_WAITING_TIME.inc_by(start.elapsed().as_secs_f64()); + continue; + } + PrepareTaskStatus::Prepared(c) => { + PIPELINING_ACTIONS_FOUND_PREPARED.inc_by(1); + *status_guard = PrepareTaskStatus::Finished; + return c; + } + PrepareTaskStatus::Finished => { + *status_guard = PrepareTaskStatus::Finished; + // Don't poison the lock. + drop(status_guard); + panic!("attempting to get_contract that has already been taken"); + } + } + } + } + + fn gas_counter(&self, view_config: Option<&ViewConfig>, gas: Gas) -> GasCounter { + let max_gas_burnt = match view_config { + Some(ViewConfig { max_gas_burnt }) => *max_gas_burnt, + None => self.config.wasm_config.limit_config.max_gas_burnt, + }; + GasCounter::new( + self.config.wasm_config.ext_costs.clone(), + max_gas_burnt, + self.config.wasm_config.regular_op_cost, + gas, + view_config.is_some(), + ) + } +} + +fn prepare_function_call( + contract_storage: &ContractStorage, + cache: Option<&dyn ContractRuntimeCache>, + + protocol_version: ProtocolVersion, + config: Arc, + gas_counter: GasCounter, + + code_hash: CryptoHash, + account_id: &AccountId, + method_name: &str, +) -> Box { + let code_ext = RuntimeContractExt { + storage: contract_storage.clone(), + account_id, + code_hash, + current_protocol_version: protocol_version, + }; + let contract = near_vm_runner::prepare(&code_ext, config, cache, gas_counter, method_name); + contract +} diff --git a/runtime/runtime/src/state_viewer/mod.rs b/runtime/runtime/src/state_viewer/mod.rs index 68e7e86bb7a..c2bb3147cfe 100644 --- a/runtime/runtime/src/state_viewer/mod.rs +++ b/runtime/runtime/src/state_viewer/mod.rs @@ -1,13 +1,14 @@ use crate::actions::execute_function_call; use crate::ext::RuntimeExt; +use crate::pipelining::ReceiptPreparationPipeline; use crate::receipt_manager::ReceiptManager; -use crate::{prepare_function_call, ApplyState}; +use crate::ApplyState; use near_crypto::{KeyType, PublicKey}; use near_parameters::RuntimeConfigStore; use near_primitives::account::{AccessKey, Account}; use near_primitives::borsh::BorshDeserialize; use near_primitives::hash::CryptoHash; -use near_primitives::receipt::ActionReceipt; +use near_primitives::receipt::{ActionReceipt, Receipt, ReceiptEnum, ReceiptV1}; use near_primitives::runtime::migration_data::{MigrationData, MigrationFlags}; use near_primitives::transaction::FunctionCallAction; use near_primitives::trie_key::trie_key_parsers; @@ -223,30 +224,36 @@ impl TrieViewer { migration_flags: MigrationFlags::default(), congestion_info: Default::default(), }; + let function_call = FunctionCallAction { + method_name: method_name.to_string(), + args: args.to_vec(), + gas: self.max_gas_burnt_view, + deposit: 0, + }; let action_receipt = ActionReceipt { signer_id: originator_id.clone(), signer_public_key: public_key, gas_price: 0, output_data_receivers: vec![], input_data_ids: vec![], - actions: vec![], - }; - let function_call = FunctionCallAction { - method_name: method_name.to_string(), - args: args.to_vec(), - gas: self.max_gas_burnt_view, - deposit: 0, + actions: vec![function_call.clone().into()], }; - let view_config = Some(ViewConfig { max_gas_burnt: self.max_gas_burnt_view }); - let contract = prepare_function_call( - &state_update, - &apply_state, - &account, - &contract_id, - &function_call, - config, - view_config.clone(), + let receipt = Receipt::V1(ReceiptV1 { + predecessor_id: contract_id.clone(), + receiver_id: contract_id.clone(), + receipt_id: empty_hash, + receipt: ReceiptEnum::Action(action_receipt.clone()), + priority: 0, + }); + let pipeline = ReceiptPreparationPipeline::new( + Arc::clone(config), + apply_state.cache.as_ref().map(|v| v.handle()), + apply_state.current_protocol_version, + state_update.contract_storage.clone(), ); + let view_config = Some(ViewConfig { max_gas_burnt: self.max_gas_burnt_view }); + let contract = pipeline.get_contract(&receipt, account.code_hash(), 0, view_config.clone()); + let mut runtime_ext = RuntimeExt::new( &mut state_update, &mut receipt_manager, From 7ad85a60a3decd6f772ed47c1faac8efc53f2202 Mon Sep 17 00:00:00 2001 From: Waclaw Banasik Date: Mon, 23 Sep 2024 17:07:32 +0100 Subject: [PATCH 07/49] fix(congestion) - Correctly handle receipt gas and size calculation in protocol upgrades (#12031) For now just a request for comments. There are still a few missing pieces like adding tests and gating this feature by a protocol feature. Please let me know if this approach makes sense on a high level. This PR addresses the issue described in #11923. Please have a look at that first for context. This is a hacky implementation of option 1: "Store the receipts together with the gas and size when added and use that when removing the receipt from buffer." The basic idea is that instead of storing `Receipt` directly in state, it can be wrapped in a `StateStoredReceipt` together with the metadata that is needed to correctly handle protocol upgrades. In order to migrate from the old way of storing receipts I'm using a rather hacky solution. I implemented a custom serialization and deserialization for the `StateStoredReceipt`. Using that customization it's possible to discriminate between serialized Receipt and serialized StateStoredReceipt. I added a helper struct `ReceiptOrStateStoredReceipt` and I made it so that both Receipt and StateStoredReceipt can be deserialized directly to it. How to differentiate between `Receipt` and `StateStoredReceipt` ? * Receipt::V0 has first two bytes in the form of [x, 0] - the second byte is zero. * Receipt::V1 has first two bytes in the form of [1, x] - where x != 0 - the first byte is one and second is non-zero. * Receipt::Vn (future) has first two bytes in the form of [n, x] - where x != 0 * StateStoredReceipt has first two bytes in the for of [T, T] where T is a magic tag == u8::MAX. The `StateStoredReceipt` can be told apart from Receipt::V0 by the second byte. The `StateStoredReceipt` can be told apart from Receipt::Vn by the first byte. Receipt::V0 and Receipt::V1 can be told apart by the second byte as described in https://github.com/near/nearcore/blob/0e5e7c7234952d1db8cbbafc984b7c8e6a1ac0ba/core/primitives/src/receipt.rs#L105-L118 How will the migration from `Receipt` to `StateStoredReceipt` happen? * In the old protocol version receipts will be stored as `Receipt` * In the new protocol version receipts will be stored as `StateStoredReceipt` * In both version receipts will be read as `ReceiptOrStateStoredReceipt`. This covers the fun case where receipts are stored in the old version and read in the new version. --- chain/client/src/test_utils/test_env.rs | 9 +- core/parameters/res/runtime_configs/72.yaml | 3 + .../res/runtime_configs/parameters.snap | 1 + .../res/runtime_configs/parameters.yaml | 2 + .../runtime_configs/parameters_testnet.yaml | 2 + core/parameters/src/config.rs | 5 + core/parameters/src/config_store.rs | 8 +- core/parameters/src/parameter.rs | 3 + core/parameters/src/parameter_table.rs | 1 + core/primitives-core/src/version.rs | 5 +- core/primitives/src/receipt.rs | 327 +++++++++++++++++- core/store/src/lib.rs | 6 +- core/store/src/trie/mem/mem_tries.rs | 2 +- core/store/src/trie/receipts_column_helper.rs | 30 +- .../client/features/congestion_control.rs | 127 ++++++- .../src/costs_to_runtime_config.rs | 1 + runtime/runtime/src/balance_checker.rs | 24 +- runtime/runtime/src/congestion_control.rs | 145 ++++++-- runtime/runtime/src/lib.rs | 16 +- runtime/runtime/src/tests/apply.rs | 6 +- .../res/protocol_schema.toml | 4 + tools/state-viewer/src/congestion_control.rs | 7 +- 22 files changed, 648 insertions(+), 86 deletions(-) diff --git a/chain/client/src/test_utils/test_env.rs b/chain/client/src/test_utils/test_env.rs index bfc5dc55cf1..4d612566f48 100644 --- a/chain/client/src/test_utils/test_env.rs +++ b/chain/client/src/test_utils/test_env.rs @@ -808,9 +808,12 @@ impl TestEnv { pub fn print_block_summary(&self, height: u64) { let client = &self.clients[0]; let block = client.chain.get_block_by_height(height); - let Ok(block) = block else { - tracing::info!(target: "test", "Block {}: missing", height); - return; + let block = match block { + Ok(block) => block, + Err(err) => { + tracing::info!(target: "test", ?err, "Block {}: missing", height); + return; + } }; let prev_hash = block.header().prev_hash(); let epoch_id = client.epoch_manager.get_epoch_id_from_prev_block(prev_hash).unwrap(); diff --git a/core/parameters/res/runtime_configs/72.yaml b/core/parameters/res/runtime_configs/72.yaml index 0ea523b8c76..857909848a1 100644 --- a/core/parameters/res/runtime_configs/72.yaml +++ b/core/parameters/res/runtime_configs/72.yaml @@ -1,4 +1,7 @@ main_storage_proof_size_soft_limit: {old: 3_000_000, new: 4_000_000} + +use_state_stored_receipt: { old: false, new: true } + # See https://github.com/near/nearcore/pull/12044 for why the values are set to these values. # In addition, `gas` is set to 1 for the large read variants, because we need that in actual code. # For this to be transparent for smart contracts, the `read_base` and `read_value_byte` values were diff --git a/core/parameters/res/runtime_configs/parameters.snap b/core/parameters/res/runtime_configs/parameters.snap index cae30235227..2162039ede0 100644 --- a/core/parameters/res/runtime_configs/parameters.snap +++ b/core/parameters/res/runtime_configs/parameters.snap @@ -218,3 +218,4 @@ allowed_shard_outgoing_gas 1_000_000_000_000_000 max_tx_gas 500_000_000_000_000 min_tx_gas 20_000_000_000_000 reject_tx_congestion_threshold 50 / 100 +use_state_stored_receipt true diff --git a/core/parameters/res/runtime_configs/parameters.yaml b/core/parameters/res/runtime_configs/parameters.yaml index cfe8a82291f..213a052acea 100644 --- a/core/parameters/res/runtime_configs/parameters.yaml +++ b/core/parameters/res/runtime_configs/parameters.yaml @@ -268,3 +268,5 @@ reject_tx_congestion_threshold: { numerator: 1, denominator: 1, } + +use_state_stored_receipt: false \ No newline at end of file diff --git a/core/parameters/res/runtime_configs/parameters_testnet.yaml b/core/parameters/res/runtime_configs/parameters_testnet.yaml index 0ab1a84e51b..c3f3876adf6 100644 --- a/core/parameters/res/runtime_configs/parameters_testnet.yaml +++ b/core/parameters/res/runtime_configs/parameters_testnet.yaml @@ -259,3 +259,5 @@ reject_tx_congestion_threshold: { numerator: 1, denominator: 1, } + +use_state_stored_receipt: false diff --git a/core/parameters/src/config.rs b/core/parameters/src/config.rs index 584b361d8ac..4c87c69cafe 100644 --- a/core/parameters/src/config.rs +++ b/core/parameters/src/config.rs @@ -31,6 +31,9 @@ pub struct RuntimeConfig { pub congestion_control_config: CongestionControlConfig, /// Configuration specific to ChunkStateWitness. pub witness_config: WitnessConfig, + + /// Whether receipts should be stored as [StateStoredReceipt]. + pub use_state_stored_receipt: bool, } impl RuntimeConfig { @@ -59,6 +62,7 @@ impl RuntimeConfig { account_creation_config: AccountCreationConfig::default(), congestion_control_config: runtime_config.congestion_control_config, witness_config: runtime_config.witness_config, + use_state_stored_receipt: runtime_config.use_state_stored_receipt, } } @@ -75,6 +79,7 @@ impl RuntimeConfig { account_creation_config: AccountCreationConfig::default(), congestion_control_config: runtime_config.congestion_control_config, witness_config: runtime_config.witness_config, + use_state_stored_receipt: runtime_config.use_state_stored_receipt, } } diff --git a/core/parameters/src/config_store.rs b/core/parameters/src/config_store.rs index bc9eccead76..a194030edd5 100644 --- a/core/parameters/src/config_store.rs +++ b/core/parameters/src/config_store.rs @@ -47,7 +47,7 @@ static CONFIG_DIFFS: &[(ProtocolVersion, &str)] = &[ (69, include_config!("69.yaml")), // Introduce ETH-implicit accounts. (70, include_config!("70.yaml")), - // Increase main_storage_proof_size_soft_limit + // Increase main_storage_proof_size_soft_limit and introduces StateStoredReceipt (72, include_config!("72.yaml")), (129, include_config!("129.yaml")), ]; @@ -119,6 +119,7 @@ impl RuntimeConfigStore { account_creation_config: runtime_config.account_creation_config.clone(), congestion_control_config: runtime_config.congestion_control_config, witness_config: runtime_config.witness_config, + use_state_stored_receipt: runtime_config.use_state_stored_receipt, }), ); store.insert(0, Arc::new(runtime_config.clone())); @@ -164,6 +165,11 @@ impl RuntimeConfigStore { Self { store: BTreeMap::from_iter([(0, Arc::new(runtime_config))].iter().cloned()) } } + /// Constructs store with custom configs. This should only be used for testing. + pub fn new_custom(store: BTreeMap>) -> Self { + Self { store } + } + /// Constructs test store. pub fn test() -> Self { Self::with_one_config(RuntimeConfig::test()) diff --git a/core/parameters/src/parameter.rs b/core/parameters/src/parameter.rs index 3ee7aa2a28c..1a359b1d55b 100644 --- a/core/parameters/src/parameter.rs +++ b/core/parameters/src/parameter.rs @@ -220,6 +220,9 @@ pub enum Parameter { MaxTxGas, MinTxGas, RejectTxCongestionThreshold, + + // Use the StateStoredReceipt structure when storing receipts in State. + UseStateStoredReceipt, } #[derive( diff --git a/core/parameters/src/parameter_table.rs b/core/parameters/src/parameter_table.rs index 5de6067106d..958948259ff 100644 --- a/core/parameters/src/parameter_table.rs +++ b/core/parameters/src/parameter_table.rs @@ -344,6 +344,7 @@ impl TryFrom<&ParameterTable> for RuntimeConfig { new_transactions_validation_state_size_soft_limit: params .get(Parameter::NewTransactionsValidationStateSizeSoftLimit)?, }, + use_state_stored_receipt: params.get(Parameter::UseStateStoredReceipt)?, }) } } diff --git a/core/primitives-core/src/version.rs b/core/primitives-core/src/version.rs index ec453a99284..63c16f50278 100644 --- a/core/primitives-core/src/version.rs +++ b/core/primitives-core/src/version.rs @@ -170,6 +170,8 @@ pub enum ProtocolFeature { // in order to calculate the rewards and kickouts for the chunk validators. // This feature introduces BlockHeaderV5. ChunkEndorsementsInBlockHeader, + /// Store receipts in State in the StateStoredReceipt format. + StateStoredReceipt, } impl ProtocolFeature { @@ -226,7 +228,8 @@ impl ProtocolFeature { ProtocolFeature::FixMinStakeRatio => 71, ProtocolFeature::IncreaseStorageProofSizeSoftLimit | ProtocolFeature::ChunkEndorsementV2 - | ProtocolFeature::ChunkEndorsementsInBlockHeader => 72, + | ProtocolFeature::ChunkEndorsementsInBlockHeader + | ProtocolFeature::StateStoredReceipt => 72, // This protocol version is reserved for use in resharding tests. An extra resharding // is simulated on top of the latest shard layout in production. Note that later diff --git a/core/primitives/src/receipt.rs b/core/primitives/src/receipt.rs index 218c14782ce..e5ae536445d 100644 --- a/core/primitives/src/receipt.rs +++ b/core/primitives/src/receipt.rs @@ -5,10 +5,11 @@ use crate::types::{AccountId, Balance, BlockHeight, ShardId}; use borsh::{BorshDeserialize, BorshSerialize}; use near_crypto::{KeyType, PublicKey}; use near_fmt::AbbrBytes; +use near_primitives_core::types::Gas; use near_schema_checker_lib::ProtocolSchema; use serde_with::base64::Base64; use serde_with::serde_as; -use std::borrow::Borrow; +use std::borrow::{Borrow, Cow}; use std::collections::{BTreeMap, HashMap}; use std::fmt; use std::io::{self, Read}; @@ -90,6 +91,109 @@ pub enum Receipt { V1(ReceiptV1), } +/// A receipt that is stored in the state with added metadata. A receipt may be +/// stored in the state as a delayed receipt, buffered receipt or a promise +/// yield receipt. The metadata contains additional information about receipt +/// +/// Please note that the StateStoredReceipt implements custom serialization and +/// deserialization. Please see the comment on [ReceiptOrStateStoredReceipt] +/// for more details. +/// +/// This struct is versioned so that it can be enhanced in the future. +#[derive(PartialEq, Eq, Debug, ProtocolSchema)] +pub enum StateStoredReceipt<'a> { + V0(StateStoredReceiptV0<'a>), +} + +/// The V0 of StateStoredReceipt. It contains the receipt and metadata. +#[derive(BorshDeserialize, BorshSerialize, PartialEq, Eq, Debug, ProtocolSchema)] +pub struct StateStoredReceiptV0<'a> { + /// The receipt. + pub receipt: Cow<'a, Receipt>, + pub metadata: StateStoredReceiptMetadata, +} + +/// The metadata associated with the receipt stored in state. +#[derive(BorshDeserialize, BorshSerialize, PartialEq, Eq, Debug, ProtocolSchema)] +pub struct StateStoredReceiptMetadata { + /// The congestion gas of the receipt when it was stored in the state. + /// Please see [compute_receipt_congestion_gas] for more details. + pub congestion_gas: Gas, + /// The congestion size of the receipt when it was stored in the state. + /// Please see [compute_receipt_size] for more details. + pub congestion_size: u64, +} + +/// The tag that is used to differentiate between the Receipt and StateStoredReceipt. +const STATE_STORED_RECEIPT_TAG: u8 = u8::MAX; + +/// This is a convenience struct for handling the migration from [Receipt] to +/// [StateStoredReceipt]. Both variants can be directly serialized and +/// deserialized to this struct. +/// +/// This structure is only meant as a migration vehicle and should not be used +/// for other purposes. In order to make any changes to how receipts are stored +/// in state the StateStoredReceipt should be used. It supports versioning. +/// +/// The receipt in both variants is stored as a Cow to allow for both owned and +/// borrowed ownership. The owned receipt should be used when pulling receipts +/// from the state. The borrowed ownership can be used when pushing receipts +/// into the state. In that case the receipt should never need to be cloned. The +/// serialization only needs a reference. +#[derive(PartialEq, Eq, Debug, ProtocolSchema)] +pub enum ReceiptOrStateStoredReceipt<'a> { + Receipt(Cow<'a, Receipt>), + StateStoredReceipt(StateStoredReceipt<'a>), +} + +impl ReceiptOrStateStoredReceipt<'_> { + pub fn into_receipt(self) -> Receipt { + match self { + ReceiptOrStateStoredReceipt::Receipt(receipt) => receipt.into_owned(), + ReceiptOrStateStoredReceipt::StateStoredReceipt(receipt) => receipt.into_receipt(), + } + } + + pub fn get_receipt(&self) -> &Receipt { + match self { + ReceiptOrStateStoredReceipt::Receipt(receipt) => receipt, + ReceiptOrStateStoredReceipt::StateStoredReceipt(receipt) => receipt.get_receipt(), + } + } +} + +impl<'a> StateStoredReceipt<'a> { + pub fn new_owned(receipt: Receipt, metadata: StateStoredReceiptMetadata) -> Self { + let receipt = Cow::Owned(receipt); + let v0 = StateStoredReceiptV0 { receipt, metadata }; + Self::V0(v0) + } + + pub fn new_borrowed(receipt: &'a Receipt, metadata: StateStoredReceiptMetadata) -> Self { + let receipt = Cow::Borrowed(receipt); + let v0 = StateStoredReceiptV0 { receipt, metadata }; + Self::V0(v0) + } + + pub fn into_receipt(self) -> Receipt { + match self { + StateStoredReceipt::V0(v0) => v0.receipt.into_owned(), + } + } + + pub fn get_receipt(&self) -> &Receipt { + match self { + StateStoredReceipt::V0(v0) => &v0.receipt, + } + } + + pub fn metadata(&self) -> &StateStoredReceiptMetadata { + match self { + StateStoredReceipt::V0(v0) => &v0.metadata, + } + } +} + impl BorshSerialize for Receipt { fn serialize(&self, writer: &mut W) -> io::Result<()> { match self { @@ -112,8 +216,8 @@ impl BorshDeserialize for Receipt { let u4 = u8::deserialize_reader(reader)?; // This is a ridiculous hackery: because the first field in `ReceiptV0` is an `AccountId` // and an account id is at most 64 bytes, for all valid `ReceiptV0` the second byte must be 0 - // because of the littel endian encoding of the length of the account id. - // On the other hand, for `ReceiptV0`, since the first byte is 1 and an account id must have nonzero + // because of the little endian encoding of the length of the account id. + // On the other hand, for `ReceiptV1`, since the first byte is 1 and an account id must have nonzero // length, so the second byte must not be zero. Therefore, we can distinguish between the two versions // by looking at the second byte. @@ -158,6 +262,100 @@ impl BorshDeserialize for Receipt { } } +impl BorshSerialize for StateStoredReceipt<'_> { + fn serialize(&self, writer: &mut W) -> io::Result<()> { + // The serialization format for StateStored receipt is as follows: + // Byte 1: STATE_STORED_RECEIPT_TAG + // Byte 2: STATE_STORED_RECEIPT_TAG + // Byte 3: enum version (e.g. V0 => 0_u8) + // serialized variant value + + BorshSerialize::serialize(&STATE_STORED_RECEIPT_TAG, writer)?; + BorshSerialize::serialize(&STATE_STORED_RECEIPT_TAG, writer)?; + match self { + StateStoredReceipt::V0(v0) => { + BorshSerialize::serialize(&0_u8, writer)?; + BorshSerialize::serialize(&v0, writer)?; + } + } + Ok(()) + } +} + +impl BorshDeserialize for StateStoredReceipt<'_> { + fn deserialize_reader(reader: &mut R) -> io::Result { + let u1 = u8::deserialize_reader(reader)?; + let u2 = u8::deserialize_reader(reader)?; + let u3 = u8::deserialize_reader(reader)?; + + if u1 != STATE_STORED_RECEIPT_TAG || u2 != STATE_STORED_RECEIPT_TAG { + let error = format!("Invalid tag found when deserializing StateStoredReceipt. Found: {}, {}. Expected: {}, {}", u1, u2, STATE_STORED_RECEIPT_TAG, STATE_STORED_RECEIPT_TAG); + let error = Error::new(ErrorKind::Other, error); + return Err(io::Error::new(ErrorKind::InvalidData, error)); + } + + match u3 { + 0 => { + let v0 = StateStoredReceiptV0::deserialize_reader(reader)?; + Ok(StateStoredReceipt::V0(v0)) + } + v => { + let error = format!("Invalid version found when deserializing StateStoredReceipt. Found: {}. Expected: 0", v); + let error = Error::new(ErrorKind::Other, error); + Err(io::Error::new(ErrorKind::InvalidData, error)) + } + } + } +} + +impl BorshSerialize for ReceiptOrStateStoredReceipt<'_> { + fn serialize(&self, writer: &mut W) -> io::Result<()> { + // This is custom serialization in order to provide backwards + // compatibility for migration from Receipt to StateStoredReceipt. + + // Please see the comment in deserialize_reader for more details. + match self { + ReceiptOrStateStoredReceipt::Receipt(receipt) => { + BorshSerialize::serialize(receipt, writer) + } + ReceiptOrStateStoredReceipt::StateStoredReceipt(receipt) => { + BorshSerialize::serialize(receipt, writer) + } + } + } +} + +impl BorshDeserialize for ReceiptOrStateStoredReceipt<'_> { + fn deserialize_reader(reader: &mut R) -> io::Result { + // This is custom deserialization in order to provide backwards + // compatibility for migration from Receipt to StateStoredReceipt. + + // Both variants (Receipt and StateStoredReceipt) need to be directly + // deserializable into the ReceiptOrStateStoredReceipt. + + // Read the first two bytes in order to discriminate between the Receipt + // and StateStoredReceipt. + // The StateStored receipt has the tag as the first two bytes. + // The Receipt::V0 has 0 as the second byte. + // The Receipt::V1 has 1 as the first byte. + let u1 = u8::deserialize_reader(reader)?; + let u2 = u8::deserialize_reader(reader)?; + + // Put the read bytes back into the reader by chaining. + let prefix = [u1, u2]; + let mut reader = prefix.chain(reader); + + if u1 == STATE_STORED_RECEIPT_TAG && u2 == STATE_STORED_RECEIPT_TAG { + let receipt = StateStoredReceipt::deserialize_reader(&mut reader)?; + Ok(ReceiptOrStateStoredReceipt::StateStoredReceipt(receipt)) + } else { + let receipt = Receipt::deserialize_reader(&mut reader)?; + let receipt = Cow::Owned(receipt); + Ok(ReceiptOrStateStoredReceipt::Receipt(receipt)) + } + } +} + pub enum ReceiptPriority { /// Used in ReceiptV1 Priority(u64), @@ -533,8 +731,7 @@ pub type ReceiptResult = HashMap>; mod tests { use super::*; - #[test] - fn test_receipt_v0_serialization() { + fn get_receipt_v0() -> Receipt { let receipt_v0 = Receipt::V0(ReceiptV0 { predecessor_id: "predecessor_id".parse().unwrap(), receiver_id: "receiver_id".parse().unwrap(), @@ -548,13 +745,10 @@ mod tests { actions: vec![Action::Transfer(TransferAction { deposit: 0 })], }), }); - let serialized_receipt = borsh::to_vec(&receipt_v0).unwrap(); - let receipt2 = Receipt::try_from_slice(&serialized_receipt).unwrap(); - assert_eq!(receipt_v0, receipt2); + receipt_v0 } - #[test] - fn test_receipt_v1_serialization() { + fn get_receipt_v1() -> Receipt { let receipt_v1 = Receipt::V1(ReceiptV1 { predecessor_id: "predecessor_id".parse().unwrap(), receiver_id: "receiver_id".parse().unwrap(), @@ -569,8 +763,121 @@ mod tests { }), priority: 1, }); + receipt_v1 + } + + #[test] + fn test_receipt_v0_serialization() { + let receipt_v0 = get_receipt_v0(); + let serialized_receipt = borsh::to_vec(&receipt_v0).unwrap(); + let receipt2 = Receipt::try_from_slice(&serialized_receipt).unwrap(); + assert_eq!(receipt_v0, receipt2); + } + + #[test] + fn test_receipt_v1_serialization() { + let receipt_v1 = get_receipt_v1(); let serialized_receipt = borsh::to_vec(&receipt_v1).unwrap(); let receipt2 = Receipt::try_from_slice(&serialized_receipt).unwrap(); assert_eq!(receipt_v1, receipt2); } + + fn test_state_stored_receipt_serialization_impl(receipt: Receipt) { + let metadata = StateStoredReceiptMetadata { congestion_gas: 42, congestion_size: 43 }; + let receipt = StateStoredReceipt::new_owned(receipt, metadata); + + let serialized_receipt = borsh::to_vec(&receipt).unwrap(); + let deserialized_receipt = StateStoredReceipt::try_from_slice(&serialized_receipt).unwrap(); + + assert_eq!(receipt, deserialized_receipt); + } + + #[test] + fn test_state_stored_receipt_serialization_v0() { + let receipt = get_receipt_v0(); + test_state_stored_receipt_serialization_impl(receipt); + } + + #[test] + fn test_state_stored_receipt_serialization_v1() { + let receipt = get_receipt_v1(); + test_state_stored_receipt_serialization_impl(receipt); + } + + #[test] + fn test_receipt_or_state_stored_receipt_serialization() { + // Case 1: + // Receipt V0 can be deserialized as ReceiptOrStateStoredReceipt + { + let receipt = get_receipt_v0(); + let receipt = Cow::Owned(receipt); + + let serialized_receipt = borsh::to_vec(&receipt).unwrap(); + let deserialized_receipt = + ReceiptOrStateStoredReceipt::try_from_slice(&serialized_receipt).unwrap(); + + assert_eq!(ReceiptOrStateStoredReceipt::Receipt(receipt), deserialized_receipt); + } + + // Case 2: + // Receipt V1 can be deserialized as ReceiptOrStateStoredReceipt + { + let receipt = get_receipt_v1(); + let receipt = Cow::Owned(receipt); + + let serialized_receipt = borsh::to_vec(&receipt).unwrap(); + let deserialized_receipt = + ReceiptOrStateStoredReceipt::try_from_slice(&serialized_receipt).unwrap(); + + assert_eq!(ReceiptOrStateStoredReceipt::Receipt(receipt), deserialized_receipt); + } + + // Case 3: + // StateStoredReceipt can be deserialized as ReceiptOrStateStoredReceipt + { + let receipt = get_receipt_v0(); + let metadata = StateStoredReceiptMetadata { congestion_gas: 42, congestion_size: 43 }; + let state_stored_receipt = StateStoredReceipt::new_owned(receipt, metadata); + + let serialized_receipt = borsh::to_vec(&state_stored_receipt).unwrap(); + let deserialized_receipt = + ReceiptOrStateStoredReceipt::try_from_slice(&serialized_receipt).unwrap(); + + assert_eq!( + ReceiptOrStateStoredReceipt::StateStoredReceipt(state_stored_receipt), + deserialized_receipt + ); + } + + // Case 4: + // ReceiptOrStateStoredReceipt::Receipt + { + let receipt = get_receipt_v0(); + let receipt = Cow::Owned(receipt); + + let receipt_or_state_stored_receipt = ReceiptOrStateStoredReceipt::Receipt(receipt); + + let serialized_receipt = borsh::to_vec(&receipt_or_state_stored_receipt).unwrap(); + let deserialized_receipt = + ReceiptOrStateStoredReceipt::try_from_slice(&serialized_receipt).unwrap(); + + assert_eq!(receipt_or_state_stored_receipt, deserialized_receipt); + } + + // Case 5: + // ReceiptOrStateStoredReceipt::StateStoredReceipt + { + let receipt = get_receipt_v0(); + let metadata = StateStoredReceiptMetadata { congestion_gas: 42, congestion_size: 43 }; + let state_stored_receipt = StateStoredReceipt::new_owned(receipt, metadata); + let receipt_or_state_stored_receipt = + ReceiptOrStateStoredReceipt::StateStoredReceipt(state_stored_receipt); + + let serialized_receipt = borsh::to_vec(&receipt_or_state_stored_receipt).unwrap(); + let deserialized_receipt = + ReceiptOrStateStoredReceipt::try_from_slice(&serialized_receipt).unwrap(); + + assert_eq!(receipt_or_state_stored_receipt, deserialized_receipt); + } + } } diff --git a/core/store/src/lib.rs b/core/store/src/lib.rs index e4444a504d9..d03e710d8c6 100644 --- a/core/store/src/lib.rs +++ b/core/store/src/lib.rs @@ -754,9 +754,9 @@ pub fn get( match trie.get(key)? { None => Ok(None), Some(data) => match T::try_from_slice(&data) { - Err(_err) => { - Err(StorageError::StorageInconsistentState("Failed to deserialize".to_string())) - } + Err(err) => Err(StorageError::StorageInconsistentState(format!( + "Failed to deserialize. err={err:?}" + ))), Ok(value) => Ok(Some(value)), }, } diff --git a/core/store/src/trie/mem/mem_tries.rs b/core/store/src/trie/mem/mem_tries.rs index f16959a6a4e..5bc292a2159 100644 --- a/core/store/src/trie/mem/mem_tries.rs +++ b/core/store/src/trie/mem/mem_tries.rs @@ -171,7 +171,7 @@ impl MemTries { } /// Returns an iterator over the memtrie for the given trie root. - pub fn get_iter<'a>(&'a self, trie: &'a Trie) -> Result { + pub fn get_iter<'a>(&'a self, trie: &'a Trie) -> Result, StorageError> { let root = if trie.root == CryptoHash::default() { None } else { diff --git a/core/store/src/trie/receipts_column_helper.rs b/core/store/src/trie/receipts_column_helper.rs index 66f645037da..b3918c58423 100644 --- a/core/store/src/trie/receipts_column_helper.rs +++ b/core/store/src/trie/receipts_column_helper.rs @@ -1,6 +1,8 @@ use crate::{get, get_pure, set, TrieAccess, TrieUpdate}; use near_primitives::errors::{IntegerOverflowError, StorageError}; -use near_primitives::receipt::{BufferedReceiptIndices, Receipt, TrieQueueIndices}; +use near_primitives::receipt::{ + BufferedReceiptIndices, ReceiptOrStateStoredReceipt, TrieQueueIndices, +}; use near_primitives::trie_key::TrieKey; use near_primitives::types::ShardId; @@ -77,7 +79,7 @@ pub trait TrieQueue { fn push( &mut self, state_update: &mut TrieUpdate, - receipt: &Receipt, + receipt: &ReceiptOrStateStoredReceipt, ) -> Result<(), IntegerOverflowError> { self.debug_check_unchanged(state_update); @@ -91,7 +93,10 @@ pub trait TrieQueue { Ok(()) } - fn pop(&mut self, state_update: &mut TrieUpdate) -> Result, StorageError> { + fn pop( + &mut self, + state_update: &mut TrieUpdate, + ) -> Result, StorageError> { self.debug_check_unchanged(state_update); let indices = self.indices(); @@ -99,7 +104,7 @@ pub trait TrieQueue { return Ok(None); } let key = self.trie_key(indices.first_index); - let receipt: Receipt = get(state_update, &key)?.ok_or_else(|| { + let receipt: ReceiptOrStateStoredReceipt = get(state_update, &key)?.ok_or_else(|| { StorageError::StorageInconsistentState(format!( "Receipt #{} should be in the state", indices.first_index @@ -255,7 +260,7 @@ impl TrieQueue for OutgoingReceiptBuffer<'_> { } impl<'a> Iterator for ReceiptIterator<'a> { - type Item = Result; + type Item = Result, StorageError>; fn next(&mut self) -> Option { let index = self.indices.next()?; @@ -292,9 +297,12 @@ impl<'a> DoubleEndedIterator for ReceiptIterator<'a> { #[cfg(test)] mod tests { + use std::borrow::Cow; + use super::*; use crate::test_utils::{gen_receipts, TestTriesBuilder}; use crate::Trie; + use near_primitives::receipt::Receipt; use near_primitives::shard_layout::ShardUId; #[test] @@ -410,10 +418,13 @@ mod tests { queue: &mut impl TrieQueue, ) { for receipt in input_receipts { - queue.push(trie, receipt).expect("pushing must not fail"); + let receipt = ReceiptOrStateStoredReceipt::Receipt(Cow::Borrowed(receipt)); + queue.push(trie, &receipt).expect("pushing must not fail"); } - let iterated_receipts: Vec = + let iterated_receipts: Vec = queue.iter(trie, true).collect::>().expect("iterating should not fail"); + let iterated_receipts: Vec = + iterated_receipts.into_iter().map(|receipt| receipt.into_receipt()).collect(); // check 1: receipts should be in queue and contained in the iterator assert_eq!(input_receipts, iterated_receipts, "receipts were not recorded in queue"); @@ -428,13 +439,16 @@ mod tests { queue: &mut impl TrieQueue, ) { // check 2: assert newly loaded queue still contains the receipts - let iterated_receipts: Vec = + let iterated_receipts: Vec = queue.iter(trie, true).collect::>().expect("iterating should not fail"); + let iterated_receipts: Vec = + iterated_receipts.into_iter().map(|receipt| receipt.into_receipt()).collect(); assert_eq!(input_receipts, iterated_receipts, "receipts were not persisted correctly"); // check 3: pop receipts from queue and check if all are returned in the right order let mut popped = vec![]; while let Some(receipt) = queue.pop(trie).expect("pop must not fail") { + let receipt = receipt.into_receipt(); popped.push(receipt); } assert_eq!(input_receipts, popped, "receipts were not popped correctly"); diff --git a/integration-tests/src/tests/client/features/congestion_control.rs b/integration-tests/src/tests/client/features/congestion_control.rs index ed976fdf819..225469168bd 100644 --- a/integration-tests/src/tests/client/features/congestion_control.rs +++ b/integration-tests/src/tests/client/features/congestion_control.rs @@ -25,17 +25,36 @@ use std::sync::Arc; const CONTRACT_ID: &str = "contract.test0"; -fn setup_runtime(sender_id: AccountId, protocol_version: ProtocolVersion) -> TestEnv { +fn get_runtime_config( + config_store: &RuntimeConfigStore, + protocol_version: ProtocolVersion, +) -> Arc { + let mut config = config_store.get_config(protocol_version).clone(); + let mut_config = Arc::make_mut(&mut config); + + adjust_runtime_config(mut_config); + + config +} + +// Make 1 wasm op cost ~4 GGas, to let "loop_forever" finish more quickly. +fn adjust_runtime_config(config: &mut RuntimeConfig) { + let wasm_config = Arc::make_mut(&mut config.wasm_config); + wasm_config.regular_op_cost = u32::MAX; +} + +/// Set up the test runtime with the given protocol version and runtime configs. +/// The test version of runtime has custom gas cost. +fn setup_test_runtime(sender_id: AccountId, protocol_version: ProtocolVersion) -> TestEnv { let mut genesis = Genesis::test_sharded_new_version(vec![sender_id], 1, vec![1, 1, 1, 1]); genesis.config.epoch_length = 10; genesis.config.protocol_version = protocol_version; + // Chain must be sharded to test cross-shard congestion control. genesis.config.shard_layout = ShardLayout::v1_test(); let mut config = RuntimeConfig::test(); - // Make 1 wasm op cost ~4 GGas, to let "loop_forever" finish more quickly. - let wasm_config = Arc::make_mut(&mut config.wasm_config); - wasm_config.regular_op_cost = u32::MAX; + adjust_runtime_config(&mut config); let runtime_configs = vec![RuntimeConfigStore::with_one_config(config)]; TestEnv::builder(&genesis.config) @@ -43,6 +62,36 @@ fn setup_runtime(sender_id: AccountId, protocol_version: ProtocolVersion) -> Tes .build() } +/// Set up the real runtime with the given protocol version and runtime configs. +/// This runtime is suitable for testing protocol upgrade and the migration from +/// Receipt to StateStoredReceipt. +fn setup_real_runtime(sender_id: AccountId, protocol_version: ProtocolVersion) -> TestEnv { + let mut genesis = Genesis::test_sharded_new_version(vec![sender_id], 1, vec![1, 1, 1, 1]); + genesis.config.epoch_length = 10; + genesis.config.protocol_version = protocol_version; + + // Chain must be sharded to test cross-shard congestion control. + genesis.config.shard_layout = ShardLayout::v1_test(); + + let config_store = RuntimeConfigStore::new(None); + let pre_config = get_runtime_config(&config_store, protocol_version); + let post_config = get_runtime_config(&config_store, PROTOCOL_VERSION); + + // Checking the migration from Receipt to StateStoredReceipt requires the + // relevant config to be disabled before the protocol upgrade and enabled + // after the protocol upgrade. + assert!(false == pre_config.use_state_stored_receipt); + assert!(true == post_config.use_state_stored_receipt); + + let runtime_configs = vec![RuntimeConfigStore::new_custom( + [(protocol_version, pre_config), (PROTOCOL_VERSION, post_config)].into_iter().collect(), + )]; + + TestEnv::builder(&genesis.config) + .nightshade_runtimes_with_runtime_config_store(&genesis, runtime_configs) + .build() +} + /// Set up the RS-Contract, which includes useful functions, such as /// `loop_forever`. /// @@ -163,7 +212,7 @@ fn test_protocol_upgrade_simple() { return; } - let mut env = setup_runtime( + let mut env = setup_real_runtime( "test0".parse().unwrap(), ProtocolFeature::CongestionControl.protocol_version() - 1, ); @@ -235,16 +284,18 @@ fn test_protocol_upgrade_under_congestion() { } let sender_id: AccountId = "test0".parse().unwrap(); - let mut env = - setup_runtime(sender_id.clone(), ProtocolFeature::CongestionControl.protocol_version() - 1); + let mut env = setup_real_runtime( + sender_id.clone(), + ProtocolFeature::CongestionControl.protocol_version() - 1, + ); // prepare a contract to call setup_contract(&mut env); let signer = InMemorySigner::from_seed(sender_id.clone(), KeyType::ED25519, sender_id.as_str()); let mut nonce = 10; - // Now, congest the network with ~1000 Pgas, enough to have some left after the protocol upgrade. - let n = 10000; + // Now, congest the network with ~100 Pgas, enough to have some left after the protocol upgrade. + let n = 1000; submit_n_100tgas_fns(&mut env, n, &mut nonce, &signer); // Allow transactions to enter the chain @@ -301,6 +352,36 @@ fn test_protocol_upgrade_under_congestion() { let check_congested_protocol_upgrade = true; check_congestion_info(&env, check_congested_protocol_upgrade); + + // Test the migration from Receipt to StateStoredReceipt + + // Wait until chain is no longer congested + let tip = env.clients[0].chain.head().unwrap(); + for i in 1.. { + let block = env.clients[0].produce_block(tip.height + i); + let block = block.unwrap().unwrap(); + let gas_used = block.chunks().get(contract_shard_id as usize).unwrap().prev_gas_used(); + + env.process_block(0, block, Provenance::PRODUCED); + + if gas_used == 0 { + break; + } + } + + // Submit some more transactions that should now be stored as StateStoredReceipts. + let included = submit_n_100tgas_fns(&mut env, n, &mut nonce, &signer); + assert!(included > 0); + + // Allow transactions to enter the chain and be processed. At this point the + // receipts will be stored and retrieved using the StateStoredReceipt + // structure. + let tip = env.clients[0].chain.head().unwrap(); + for i in 1..10 { + env.produce_block(0, tip.height + i); + } + + // The summary may be incomplete because of GC. env.print_summary(); } @@ -374,18 +455,27 @@ fn new_cheap_fn_call( /// Submit N transaction containing a function call action with 100 Tgas /// attached that will all be burned when called. -fn submit_n_100tgas_fns(env: &mut TestEnv, n: u32, nonce: &mut u64, signer: &InMemorySigner) { +fn submit_n_100tgas_fns( + env: &mut TestEnv, + n: u32, + nonce: &mut u64, + signer: &InMemorySigner, +) -> u32 { + let mut included = 0; let block = env.clients[0].chain.get_head_block().unwrap(); for _ in 0..n { let fn_tx = new_fn_call_100tgas(nonce, signer, *block.hash()); // this only adds the tx to the pool, no chain progress is made let response = env.clients[0].process_tx(fn_tx, false, false); match response { - ProcessTxResponse::ValidTx - | ProcessTxResponse::InvalidTx(InvalidTxError::ShardCongested { .. }) => (), + ProcessTxResponse::ValidTx => { + included += 1; + } + ProcessTxResponse::InvalidTx(InvalidTxError::ShardCongested { .. }) => (), other => panic!("unexpected result from submitting tx: {other:?}"), } } + included } /// Submit N transaction containing a cheap function call action. @@ -430,7 +520,7 @@ fn test_transaction_limit_for_local_congestion() { let contract_id: AccountId = CONTRACT_ID.parse().unwrap(); let sender_id = contract_id.clone(); let dummy_receiver: AccountId = "a_dummy_receiver".parse().unwrap(); - let env = setup_runtime("test0".parse().unwrap(), PROTOCOL_VERSION); + let env = setup_test_runtime("test0".parse().unwrap(), PROTOCOL_VERSION); let ( remote_tx_included_without_congestion, @@ -502,10 +592,10 @@ fn test_transaction_limit_for_remote_congestion() { } /// Test that clients stop including transactions to fully congested receivers. -/// -/// #[test] fn test_transaction_filtering() { + init_test_logger(); + if !ProtocolFeature::CongestionControl.enabled(PROTOCOL_VERSION) { return; } @@ -540,7 +630,7 @@ fn measure_remote_tx_limit(upper_limit_congestion: f64) -> (usize, usize, usize, let remote_id: AccountId = "test0".parse().unwrap(); let contract_id: AccountId = CONTRACT_ID.parse().unwrap(); let dummy_id: AccountId = "a_dummy_receiver".parse().unwrap(); - let env = setup_runtime(remote_id.clone(), PROTOCOL_VERSION); + let env = setup_test_runtime(remote_id.clone(), PROTOCOL_VERSION); let tip = env.clients[0].chain.head().unwrap(); let remote_shard_id = @@ -608,7 +698,8 @@ fn measure_tx_limit( let mut remote_tx_included_without_congestion = 0; let mut local_tx_included_without_congestion = 0; for i in 2..timeout { - env.produce_block(0, tip.height + i); + let height = tip.height + i; + env.produce_block(0, height); let sender_chunk = head_chunk(&env, remote_shard_id); let contract_chunk = head_chunk(&env, contract_shard_id); @@ -663,7 +754,7 @@ fn measure_tx_limit( #[test] fn test_rpc_client_rejection() { let sender_id: AccountId = "test0".parse().unwrap(); - let mut env = setup_runtime(sender_id.clone(), PROTOCOL_VERSION); + let mut env = setup_test_runtime(sender_id.clone(), PROTOCOL_VERSION); // prepare a contract to call setup_contract(&mut env); diff --git a/runtime/runtime-params-estimator/src/costs_to_runtime_config.rs b/runtime/runtime-params-estimator/src/costs_to_runtime_config.rs index 6bff5f77735..e37eec6a193 100644 --- a/runtime/runtime-params-estimator/src/costs_to_runtime_config.rs +++ b/runtime/runtime-params-estimator/src/costs_to_runtime_config.rs @@ -39,6 +39,7 @@ pub fn costs_to_runtime_config(cost_table: &CostTable) -> anyhow::Result Result, StorageError> { indexes .map(|index| { - get(state, &TrieKey::DelayedReceipt { index })?.ok_or_else(|| { - StorageError::StorageInconsistentState(format!( - "Delayed receipt #{} should be in the state", - index - )) - }) + let receipt: Result = + get(state, &TrieKey::DelayedReceipt { index })?.ok_or_else(|| { + StorageError::StorageInconsistentState(format!( + "Delayed receipt #{} should be in the state", + index + )) + }); + receipt.map(|receipt| receipt.into_receipt()) }) .collect() } @@ -152,7 +154,9 @@ fn buffered_receipts( if let Some(num_forwarded) = after.first_index.checked_sub(before.first_index) { // The first n receipts were forwarded. for receipt in initial_buffer.iter(initial_state, true).take(num_forwarded as usize) { - forwarded_receipts.push(receipt?) + let receipt = receipt?; + let receipt = receipt.into_receipt(); + forwarded_receipts.push(receipt) } } if let Some(num_buffered) = @@ -160,7 +164,9 @@ fn buffered_receipts( { // The last n receipts are new. ("rev" to take from the back) for receipt in final_buffer.iter(final_state, true).rev().take(num_buffered as usize) { - new_buffered_receipts.push(receipt?); + let receipt = receipt?; + let receipt = receipt.into_receipt(); + new_buffered_receipts.push(receipt) } } } diff --git a/runtime/runtime/src/congestion_control.rs b/runtime/runtime/src/congestion_control.rs index 032eb871d1d..dbbd1228a20 100644 --- a/runtime/runtime/src/congestion_control.rs +++ b/runtime/runtime/src/congestion_control.rs @@ -5,7 +5,10 @@ use crate::ApplyState; use near_parameters::{ActionCosts, RuntimeConfig}; use near_primitives::congestion_info::{CongestionControl, CongestionInfo, CongestionInfoV1}; use near_primitives::errors::{IntegerOverflowError, RuntimeError}; -use near_primitives::receipt::{Receipt, ReceiptEnum}; +use near_primitives::receipt::{ + Receipt, ReceiptEnum, ReceiptOrStateStoredReceipt, StateStoredReceipt, + StateStoredReceiptMetadata, +}; use near_primitives::types::{EpochInfoProvider, Gas, ShardId}; use near_primitives::version::ProtocolFeature; use near_store::trie::receipts_column_helper::{ @@ -13,6 +16,7 @@ use near_store::trie::receipts_column_helper::{ }; use near_store::{StorageError, TrieAccess, TrieUpdate}; use near_vm_runner::logic::ProtocolVersion; +use std::borrow::Cow; use std::collections::HashMap; /// Handle receipt forwarding for different protocol versions. @@ -198,17 +202,21 @@ impl ReceiptSinkV2<'_> { self.outgoing_buffers.to_shard(shard_id).iter(&state_update.trie, true) { let receipt = receipt_result?; - let bytes = receipt_size(&receipt)?; let gas = receipt_congestion_gas(&receipt, &apply_state.config)?; + let size = receipt_size(&receipt)?; + let receipt = receipt.into_receipt(); + match Self::try_forward( receipt, + gas, + size, shard_id, &mut self.outgoing_limit, self.outgoing_receipts, apply_state, )? { ReceiptForwarding::Forwarded => { - self.own_congestion_info.remove_receipt_bytes(bytes as u64)?; + self.own_congestion_info.remove_receipt_bytes(size)?; self.own_congestion_info.remove_buffered_receipt_gas(gas)?; // count how many to release later to avoid modifying // `state_update` while iterating based on @@ -236,8 +244,14 @@ impl ReceiptSinkV2<'_> { ) -> Result<(), RuntimeError> { let shard = epoch_info_provider .account_id_to_shard_id(receipt.receiver_id(), &apply_state.epoch_id)?; + + let size = compute_receipt_size(&receipt)?; + let gas = compute_receipt_congestion_gas(&receipt, &apply_state.config)?; + match Self::try_forward( receipt, + gas, + size, shard, &mut self.outgoing_limit, self.outgoing_receipts, @@ -245,7 +259,14 @@ impl ReceiptSinkV2<'_> { )? { ReceiptForwarding::Forwarded => (), ReceiptForwarding::NotForwarded(receipt) => { - self.buffer_receipt(&receipt, state_update, shard, &apply_state.config)?; + self.buffer_receipt( + receipt, + size, + gas, + state_update, + shard, + apply_state.config.use_state_stored_receipt, + )?; } } Ok(()) @@ -259,6 +280,8 @@ impl ReceiptSinkV2<'_> { /// namely `outgoing_limit` and `outgoing_receipt`. fn try_forward( receipt: Receipt, + gas: u64, + size: u64, shard: ShardId, outgoing_limit: &mut HashMap, outgoing_receipts: &mut Vec, @@ -270,19 +293,17 @@ impl ReceiptSinkV2<'_> { // any case, if we cannot know a limit, treating it as literally "no // limit" is the safest approach to ensure availability. // For the size limit, we default to the usual limit that is applied to all (non-special) shards. - let forward_limit = outgoing_limit.entry(shard).or_insert(OutgoingLimit { + let default_outgoing_limit = OutgoingLimit { gas: Gas::MAX, size: apply_state.config.congestion_control_config.outgoing_receipts_usual_size_limit, - }); - let gas_to_forward = receipt_congestion_gas(&receipt, &apply_state.config)?; - let size_to_forward: u64 = - receipt_size(&receipt)?.try_into().expect("Can't convert usize to u64"); + }; + let forward_limit = outgoing_limit.entry(shard).or_insert(default_outgoing_limit); - if forward_limit.gas > gas_to_forward && forward_limit.size > size_to_forward { + if forward_limit.gas > gas && forward_limit.size > size { outgoing_receipts.push(receipt); // underflow impossible: checked forward_limit > gas/size_to_forward above - forward_limit.gas -= gas_to_forward; - forward_limit.size -= size_to_forward; + forward_limit.gas -= gas; + forward_limit.size -= size; Ok(ReceiptForwarding::Forwarded) } else { Ok(ReceiptForwarding::NotForwarded(receipt)) @@ -292,24 +313,60 @@ impl ReceiptSinkV2<'_> { /// Put a receipt in the outgoing receipt buffer of a shard. fn buffer_receipt( &mut self, - receipt: &Receipt, + receipt: Receipt, + size: u64, + gas: u64, state_update: &mut TrieUpdate, shard: u64, - config: &RuntimeConfig, + use_state_stored_receipt: bool, ) -> Result<(), RuntimeError> { - let bytes = receipt_size(&receipt)?; - let gas = receipt_congestion_gas(&receipt, config)?; - self.own_congestion_info.add_receipt_bytes(bytes as u64)?; + let receipt = match use_state_stored_receipt { + true => { + let metadata = + StateStoredReceiptMetadata { congestion_gas: gas, congestion_size: size }; + let receipt = StateStoredReceipt::new_owned(receipt, metadata); + let receipt = ReceiptOrStateStoredReceipt::StateStoredReceipt(receipt); + receipt + } + false => ReceiptOrStateStoredReceipt::Receipt(std::borrow::Cow::Owned(receipt)), + }; + + self.own_congestion_info.add_receipt_bytes(size)?; self.own_congestion_info.add_buffered_receipt_gas(gas)?; + self.outgoing_buffers.to_shard(shard).push(state_update, &receipt)?; Ok(()) } } +/// Get the receipt gas from the receipt that was retrieved from the state. +/// If it is a [Receipt], the gas will be computed. +/// If it s the [StateStoredReceipt], the size will be read from the metadata. pub(crate) fn receipt_congestion_gas( - receipt: &Receipt, + receipt: &ReceiptOrStateStoredReceipt, config: &RuntimeConfig, ) -> Result { + match receipt { + ReceiptOrStateStoredReceipt::Receipt(receipt) => { + compute_receipt_congestion_gas(receipt, config) + } + ReceiptOrStateStoredReceipt::StateStoredReceipt(receipt) => { + Ok(receipt.metadata().congestion_gas) + } + } +} + +/// Calculate the gas of a receipt before it is pushed into a state queue or +/// buffer. Please note that this method should only be used when storing +/// receipts into state. It should not be used for retrieving receipts from the +/// state. +/// +/// The calculation is part of protocol and should only be modified with a +/// protocol upgrade. +pub(crate) fn compute_receipt_congestion_gas( + receipt: &Receipt, + config: &RuntimeConfig, +) -> Result { match receipt.receipt() { ReceiptEnum::Action(action_receipt) => { // account for gas guaranteed to be used for executing the receipts @@ -424,11 +481,24 @@ impl DelayedReceiptQueueWrapper { receipt: &Receipt, config: &RuntimeConfig, ) -> Result<(), RuntimeError> { - let delayed_gas = receipt_congestion_gas(receipt, &config)?; - let delayed_bytes = receipt_size(receipt)? as u64; - self.new_delayed_gas = safe_add_gas(self.new_delayed_gas, delayed_gas)?; - self.new_delayed_bytes = safe_add_gas(self.new_delayed_bytes, delayed_bytes)?; - self.queue.push(trie_update, receipt)?; + let gas = compute_receipt_congestion_gas(&receipt, &config)?; + let size = compute_receipt_size(&receipt)? as u64; + + // TODO It would be great to have this method take owned Receipt and + // get rid of the Cow from the Receipt and StateStoredReceipt. + let receipt = match config.use_state_stored_receipt { + true => { + let metadata = + StateStoredReceiptMetadata { congestion_gas: gas, congestion_size: size }; + let receipt = StateStoredReceipt::new_borrowed(receipt, metadata); + ReceiptOrStateStoredReceipt::StateStoredReceipt(receipt) + } + false => ReceiptOrStateStoredReceipt::Receipt(Cow::Borrowed(receipt)), + }; + + self.new_delayed_gas = safe_add_gas(self.new_delayed_gas, gas)?; + self.new_delayed_bytes = safe_add_gas(self.new_delayed_bytes, size)?; + self.queue.push(trie_update, &receipt)?; Ok(()) } @@ -436,7 +506,7 @@ impl DelayedReceiptQueueWrapper { &mut self, trie_update: &mut TrieUpdate, config: &RuntimeConfig, - ) -> Result, RuntimeError> { + ) -> Result, RuntimeError> { let receipt = self.queue.pop(trie_update)?; if let Some(receipt) = &receipt { let delayed_gas = receipt_congestion_gas(receipt, &config)?; @@ -467,9 +537,30 @@ impl DelayedReceiptQueueWrapper { } } -pub(crate) fn receipt_size(receipt: &Receipt) -> Result { - // `borsh::object_length` may only fail when the total size overflows u32 - borsh::object_length(&receipt).map_err(|_| IntegerOverflowError) +/// Get the receipt size from the receipt that was retrieved from the state. +/// If it is a [Receipt], the size will be computed. +/// If it s the [StateStoredReceipt], the size will be read from the metadata. +pub(crate) fn receipt_size( + receipt: &ReceiptOrStateStoredReceipt, +) -> Result { + match receipt { + ReceiptOrStateStoredReceipt::Receipt(receipt) => compute_receipt_size(receipt), + ReceiptOrStateStoredReceipt::StateStoredReceipt(receipt) => { + Ok(receipt.metadata().congestion_size) + } + } +} + +/// Calculate the size of a receipt before it is pushed into a state queue or +/// buffer. Please note that this method should only be used when storing +/// receipts into state. It should not be used for retrieving receipts from the +/// state. +/// +/// The calculation is part of protocol and should only be modified with a +/// protocol upgrade. +pub(crate) fn compute_receipt_size(receipt: &Receipt) -> Result { + let size = borsh::object_length(&receipt).map_err(|_| IntegerOverflowError)?; + size.try_into().map_err(|_| IntegerOverflowError) } fn int_overflow_to_storage_err(_err: IntegerOverflowError) -> StorageError { diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index a581c6db6d0..5b9dde14a90 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -27,7 +27,7 @@ use near_primitives::errors::{ use near_primitives::hash::CryptoHash; use near_primitives::receipt::{ ActionReceipt, DataReceipt, DelayedReceiptIndices, PromiseYieldIndices, PromiseYieldTimeout, - Receipt, ReceiptEnum, ReceiptV0, ReceivedData, + Receipt, ReceiptEnum, ReceiptOrStateStoredReceipt, ReceiptV0, ReceivedData, }; use near_primitives::runtime::migration_data::{MigrationData, MigrationFlags}; use near_primitives::sandbox::state_patch::SandboxStatePatch; @@ -1769,6 +1769,8 @@ impl Runtime { .delayed_receipts .pop(&mut processing_state.state_update, &processing_state.apply_state.config)? .expect("queue is not empty"); + let receipt = receipt.into_receipt(); + if let Some(nsi) = &mut next_schedule_after { *nsi = nsi.saturating_sub(1); if *nsi == 0 { @@ -2458,6 +2460,18 @@ impl<'a> MaybeRefReceipt for &'a Receipt { } } +impl MaybeRefReceipt for ReceiptOrStateStoredReceipt<'_> { + fn as_ref(&self) -> &Receipt { + self.get_receipt() + } +} + +impl<'a> MaybeRefReceipt for &'a ReceiptOrStateStoredReceipt<'a> { + fn as_ref(&self) -> &Receipt { + self.get_receipt() + } +} + /// Schedule a one receipt for contract preparation. /// /// The caller should call this method again after the returned number of receipts from `iterator` diff --git a/runtime/runtime/src/tests/apply.rs b/runtime/runtime/src/tests/apply.rs index 630c23d1d23..3d6c163e7ee 100644 --- a/runtime/runtime/src/tests/apply.rs +++ b/runtime/runtime/src/tests/apply.rs @@ -1,6 +1,6 @@ use super::{to_yocto, GAS_PRICE}; use crate::config::safe_add_gas; -use crate::congestion_control::{receipt_congestion_gas, receipt_size}; +use crate::congestion_control::{compute_receipt_congestion_gas, compute_receipt_size}; use crate::tests::{create_receipt_with_actions, MAX_ATTACHED_GAS}; use crate::total_prepaid_exec_fees; use crate::{ApplyResult, ApplyState, Runtime, ValidatorAccountsUpdate}; @@ -1221,8 +1221,8 @@ fn test_congestion_delayed_receipts_accounting() { if ProtocolFeature::CongestionControl.enabled(PROTOCOL_VERSION) { let congestion = apply_result.congestion_info.unwrap(); let expected_delayed_gas = - (n - 1) * receipt_congestion_gas(&receipts[0], &apply_state.config).unwrap(); - let expected_receipts_bytes = (n - 1) * receipt_size(&receipts[0]).unwrap() as u64; + (n - 1) * compute_receipt_congestion_gas(&receipts[0], &apply_state.config).unwrap(); + let expected_receipts_bytes = (n - 1) * compute_receipt_size(&receipts[0]).unwrap() as u64; assert_eq!(expected_delayed_gas as u128, congestion.delayed_receipts_gas()); assert_eq!(expected_receipts_bytes, congestion.receipt_bytes()); diff --git a/tools/protocol-schema-check/res/protocol_schema.toml b/tools/protocol-schema-check/res/protocol_schema.toml index ab871b63095..7c138ad6180 100644 --- a/tools/protocol-schema-check/res/protocol_schema.toml +++ b/tools/protocol-schema-check/res/protocol_schema.toml @@ -170,6 +170,7 @@ ReasonForBan = 792112981 Receipt = 2916802703 ReceiptEnum = 3157292228 ReceiptList = 273687817 +ReceiptOrStateStoredReceipt = 215600480 ReceiptProof = 1019992812 ReceiptProofResponse = 4034805727 ReceiptV0 = 3604411866 @@ -217,6 +218,9 @@ StateResponseInfo = 2184941925 StateResponseInfoV1 = 1435664823 StateResponseInfoV2 = 1784931382 StateRootNode = 1865105129 +StateStoredReceipt = 3853311293 +StateStoredReceiptMetadata = 2895538362 +StateStoredReceiptV0 = 4029868827 StateSyncDumpProgress = 2225888613 StorageError = 1838871872 StoredChunkStateTransitionData = 516372819 diff --git a/tools/state-viewer/src/congestion_control.rs b/tools/state-viewer/src/congestion_control.rs index 2f6f7a6d5d8..b5ce12a9ae6 100644 --- a/tools/state-viewer/src/congestion_control.rs +++ b/tools/state-viewer/src/congestion_control.rs @@ -1,11 +1,14 @@ use rand::Rng; +use std::borrow::Cow; use std::path::Path; use near_chain::types::RuntimeAdapter; use near_chain::{ChainStore, ChainStoreAccess}; use near_epoch_manager::EpochManagerAdapter; use near_primitives::hash::CryptoHash; -use near_primitives::receipt::{DataReceipt, Receipt, ReceiptEnum, ReceiptV1}; +use near_primitives::receipt::{ + DataReceipt, Receipt, ReceiptEnum, ReceiptOrStateStoredReceipt, ReceiptV1, +}; use near_primitives::types::{ShardId, StateChangeCause, StateRoot}; use near_store::trie::receipts_column_helper::{DelayedReceiptQueue, TrieQueue}; use near_store::{ShardTries, ShardUId, Store, TrieUpdate}; @@ -189,6 +192,8 @@ impl PrepareBenchmarkCmd { for _ in 0..self.receipt_count { let receipt = self.create_receipt(); + let receipt = Cow::Borrowed(&receipt); + let receipt = ReceiptOrStateStoredReceipt::Receipt(receipt); queue.push(&mut trie_update, &receipt).unwrap(); } From b079a53f750b1a4028f799d59772be73850b6439 Mon Sep 17 00:00:00 2001 From: Simonas Kazlauskas Date: Mon, 23 Sep 2024 19:15:34 +0300 Subject: [PATCH 08/49] chore: ignore `RUSTSEC-2024-0370` (#12126) Fixes the CI. --- .cargo/audit.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.cargo/audit.toml b/.cargo/audit.toml index d503f450eac..75ada3f8387 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -27,4 +27,8 @@ ignore = [ # older versions of parking-lot are vulnerable, but used by wasmer0, which we need to keep alive for replayability reasons. # We should remove it, as well as this ignore, as soon as we get limited replayability. "RUSTSEC-2020-0070", + + # proc-macro-error is unmaintained, but hard to replace right now. + # Follow https://github.com/Kyuuhachi/syn_derive/issues/4 + "RUSTSEC-2024-0370", ] From 0416fdfc7aa8ccc29e62e9eaacfe7e827b344f0d Mon Sep 17 00:00:00 2001 From: Saketh Are Date: Mon, 23 Sep 2024 15:24:07 -0400 Subject: [PATCH 09/49] fix(network): bound size of tier3_requests queue (#12124) This simple limit is added for security purposes; it prevents the node from going OOM due to unbounded addition to the queue. In the future we will implement separate resource pools to prioritize incoming requests from validator nodes. --- Currently a node processes at most one tier3 request per second: https://github.com/near/nearcore/blob/d6d6e4782f7d57a7b7acc2894e1668355012ff74/chain/network/src/peer_manager/peer_manager_actor.rs#L92 and the default timeout on the node which sent the request is 60 seconds: https://github.com/near/nearcore/blob/d6d6e4782f7d57a7b7acc2894e1668355012ff74/core/chain-configs/src/client_config.rs#L282 Hence a queue limit of 60 means that any dropped requests would not have been served in time anyway. --- .../src/peer_manager/network_state/mod.rs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/chain/network/src/peer_manager/network_state/mod.rs b/chain/network/src/peer_manager/network_state/mod.rs index 739b94e8101..9cfa1b4bb41 100644 --- a/chain/network/src/peer_manager/network_state/mod.rs +++ b/chain/network/src/peer_manager/network_state/mod.rs @@ -68,6 +68,9 @@ pub const PRUNE_EDGES_AFTER: time::Duration = time::Duration::minutes(30); /// How long to wait between reconnection attempts to the same peer pub(crate) const RECONNECT_ATTEMPT_INTERVAL: time::Duration = time::Duration::seconds(10); +/// Limit number of pending tier3 requests to avoid OOM. +pub(crate) const LIMIT_TIER3_REQUESTS: usize = 60; + impl WhitelistNode { pub fn from_peer_info(pi: &PeerInfo) -> anyhow::Result { Ok(Self { @@ -784,14 +787,21 @@ impl NetworkState { None } RoutedMessageBody::StatePartRequest(request) => { - self.tier3_requests.lock().push_back(Tier3Request { - peer_info: PeerInfo { id: peer_id, addr: Some(request.addr), account_id: None }, - body: Tier3RequestBody::StatePart(StatePartRequestBody { - shard_id: request.shard_id, - sync_hash: request.sync_hash, - part_id: request.part_id, - }), - }); + let mut queue = self.tier3_requests.lock(); + if queue.len() < LIMIT_TIER3_REQUESTS { + queue.push_back(Tier3Request { + peer_info: PeerInfo { + id: peer_id, + addr: Some(request.addr), + account_id: None, + }, + body: Tier3RequestBody::StatePart(StatePartRequestBody { + shard_id: request.shard_id, + sync_hash: request.sync_hash, + part_id: request.part_id, + }), + }); + } None } body => { From 59814e344e559f554c02c200380c05d7e330bfd0 Mon Sep 17 00:00:00 2001 From: Shreyan Gupta Date: Mon, 23 Sep 2024 12:38:41 -0700 Subject: [PATCH 10/49] [cleanup] Remove flat storage inline migration handler (#12122) No longer needed after the migration. I noticed while working on some refactoring wrt store. --- core/store/src/flat/inlining_migration.rs | 481 ---------------------- core/store/src/flat/mod.rs | 2 - core/store/src/flat/store_helper.rs | 33 +- core/store/src/flat/types.rs | 7 - nearcore/src/lib.rs | 12 - neard/src/cli.rs | 2 - tools/flat-storage/src/commands.rs | 30 +- 7 files changed, 2 insertions(+), 565 deletions(-) delete mode 100644 core/store/src/flat/inlining_migration.rs diff --git a/core/store/src/flat/inlining_migration.rs b/core/store/src/flat/inlining_migration.rs deleted file mode 100644 index 37d777c0961..00000000000 --- a/core/store/src/flat/inlining_migration.rs +++ /dev/null @@ -1,481 +0,0 @@ -use std::collections::HashMap; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; -use std::thread::JoinHandle; -use std::time::Duration; - -use borsh::BorshDeserialize; -use crossbeam::channel; -use itertools::Itertools; -use near_primitives::hash::CryptoHash; -use near_primitives::shard_layout::ShardUId; -use near_primitives::state::FlatStateValue; -use tracing::{debug, info}; - -use crate::flat::store_helper::set_flat_state_values_inlining_migration_status; -use crate::metrics::flat_state_metrics::inlining_migration::{ - FLAT_STATE_PAUSED_DURATION, INLINED_COUNT, INLINED_TOTAL_VALUES_SIZE, PROCESSED_COUNT, - PROCESSED_TOTAL_VALUES_SIZE, SKIPPED_COUNT, -}; -use crate::{DBCol, Store, TrieDBStorage, TrieStorage}; - -use super::store_helper::{ - decode_flat_state_db_key, get_flat_state_values_inlining_migration_status, -}; -use super::types::FlatStateValuesInliningMigrationStatus; -use super::FlatStorageManager; - -pub struct FlatStateValuesInliningMigrationHandle { - handle: JoinHandle<()>, - keep_running: Arc, -} - -const BACKGROUND_MIGRATION_BATCH_SIZE: usize = 50_000; - -impl FlatStateValuesInliningMigrationHandle { - pub fn start_background_migration( - store: Store, - flat_storage_manager: FlatStorageManager, - read_state_threads: usize, - ) -> Self { - let keep_running = Arc::new(AtomicBool::new(true)); - let keep_runnning_clone = keep_running.clone(); - let handle = std::thread::spawn(move || { - let status = get_flat_state_values_inlining_migration_status(&store) - .expect("failed to read fs migration status"); - info!(target: "store", ?status, "Read FlatState values inlining migration status"); - if status == FlatStateValuesInliningMigrationStatus::Finished { - return; - } - set_flat_state_values_inlining_migration_status( - &store, - FlatStateValuesInliningMigrationStatus::InProgress, - ) - .expect("failed to set fs migration in-progress status"); - let completed = inline_flat_state_values( - store.clone(), - &flat_storage_manager, - &keep_running, - read_state_threads, - BACKGROUND_MIGRATION_BATCH_SIZE, - ); - if completed { - set_flat_state_values_inlining_migration_status( - &store, - FlatStateValuesInliningMigrationStatus::Finished, - ) - .expect("failed to set fs migration finished status"); - } - }); - Self { handle, keep_running: keep_runnning_clone } - } - - pub fn stop(self) { - self.keep_running.store(false, std::sync::atomic::Ordering::Relaxed); - self.handle.join().expect("join should not fail here"); - } -} - -struct ReadValueRequest { - shard_uid: ShardUId, - value_hash: CryptoHash, -} - -struct ReadValueResponse { - value_hash: CryptoHash, - value_bytes: Option>, -} - -/// An abstraction that enables reading values from State in parallel using -/// multiple threads. -struct StateValueReader { - pending_requests: usize, - value_request_send: channel::Sender, - value_response_recv: channel::Receiver, - join_handles: Vec>, -} - -impl StateValueReader { - fn new(store: Store, num_threads: usize) -> Self { - let (value_request_send, value_request_recv) = channel::unbounded(); - let (value_response_send, value_response_recv) = channel::unbounded(); - let mut join_handles = Vec::new(); - for _ in 0..num_threads { - join_handles.push(Self::spawn_read_value_thread( - store.clone(), - value_request_recv.clone(), - value_response_send.clone(), - )); - } - Self { pending_requests: 0, value_request_send, value_response_recv, join_handles } - } - - fn submit(&mut self, shard_uid: ShardUId, value_hash: CryptoHash) { - let req = ReadValueRequest { shard_uid, value_hash }; - self.value_request_send.send(req).expect("send should not fail here"); - self.pending_requests += 1; - } - - fn receive_all(&mut self) -> HashMap> { - let mut ret = HashMap::new(); - while self.pending_requests > 0 { - let resp = self.value_response_recv.recv().expect("recv should not fail here"); - if let Some(value) = resp.value_bytes { - ret.insert(resp.value_hash, value); - } - self.pending_requests -= 1; - } - ret - } - - fn spawn_read_value_thread( - store: Store, - recv: channel::Receiver, - send: channel::Sender, - ) -> std::thread::JoinHandle<()> { - std::thread::spawn(move || { - while let Ok(req) = recv.recv() { - let trie_storage = TrieDBStorage::new(store.clone(), req.shard_uid); - let bytes = match trie_storage.retrieve_raw_bytes(&req.value_hash) { - Ok(bytes) => Some(bytes.to_vec()), - Err(err) => { - log_skipped("failed to read value from State", err); - None - } - }; - send.send(ReadValueResponse { value_hash: req.value_hash, value_bytes: bytes }) - .expect("send should not fail here"); - } - }) - } - - /// Note that we cannot use standard `drop` because it takes `&mut self` - /// as an argument which prevents manual drop of `self.value_request_send` - fn close(self) { - std::mem::drop(self.value_request_send); - for join_handle in self.join_handles { - join_handle.join().expect("join should not fail here"); - } - } -} - -/// Inlines all FlatState values having length below `FlatStateValue::INLINE_DISK_VALUE_THRESHOLD`. -/// Migration is safe to be executed in parallel with block processing, which -/// is achieved by temporary preventing FlatState updates with -/// `FlatStorageManager::set_flat_state_updates_mode`. -/// -/// * `read_state_threads` - number of threads for reading values from `State` in parallel. -/// * `batch_size` - number of values to be processed for inlining in one batch. -pub fn inline_flat_state_values( - store: Store, - flat_storage_manager: &FlatStorageManager, - keep_running: &AtomicBool, - read_state_threads: usize, - batch_size: usize, -) -> bool { - info!(target: "store", %read_state_threads, %batch_size, "Starting FlatState value inlining migration"); - let migration_start = std::time::Instant::now(); - let mut value_reader = StateValueReader::new(store.clone(), read_state_threads); - let mut inlined_total_count = 0; - let mut interrupted = false; - for (batch_index, batch) in - store.iter(DBCol::FlatState).chunks(batch_size).into_iter().enumerate() - { - if !keep_running.load(std::sync::atomic::Ordering::Relaxed) { - info!(target: "store", %batch_index, "FlatState value inlining migration was interrupted"); - interrupted = true; - break; - } - let (mut min_key, mut max_key) = (None, None); - for entry in batch { - PROCESSED_COUNT.inc(); - let (key, value) = match entry { - Ok(v) => v, - Err(err) => { - log_skipped("rocksdb iterator error", err); - continue; - } - }; - let shard_uid = match decode_flat_state_db_key(&key) { - Ok((shard_uid, _)) => shard_uid, - Err(err) => { - log_skipped("failed to decode FlatState key", err); - continue; - } - }; - let fs_value = match FlatStateValue::try_from_slice(&value) { - Ok(fs_value) => fs_value, - Err(err) => { - log_skipped("failed to deserialise FlatState value", err); - continue; - } - }; - let value_size = match &fs_value { - FlatStateValue::Ref(value_ref) => value_ref.length as u64, - FlatStateValue::Inlined(bytes) => bytes.len() as u64, - }; - PROCESSED_TOTAL_VALUES_SIZE.inc_by(value_size); - if let FlatStateValue::Ref(value_ref) = fs_value { - if value_ref.length as usize <= FlatStateValue::INLINE_DISK_VALUE_THRESHOLD { - if min_key.is_none() { - min_key = Some(key.to_vec()); - } - max_key = Some(key.to_vec()); - INLINED_TOTAL_VALUES_SIZE.inc_by(value_size); - value_reader.submit(shard_uid, value_ref.hash); - } - } - } - let hash_to_value = value_reader.receive_all(); - let mut inlined_batch_count = 0; - let mut batch_duration = std::time::Duration::ZERO; - if !hash_to_value.is_empty() { - // Possibly flat storage head can be locked. If that happens wait a little bit and try again. - // The number of attempts is infinite because flat storage head is supposed to be usually unlocked. - interrupted = lock_flat_head_blocking(flat_storage_manager, keep_running, batch_index); - if interrupted { - break; - } - tracing::debug!(target: "store", "Locked flat storage for the inlining migration"); - - // Here we need to re-read the latest FlatState values in `min_key..=max_key` range - // while updates are disabled. This way we prevent updating the values that - // were updated since migration start. - let batch_inlining_start = std::time::Instant::now(); - let mut store_update = store.store_update(); - // rockdb API accepts the exclusive end of the range, so we append - // `0u8` here to make sure `max_key` is included in the range - let upper_bound_key = max_key.map(|mut v| { - v.push(0u8); - v - }); - for (key, value) in store - .iter_range(DBCol::FlatState, min_key.as_deref(), upper_bound_key.as_deref()) - .flat_map(|v| v) - { - if let Ok(FlatStateValue::Ref(value_ref)) = FlatStateValue::try_from_slice(&value) { - if let Some(value) = hash_to_value.get(&value_ref.hash) { - store_update.set( - DBCol::FlatState, - &key, - &borsh::to_vec(&FlatStateValue::inlined(value)) - .expect("borsh should not fail here"), - ); - inlined_batch_count += 1; - INLINED_COUNT.inc(); - } - } - } - store_update.commit().expect("failed to commit inlined values"); - assert!(flat_storage_manager.set_flat_state_updates_mode(true)); - tracing::debug!(target: "store", "Unlocked flat storage after the inlining migration"); - inlined_total_count += inlined_batch_count; - batch_duration = batch_inlining_start.elapsed(); - FLAT_STATE_PAUSED_DURATION.observe(batch_duration.as_secs_f64()); - } - debug!(target: "store", %batch_index, %inlined_batch_count, %inlined_total_count, ?batch_duration, "Processed flat state value inlining batch"); - } - value_reader.close(); - let migration_elapsed = migration_start.elapsed(); - info!(target: "store", %inlined_total_count, ?migration_elapsed, %interrupted, "Finished FlatState value inlining migration"); - !interrupted -} - -/// Blocks until the flat head is locked or until the thread is interrupted. -/// Returns whether it was interrupted. -fn lock_flat_head_blocking( - flat_storage_manager: &FlatStorageManager, - keep_running: &AtomicBool, - batch_index: usize, -) -> bool { - loop { - if !keep_running.load(std::sync::atomic::Ordering::Relaxed) { - tracing::info!(target: "store", batch_index, "FlatState value inlining migration was interrupted"); - return true; - } - if flat_storage_manager.set_flat_state_updates_mode(false) { - return false; - } - tracing::debug!(target: "store", "Couldn't lock flat storage for the inlining migration, will retry locking."); - std::thread::sleep(Duration::from_secs(1)); - } -} - -fn log_skipped(reason: &str, err: impl std::error::Error) { - debug!(target: "store", %reason, %err, "Skipped value during FlatState inlining"); - SKIPPED_COUNT.inc(); -} - -#[cfg(test)] -mod tests { - use super::inline_flat_state_values; - use crate::flat::store_helper::encode_flat_state_db_key; - use crate::flat::{FlatStateValuesInliningMigrationHandle, FlatStorageManager}; - use crate::{DBCol, NodeStorage, Store, TrieCachingStorage}; - use borsh::BorshDeserialize; - use near_o11y::testonly::init_test_logger; - use near_primitives::hash::{hash, CryptoHash}; - use near_primitives::shard_layout::{ShardLayout, ShardUId}; - use near_primitives::state::FlatStateValue; - use std::sync::atomic::AtomicBool; - use std::time::Duration; - - #[test] - fn full_migration() { - let store = NodeStorage::test_opener().1.open().unwrap().get_hot_store(); - let shard_uid = ShardLayout::v0_single_shard().shard_uids().next().unwrap(); - let values = [ - vec![0], - vec![1], - vec![2; FlatStateValue::INLINE_DISK_VALUE_THRESHOLD + 1], - vec![3], - vec![4], - vec![5], - ]; - populate_flat_store(&store, shard_uid, &values); - let flat_storage_manager = create_flat_storage_for_genesis(&store, shard_uid); - inline_flat_state_values( - store.clone(), - &flat_storage_manager, - &AtomicBool::new(true), - 2, - 4, - ); - assert_eq!( - store - .iter(DBCol::FlatState) - .flat_map(|r| r.map(|(_, v)| FlatStateValue::try_from_slice(&v).unwrap())) - .collect::>(), - vec![ - FlatStateValue::inlined(&values[0]), - FlatStateValue::inlined(&values[1]), - FlatStateValue::value_ref(&values[2]), - FlatStateValue::inlined(&values[3]), - FlatStateValue::inlined(&values[4]), - FlatStateValue::inlined(&values[5]), - ] - ); - } - - #[test] - // Initializes several ref values. - // Locks flat storage head, and checks that the migration doesn't crash and doesn't proceed. - // Then it unlocks flat head and checks that the migration completes. - fn block_migration() { - init_test_logger(); - let store = NodeStorage::test_opener().1.open().unwrap().get_hot_store(); - let shard_uid = ShardLayout::v0_single_shard().shard_uids().next().unwrap(); - let values = [ - vec![0], - vec![1], - vec![2; FlatStateValue::INLINE_DISK_VALUE_THRESHOLD + 1], - vec![3], - vec![4], - vec![5], - ]; - populate_flat_store(&store, shard_uid, &values); - let flat_storage_manager = create_flat_storage_for_genesis(&store, shard_uid); - // Lock flat head. - assert!(flat_storage_manager.set_flat_state_updates_mode(false)); - // Start a separate thread that should block waiting for the flat head. - let _handle = FlatStateValuesInliningMigrationHandle::start_background_migration( - store.clone(), - flat_storage_manager.clone(), - 2, - ); - - // Give it time and check that no progress was made on the migration. - std::thread::sleep(Duration::from_secs(2)); - assert_eq!(count_inlined_values(&store), 0); - - // Unlock. - assert!(flat_storage_manager.set_flat_state_updates_mode(true)); - - // Give it more time. It should be unblocked now and the migration should complete. - std::thread::sleep(Duration::from_secs(2)); - assert_eq!(count_inlined_values(&store), 5); - } - - #[test] - // Initializes several ref values. - // Locks flat storage head, and checks that the migration doesn't crash and doesn't proceed. - // Interrupt the migration and check that the thread exits. - fn interrupt_blocked_migration() { - init_test_logger(); - let store = NodeStorage::test_opener().1.open().unwrap().get_hot_store(); - let shard_uid = ShardLayout::v0_single_shard().shard_uids().next().unwrap(); - let values = [ - vec![0], - vec![1], - vec![2; FlatStateValue::INLINE_DISK_VALUE_THRESHOLD + 1], - vec![3], - vec![4], - vec![5], - ]; - populate_flat_store(&store, shard_uid, &values); - let flat_storage_manager = create_flat_storage_for_genesis(&store, shard_uid); - // Lock flat head. - assert!(flat_storage_manager.set_flat_state_updates_mode(false)); - // Start a separate thread that should block waiting for the flat head. - let handle = FlatStateValuesInliningMigrationHandle::start_background_migration( - store.clone(), - flat_storage_manager, - 2, - ); - - // Give it time and check that no progress was made on the migration. - std::thread::sleep(Duration::from_secs(2)); - assert_eq!(count_inlined_values(&store), 0); - - // Interrupt. - handle.keep_running.store(false, std::sync::atomic::Ordering::Relaxed); - assert!(handle.handle.join().is_ok()); - // Check that no migration took place. - assert_eq!(count_inlined_values(&store), 0); - } - - fn populate_flat_store(store: &Store, shard_uid: ShardUId, values: &[Vec]) { - let mut store_update = store.store_update(); - for (i, value) in values.iter().enumerate() { - let trie_key = - TrieCachingStorage::get_key_from_shard_uid_and_hash(shard_uid, &hash(&value)); - store_update.increment_refcount(DBCol::State, &trie_key, &value); - let fs_key = encode_flat_state_db_key(shard_uid, &[i as u8]); - let fs_value = borsh::to_vec(&FlatStateValue::value_ref(&value)).unwrap(); - store_update.set(DBCol::FlatState, &fs_key, &fs_value); - } - store_update.commit().unwrap(); - } - - fn create_flat_storage_for_genesis(store: &Store, shard_uid: ShardUId) -> FlatStorageManager { - let flat_storage_manager = FlatStorageManager::new(store.clone()); - let mut store_update = store.store_update(); - flat_storage_manager.set_flat_storage_for_genesis( - &mut store_update, - shard_uid, - &CryptoHash::default(), - 0, - ); - store_update.commit().unwrap(); - flat_storage_manager.create_flat_storage_for_shard(shard_uid).unwrap(); - flat_storage_manager - } - - fn count_inlined_values(store: &Store) -> u64 { - store - .iter(DBCol::FlatState) - .flat_map(|r| { - r.map(|(_, v)| { - if matches!( - FlatStateValue::try_from_slice(&v).unwrap(), - FlatStateValue::Inlined(_) - ) { - 1 - } else { - 0 - } - }) - }) - .sum::() - } -} diff --git a/core/store/src/flat/mod.rs b/core/store/src/flat/mod.rs index d195bf7e3ee..ff714ace1e6 100644 --- a/core/store/src/flat/mod.rs +++ b/core/store/src/flat/mod.rs @@ -27,7 +27,6 @@ mod chunk_view; pub mod delta; -mod inlining_migration; mod manager; mod metrics; mod storage; @@ -38,7 +37,6 @@ mod types; pub use chunk_view::FlatStorageChunkView; pub use delta::{FlatStateChanges, FlatStateDelta, FlatStateDeltaMetadata}; -pub use inlining_migration::{inline_flat_state_values, FlatStateValuesInliningMigrationHandle}; pub use manager::FlatStorageManager; pub use metrics::FlatStorageCreationMetrics; pub use storage::FlatStorage; diff --git a/core/store/src/flat/store_helper.rs b/core/store/src/flat/store_helper.rs index ad05b92e7ae..472c7b0816d 100644 --- a/core/store/src/flat/store_helper.rs +++ b/core/store/src/flat/store_helper.rs @@ -2,10 +2,7 @@ //! TODO(#8577): remove this file and move functions to the corresponding structs use super::delta::{FlatStateDelta, FlatStateDeltaMetadata}; -use super::types::{ - FlatStateIterator, FlatStateValuesInliningMigrationStatus, FlatStorageResult, FlatStorageStatus, -}; -use crate::db::FLAT_STATE_VALUES_INLINING_MIGRATION_STATUS_KEY; +use super::types::{FlatStateIterator, FlatStorageResult, FlatStorageStatus}; use crate::flat::delta::{BlockWithChangesInfo, FlatStateChanges, KeyForFlatStateDelta}; use crate::flat::types::FlatStorageError; use crate::flat::FlatStorageReadyStatus; @@ -147,34 +144,6 @@ pub fn decode_flat_state_db_key(key: &[u8]) -> io::Result<(ShardUId, Vec)> { Ok((shard_uid, trie_key.to_vec())) } -pub fn get_flat_state_values_inlining_migration_status( - store: &Store, -) -> FlatStorageResult { - store - .get_ser(DBCol::Misc, FLAT_STATE_VALUES_INLINING_MIGRATION_STATUS_KEY) - .map(|status| status.unwrap_or(FlatStateValuesInliningMigrationStatus::Empty)) - .map_err(|err| { - FlatStorageError::StorageInternalError(format!( - "failed to read FlatState values inlining migration status: {err}" - )) - }) -} - -pub fn set_flat_state_values_inlining_migration_status( - store: &Store, - status: FlatStateValuesInliningMigrationStatus, -) -> FlatStorageResult<()> { - let mut store_update = store.store_update(); - store_update - .set_ser(DBCol::Misc, FLAT_STATE_VALUES_INLINING_MIGRATION_STATUS_KEY, &status) - .expect("Borsh should not have failed here"); - store_update.commit().map_err(|err| { - FlatStorageError::StorageInternalError(format!( - "failed to commit FlatState values inlining migration status: {err}" - )) - }) -} - pub fn get_flat_state_value( store: &Store, shard_uid: ShardUId, diff --git a/core/store/src/flat/types.rs b/core/store/src/flat/types.rs index 5397d8c6541..744ae1b936b 100644 --- a/core/store/src/flat/types.rs +++ b/core/store/src/flat/types.rs @@ -46,13 +46,6 @@ impl From for StorageError { pub type FlatStorageResult = Result; -#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)] -pub enum FlatStateValuesInliningMigrationStatus { - Empty, - InProgress, - Finished, -} - #[derive( BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq, serde::Serialize, ProtocolSchema, )] diff --git a/nearcore/src/lib.rs b/nearcore/src/lib.rs index 4329a352ecc..4f917942f04 100644 --- a/nearcore/src/lib.rs +++ b/nearcore/src/lib.rs @@ -36,7 +36,6 @@ use near_epoch_manager::EpochManagerAdapter; use near_network::PeerManagerActor; use near_primitives::block::GenesisId; use near_primitives::types::EpochId; -use near_store::flat::FlatStateValuesInliningMigrationHandle; use near_store::genesis::initialize_sharded_genesis_state; use near_store::metadata::DbKind; use near_store::metrics::spawn_db_metrics_loop; @@ -221,9 +220,6 @@ pub struct NearNode { pub cold_store_loop_handle: Option, /// Contains handles to background threads that may be dumping state to S3. pub state_sync_dumper: StateSyncDumper, - /// A handle to control background flat state values inlining migration. - /// Needed temporarily, will be removed after the migration is completed. - pub flat_state_migration_handle: FlatStateValuesInliningMigrationHandle, // A handle that allows the main process to interrupt resharding if needed. // This typically happens when the main process is interrupted. pub resharding_handle: ReshardingHandle, @@ -423,13 +419,6 @@ pub fn start_with_config_and_synchronization( ); shards_manager_adapter.bind(shards_manager_actor.with_auto_span_context()); - let flat_state_migration_handle = - FlatStateValuesInliningMigrationHandle::start_background_migration( - storage.get_hot_store(), - runtime.get_flat_storage_manager(), - config.client_config.client_background_migration_threads, - ); - let mut state_sync_dumper = StateSyncDumper { clock: Clock::real(), client_config: config.client_config.clone(), @@ -518,7 +507,6 @@ pub fn start_with_config_and_synchronization( arbiters, cold_store_loop_handle, state_sync_dumper, - flat_state_migration_handle, resharding_handle, }) } diff --git a/neard/src/cli.rs b/neard/src/cli.rs index b3436d99ed6..121f6e12d45 100644 --- a/neard/src/cli.rs +++ b/neard/src/cli.rs @@ -530,7 +530,6 @@ impl RunCmd { rpc_servers, cold_store_loop_handle, mut state_sync_dumper, - flat_state_migration_handle, resharding_handle, .. } = nearcore::start_with_config_and_synchronization( @@ -557,7 +556,6 @@ impl RunCmd { } state_sync_dumper.stop(); resharding_handle.stop(); - flat_state_migration_handle.stop(); futures::future::join_all(rpc_servers.iter().map(|(name, server)| async move { server.stop(true).await; debug!(target: "neard", "{} server stopped", name); diff --git a/tools/flat-storage/src/commands.rs b/tools/flat-storage/src/commands.rs index 6e4cc70fd05..ef6527411c4 100644 --- a/tools/flat-storage/src/commands.rs +++ b/tools/flat-storage/src/commands.rs @@ -10,13 +10,11 @@ use near_primitives::shard_layout::{account_id_to_shard_id, ShardVersion}; use near_primitives::state::FlatStateValue; use near_primitives::types::{BlockHeight, ShardId}; use near_store::flat::{ - inline_flat_state_values, store_helper, FlatStateChanges, FlatStateDelta, - FlatStateDeltaMetadata, FlatStorageManager, FlatStorageStatus, + store_helper, FlatStateChanges, FlatStateDelta, FlatStateDeltaMetadata, FlatStorageStatus, }; use near_store::{DBCol, Mode, NodeStorage, ShardUId, Store, StoreOpener}; use nearcore::{load_config, NearConfig, NightshadeRuntime, NightshadeRuntimeExt}; use std::collections::{HashMap, HashSet}; -use std::sync::atomic::AtomicBool; use std::{path::PathBuf, sync::Arc, time::Duration}; use tqdm::tqdm; @@ -45,9 +43,6 @@ enum SubCommand { /// storage is enabled only during nightly with separate DB version). SetStoreVersion(SetStoreVersionCmd), - /// Run FlatState value inininig migration - MigrateValueInlining(MigrateValueInliningCmd), - /// Construct and store trie in a separate directory from flat storage state for a given shard. /// The trie is constructed for the block height equal to flat_head ConstructTrieFromFlat(ConstructTriedFromFlatCmd), @@ -376,26 +371,6 @@ impl FlatStorageCommand { Ok(()) } - fn migrate_value_inlining( - &self, - cmd: &MigrateValueInliningCmd, - home_dir: &PathBuf, - near_config: &NearConfig, - opener: StoreOpener, - ) -> anyhow::Result<()> { - let store = - Self::get_db(&opener, home_dir, &near_config, near_store::Mode::ReadWriteExisting).4; - let flat_storage_manager = FlatStorageManager::new(store.clone()); - inline_flat_state_values( - store, - &flat_storage_manager, - &AtomicBool::new(true), - cmd.num_threads, - cmd.batch_size, - ); - Ok(()) - } - fn construct_trie_from_flat( &self, cmd: &ConstructTriedFromFlatCmd, @@ -648,9 +623,6 @@ impl FlatStorageCommand { SubCommand::Reset(cmd) => self.reset(cmd, home_dir, &near_config, opener), SubCommand::Init(cmd) => self.init(cmd, home_dir, &near_config, opener), SubCommand::Verify(cmd) => self.verify(cmd, home_dir, &near_config, opener), - SubCommand::MigrateValueInlining(cmd) => { - self.migrate_value_inlining(cmd, home_dir, &near_config, opener) - } SubCommand::ConstructTrieFromFlat(cmd) => { self.construct_trie_from_flat(cmd, home_dir, &near_config, opener) } From bce055abc939dd047d71d1fe8e1df2b4dd0c058d Mon Sep 17 00:00:00 2001 From: Shreyan Gupta Date: Mon, 23 Sep 2024 13:18:16 -0700 Subject: [PATCH 11/49] [stateless_validation] Refactor ChunkEndorsementsState (#12116) I noticed in endorsement tracker, we were first checking if we have enough endorsement stake and then using that information to generate the signature vector. It's simpler and more efficient to generate this while calculating enough stake. ChunkEndorsementsState doesn't have to be an enum, we can include the signatures in EndorsementStats (and rename it to ChunkEndorsementsState). --- .../stateless_validation/chunk_endorsement.rs | 14 +++--- chain/client/src/chunk_inclusion_tracker.rs | 29 ++++------- chain/client/src/debug.rs | 10 ++-- .../chunk_endorsement/mod.rs | 17 +------ .../chunk_endorsement/tracker_v1.rs | 42 +++++----------- .../chunk_endorsement/tracker_v2.rs | 43 +++++----------- .../validator_assignment.rs | 50 ++++++++++++------- 7 files changed, 80 insertions(+), 125 deletions(-) diff --git a/chain/chain/src/stateless_validation/chunk_endorsement.rs b/chain/chain/src/stateless_validation/chunk_endorsement.rs index db148245a8f..1531e7d65eb 100644 --- a/chain/chain/src/stateless_validation/chunk_endorsement.rs +++ b/chain/chain/src/stateless_validation/chunk_endorsement.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::HashMap; use itertools::Itertools; use near_chain_primitives::Error; @@ -79,7 +79,7 @@ pub fn validate_chunk_endorsements_in_block( // Verify that the signature in block body are valid for given chunk_validator. // Signature can be either None, or Some(signature). // We calculate the stake of the chunk_validators for who we have the signature present. - let mut endorsed_chunk_validators = HashSet::new(); + let mut endorsed_chunk_validators = HashMap::new(); for (account_id, signature) in ordered_chunk_validators.iter().zip(signatures) { let Some(signature) = signature else { continue }; let (validator, _) = epoch_manager.get_validator_by_account_id( @@ -104,13 +104,13 @@ pub fn validate_chunk_endorsements_in_block( } // Add validators with signature in endorsed_chunk_validators. We later use this to check stake. - endorsed_chunk_validators.insert(account_id); + endorsed_chunk_validators.insert(account_id, *signature.clone()); } - let endorsement_stats = - chunk_validator_assignments.compute_endorsement_stats(&endorsed_chunk_validators); - if !endorsement_stats.has_enough_stake() { - tracing::error!(target: "chain", ?endorsement_stats, "Chunk does not have enough stake to be endorsed"); + let endorsement_state = + chunk_validator_assignments.compute_endorsement_state(endorsed_chunk_validators); + if !endorsement_state.is_endorsed { + tracing::error!(target: "chain", ?endorsement_state, "Chunk does not have enough stake to be endorsed"); return Err(Error::InvalidChunkEndorsement); } diff --git a/chain/client/src/chunk_inclusion_tracker.rs b/chain/client/src/chunk_inclusion_tracker.rs index 8400791b970..ffc7da1f25d 100644 --- a/chain/client/src/chunk_inclusion_tracker.rs +++ b/chain/client/src/chunk_inclusion_tracker.rs @@ -6,14 +6,13 @@ use near_o11y::log_assert_fail; use near_primitives::block_body::ChunkEndorsementSignatures; use near_primitives::hash::CryptoHash; use near_primitives::sharding::{ChunkHash, ShardChunkHeader}; +use near_primitives::stateless_validation::validator_assignment::ChunkEndorsementsState; use near_primitives::types::{AccountId, EpochId, ShardId}; use std::collections::HashMap; use std::num::NonZeroUsize; use crate::metrics; -use crate::stateless_validation::chunk_endorsement::{ - ChunkEndorsementTracker, ChunkEndorsementsState, -}; +use crate::stateless_validation::chunk_endorsement::ChunkEndorsementTracker; const CHUNK_HEADERS_FOR_INCLUSION_CACHE_SIZE: usize = 2048; const NUM_EPOCH_CHUNK_PRODUCERS_TO_KEEP_IN_BLOCKLIST: usize = 1000; @@ -27,12 +26,6 @@ struct ChunkInfo { pub endorsements: ChunkEndorsementsState, } -impl ChunkInfo { - fn is_endorsed(&self) -> bool { - matches!(self.endorsements, ChunkEndorsementsState::Endorsed(_, _)) - } -} - pub struct ChunkInclusionTracker { // Track chunks that are ready to be included in a block. // Key is the previous_block_hash as the chunk is created based on this block. It's possible that @@ -88,7 +81,7 @@ impl ChunkInclusionTracker { chunk_header, received_time: Utc::now_utc(), chunk_producer, - endorsements: ChunkEndorsementsState::NotEnoughStake(None), + endorsements: ChunkEndorsementsState::default(), }; self.chunk_hash_to_chunk_info.insert(chunk_hash, chunk_info); } @@ -155,8 +148,8 @@ impl ChunkInclusionTracker { for (shard_id, chunk_hash) in entry { let chunk_info = self.chunk_hash_to_chunk_info.get(chunk_hash).unwrap(); let banned = self.is_banned(epoch_id, &chunk_info); - let has_chunk_endorsements = chunk_info.is_endorsed(); - if !has_chunk_endorsements { + let is_endorsed = chunk_info.endorsements.is_endorsed; + if !is_endorsed { tracing::debug!( target: "client", chunk_hash = ?chunk_info.chunk_header.chunk_hash(), @@ -164,7 +157,7 @@ impl ChunkInclusionTracker { "Not including chunk because of insufficient chunk endorsements" ); } - if !banned && has_chunk_endorsements { + if !banned && is_endorsed { // only add to chunk_headers_ready_for_inclusion if chunk is not from a banned chunk producer // and chunk has sufficient chunk endorsements. // Chunk endorsements are got as part of call to prepare_chunk_headers_ready_for_inclusion @@ -203,10 +196,7 @@ impl ChunkInclusionTracker { ) -> Result<(ShardChunkHeader, ChunkEndorsementSignatures), Error> { let chunk_info = self.get_chunk_info(chunk_hash)?; let chunk_header = chunk_info.chunk_header.clone(); - let signatures = match &chunk_info.endorsements { - ChunkEndorsementsState::Endorsed(_, signatures) => signatures.clone(), - ChunkEndorsementsState::NotEnoughStake(_) => vec![], - }; + let signatures = chunk_info.endorsements.signatures.clone(); Ok((chunk_header, signatures)) } @@ -228,9 +218,10 @@ impl ChunkInclusionTracker { log_assert_fail!("Chunk info is missing for shard {shard_id} chunk {chunk_hash:?}"); continue; }; - let Some(stats) = chunk_info.endorsements.stats() else { + let stats = &chunk_info.endorsements; + if stats.total_stake == 0 { continue; - }; + } let shard_label = shard_id.to_string(); let label_values = &[shard_label.as_ref()]; metrics::BLOCK_PRODUCER_ENDORSED_STAKE_RATIO diff --git a/chain/client/src/debug.rs b/chain/client/src/debug.rs index b898c7869f7..47ca3fc1e4d 100644 --- a/chain/client/src/debug.rs +++ b/chain/client/src/debug.rs @@ -714,7 +714,7 @@ impl ClientActorInner { continue; } // Compute total stake and endorsed stake. - let mut endorsed_chunk_validators = HashSet::new(); + let mut endorsed_chunk_validators = HashMap::new(); for (account_id, signature) in ordered_chunk_validators.iter().zip(signatures) { let Some(signature) = signature else { continue }; let Ok((validator, _)) = self.client.epoch_manager.get_validator_by_account_id( @@ -731,13 +731,13 @@ impl ClientActorInner { ) { continue; } - endorsed_chunk_validators.insert(account_id); + endorsed_chunk_validators.insert(account_id, *signature.clone()); } - let endorsement_stats = - chunk_validator_assignments.compute_endorsement_stats(&endorsed_chunk_validators); + let endorsement_state = + chunk_validator_assignments.compute_endorsement_state(endorsed_chunk_validators); chunk_endorsements.insert( chunk_header.chunk_hash(), - endorsement_stats.endorsed_stake as f64 / endorsement_stats.total_stake as f64, + endorsement_state.endorsed_stake as f64 / endorsement_state.total_stake as f64, ); } Some(chunk_endorsements) diff --git a/chain/client/src/stateless_validation/chunk_endorsement/mod.rs b/chain/client/src/stateless_validation/chunk_endorsement/mod.rs index d7a77844dd9..3a1c8352450 100644 --- a/chain/client/src/stateless_validation/chunk_endorsement/mod.rs +++ b/chain/client/src/stateless_validation/chunk_endorsement/mod.rs @@ -3,10 +3,9 @@ use std::sync::Arc; use near_chain::ChainStoreAccess; use near_chain_primitives::Error; use near_epoch_manager::EpochManagerAdapter; -use near_primitives::block_body::ChunkEndorsementSignatures; use near_primitives::sharding::ShardChunkHeader; use near_primitives::stateless_validation::chunk_endorsement::ChunkEndorsement; -use near_primitives::stateless_validation::validator_assignment::EndorsementStats; +use near_primitives::stateless_validation::validator_assignment::ChunkEndorsementsState; use near_primitives::version::ProtocolFeature; use near_store::Store; @@ -15,20 +14,6 @@ use crate::Client; mod tracker_v1; mod tracker_v2; -pub enum ChunkEndorsementsState { - Endorsed(Option, ChunkEndorsementSignatures), - NotEnoughStake(Option), -} - -impl ChunkEndorsementsState { - pub fn stats(&self) -> Option<&EndorsementStats> { - match self { - Self::Endorsed(stats, _) => stats.as_ref(), - Self::NotEnoughStake(stats) => stats.as_ref(), - } - } -} - /// Module to track chunk endorsements received from chunk validators. pub struct ChunkEndorsementTracker { epoch_manager: Arc, diff --git a/chain/client/src/stateless_validation/chunk_endorsement/tracker_v1.rs b/chain/client/src/stateless_validation/chunk_endorsement/tracker_v1.rs index f88a52e92cd..30dc76f41ff 100644 --- a/chain/client/src/stateless_validation/chunk_endorsement/tracker_v1.rs +++ b/chain/client/src/stateless_validation/chunk_endorsement/tracker_v1.rs @@ -1,5 +1,6 @@ use lru::LruCache; use near_primitives::stateless_validation::chunk_endorsement::ChunkEndorsementV1; +use near_primitives::stateless_validation::validator_assignment::ChunkEndorsementsState; use std::collections::HashMap; use std::num::NonZeroUsize; use std::sync::{Arc, Mutex}; @@ -10,8 +11,6 @@ use near_primitives::checked_feature; use near_primitives::sharding::{ChunkHash, ShardChunkHeader}; use near_primitives::types::AccountId; -use super::ChunkEndorsementsState; - // This is the number of unique chunks for which we would track the chunk endorsements. // Ideally, we should not be processing more than num_shards chunks at a time. const NUM_CHUNKS_IN_CHUNK_ENDORSEMENTS_CACHE: usize = 100; @@ -104,13 +103,8 @@ impl ChunkEndorsementTracker { ) } - /// Called by block producer. - /// Returns ChunkEndorsementsState::Endorsed if node has enough signed stake for the chunk - /// represented by chunk_header. - /// Signatures have the same order as ordered_chunk_validators, thus ready to be included in a block as is. - /// Returns ChunkEndorsementsState::NotEnoughStake if chunk doesn't have enough stake. - /// For older protocol version, we return ChunkEndorsementsState::Endorsed with an empty array of - /// chunk endorsements. + /// This function is called by block producer potentially multiple times if there's not enough stake. + /// For older protocol version, we return an empty array of chunk endorsements. pub fn collect_chunk_endorsements( &self, chunk_header: &ShardChunkHeader, @@ -209,7 +203,10 @@ impl ChunkEndorsementTrackerInner { let protocol_version = self.epoch_manager.get_epoch_protocol_version(&epoch_id)?; if !checked_feature!("stable", StatelessValidation, protocol_version) { // Return an empty array of chunk endorsements for older protocol versions. - return Ok(ChunkEndorsementsState::Endorsed(None, vec![])); + return Ok(ChunkEndorsementsState { + is_endorsed: true, + ..ChunkEndorsementsState::default() + }); } let chunk_validator_assignments = self.epoch_manager.get_chunk_validator_assignments( @@ -226,29 +223,14 @@ impl ChunkEndorsementTrackerInner { self.chunk_endorsements.get(&chunk_header.chunk_hash()) else { // Early return if no chunk_endorsements found in our cache. - return Ok(ChunkEndorsementsState::NotEnoughStake(None)); + return Ok(ChunkEndorsementsState::default()); }; - let endorsement_stats = chunk_validator_assignments - .compute_endorsement_stats(&chunk_endorsements.keys().collect()); - - // Check whether the current set of chunk_validators have enough stake to include chunk in block. - if !endorsement_stats.has_enough_stake() { - return Ok(ChunkEndorsementsState::NotEnoughStake(Some(endorsement_stats))); - } - - // We've already verified the chunk_endorsements are valid, collect signatures. - let signatures = chunk_validator_assignments - .ordered_chunk_validators() - .iter() - .map(|account_id| { - // map Option to Option> - chunk_endorsements - .get(account_id) - .map(|endorsement| Box::new(endorsement.signature.clone())) - }) + let validator_signatures = chunk_endorsements + .into_iter() + .map(|(account_id, endorsement)| (account_id, endorsement.signature.clone())) .collect(); - Ok(ChunkEndorsementsState::Endorsed(Some(endorsement_stats), signatures)) + Ok(chunk_validator_assignments.compute_endorsement_state(validator_signatures)) } } diff --git a/chain/client/src/stateless_validation/chunk_endorsement/tracker_v2.rs b/chain/client/src/stateless_validation/chunk_endorsement/tracker_v2.rs index af26ea9e3ed..f6a3ee1cf5d 100644 --- a/chain/client/src/stateless_validation/chunk_endorsement/tracker_v2.rs +++ b/chain/client/src/stateless_validation/chunk_endorsement/tracker_v2.rs @@ -7,6 +7,7 @@ use near_chain_primitives::Error; use near_epoch_manager::EpochManagerAdapter; use near_primitives::sharding::ShardChunkHeader; use near_primitives::stateless_validation::chunk_endorsement::ChunkEndorsementV2; +use near_primitives::stateless_validation::validator_assignment::ChunkEndorsementsState; use near_primitives::stateless_validation::ChunkProductionKey; use near_primitives::types::AccountId; use near_primitives::version::ProtocolFeature; @@ -14,8 +15,6 @@ use near_store::Store; use crate::stateless_validation::validate::validate_chunk_endorsement; -use super::ChunkEndorsementsState; - // This is the number of unique chunks for which we would track the chunk endorsements. // Ideally, we should not be processing more than num_shards chunks at a time. const NUM_CHUNKS_IN_CHUNK_ENDORSEMENTS_CACHE: usize = 100; @@ -64,10 +63,6 @@ impl ChunkEndorsementTracker { } /// This function is called by block producer potentially multiple times if there's not enough stake. - /// We return ChunkEndorsementsState::Endorsed if node has enough signed stake for the chunk represented - /// by chunk_header. Signatures have the same order as ordered_chunk_validators, thus ready to be included - /// in a block as is. - /// We returns ChunkEndorsementsState::NotEnoughStake if chunk doesn't have enough stake. pub(crate) fn collect_chunk_endorsements( &mut self, chunk_header: &ShardChunkHeader, @@ -77,8 +72,11 @@ impl ChunkEndorsementTracker { self.epoch_manager.get_epoch_id_from_prev_block(chunk_header.prev_block_hash())?; let protocol_version = self.epoch_manager.get_epoch_protocol_version(&epoch_id)?; if !ProtocolFeature::StatelessValidation.enabled(protocol_version) { - // Return an empty array of chunk endorsements for older protocol versions. - return Ok(ChunkEndorsementsState::Endorsed(None, vec![])); + // Return an endorsed empty array of chunk endorsements for older protocol versions. + return Ok(ChunkEndorsementsState { + is_endorsed: true, + ..ChunkEndorsementsState::default() + }); } let height_created = chunk_header.height_created(); @@ -96,30 +94,13 @@ impl ChunkEndorsementTracker { // 1. The chunk endorsements are from valid chunk_validator for this chunk. // 2. The chunk endorsements signatures are valid. // 3. We still need to validate if the chunk_hash matches the chunk_header.chunk_hash() - let Some(entry) = self.chunk_endorsements.get_mut(&key) else { - // Early return if no chunk_endorsements found in our cache. - return Ok(ChunkEndorsementsState::NotEnoughStake(None)); - }; - entry.retain(|_, endorsement| endorsement.chunk_hash() == &chunk_header.chunk_hash()); - - let endorsement_stats = - chunk_validator_assignments.compute_endorsement_stats(&entry.keys().collect()); - - // Check whether the current set of chunk_validators have enough stake to include chunk in block. - if !endorsement_stats.has_enough_stake() { - return Ok(ChunkEndorsementsState::NotEnoughStake(Some(endorsement_stats))); - } - - // We've already verified the chunk_endorsements are valid, collect signatures. - let signatures = chunk_validator_assignments - .ordered_chunk_validators() - .iter() - .map(|account_id| { - // map Option to Option> - entry.get(account_id).map(|endorsement| Box::new(endorsement.signature())) - }) + let entry = self.chunk_endorsements.get_or_insert(key, || HashMap::new()); + let validator_signatures = entry + .into_iter() + .filter(|(_, endorsement)| endorsement.chunk_hash() == &chunk_header.chunk_hash()) + .map(|(account_id, endorsement)| (account_id, endorsement.signature())) .collect(); - Ok(ChunkEndorsementsState::Endorsed(Some(endorsement_stats), signatures)) + Ok(chunk_validator_assignments.compute_endorsement_state(validator_signatures)) } } diff --git a/core/primitives/src/stateless_validation/validator_assignment.rs b/core/primitives/src/stateless_validation/validator_assignment.rs index 358962d13d3..8a2d8176000 100644 --- a/core/primitives/src/stateless_validation/validator_assignment.rs +++ b/core/primitives/src/stateless_validation/validator_assignment.rs @@ -1,24 +1,28 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::fmt::Debug; +use near_crypto::Signature; use near_primitives_core::types::{AccountId, Balance}; -#[derive(Debug)] -pub struct EndorsementStats { +use crate::block_body::ChunkEndorsementSignatures; + +#[derive(Debug, Default)] +pub struct ChunkEndorsementsState { pub total_stake: Balance, pub endorsed_stake: Balance, pub total_validators_count: usize, pub endorsed_validators_count: usize, + pub is_endorsed: bool, + // Signatures are empty if the chunk is not endorsed + pub signatures: ChunkEndorsementSignatures, } -impl EndorsementStats { - pub fn has_enough_stake(&self) -> bool { - self.endorsed_stake >= self.required_stake() - } +fn has_enough_stake(total_stake: Balance, endorsed_stake: Balance) -> bool { + endorsed_stake >= required_stake(total_stake) +} - pub fn required_stake(&self) -> Balance { - self.total_stake * 2 / 3 + 1 - } +fn required_stake(total_stake: Balance) -> Balance { + total_stake * 2 / 3 + 1 } #[derive(Debug, Default)] @@ -49,25 +53,37 @@ impl ChunkValidatorAssignments { &self.assignments } - pub fn compute_endorsement_stats( + pub fn compute_endorsement_state( &self, - endorsed_chunk_validators: &HashSet<&AccountId>, - ) -> EndorsementStats { + mut validator_signatures: HashMap<&AccountId, Signature>, + ) -> ChunkEndorsementsState { let mut total_stake = 0; let mut endorsed_stake = 0; let mut endorsed_validators_count = 0; + let mut signatures = vec![]; for (account_id, stake) in &self.assignments { total_stake += stake; - if endorsed_chunk_validators.contains(account_id) { - endorsed_stake += stake; - endorsed_validators_count += 1; + match validator_signatures.remove(account_id) { + Some(signature) => { + endorsed_stake += stake; + endorsed_validators_count += 1; + signatures.push(Some(Box::new(signature))); + } + None => signatures.push(None), } } - EndorsementStats { + // Signatures are empty if the chunk is not endorsed + let is_endorsed = has_enough_stake(total_stake, endorsed_stake); + if !is_endorsed { + signatures.clear(); + } + ChunkEndorsementsState { total_stake, endorsed_stake, endorsed_validators_count, total_validators_count: self.assignments.len(), + is_endorsed, + signatures, } } } From 523837bfa37df2cff64043daa7471aa49333aaaf Mon Sep 17 00:00:00 2001 From: Shreyan Gupta Date: Mon, 23 Sep 2024 13:26:57 -0700 Subject: [PATCH 12/49] [memtrie] Fix refcount for HybridArena (#12118) On converting existing arena memory to shared memory, we lose the ability to refcount the shared memory part. However, while adding new nodes and new entries in the owned memory part, we could potentially be referring to the shared memory. In such cases, while adding or removing nodes, we would like to ignore all increments and decrements to refcounts in the shared part of memory. Added a unit test for expected scenario. --- core/store/src/trie/mem/arena/concurrent.rs | 4 ++ core/store/src/trie/mem/arena/frozen.rs | 13 ++++ core/store/src/trie/mem/arena/hybrid.rs | 4 ++ core/store/src/trie/mem/arena/mod.rs | 5 ++ .../store/src/trie/mem/arena/single_thread.rs | 4 ++ core/store/src/trie/mem/mem_tries.rs | 2 +- core/store/src/trie/mem/node/encoding.rs | 10 +++ core/store/src/trie/mem/updating.rs | 72 ++++++++++++++++++- 8 files changed, 111 insertions(+), 3 deletions(-) diff --git a/core/store/src/trie/mem/arena/concurrent.rs b/core/store/src/trie/mem/arena/concurrent.rs index 90c4687cc71..d7210edb31f 100644 --- a/core/store/src/trie/mem/arena/concurrent.rs +++ b/core/store/src/trie/mem/arena/concurrent.rs @@ -121,6 +121,10 @@ impl ArenaMemory for ConcurrentArenaMemory { } impl ArenaMemoryMut for ConcurrentArenaMemory { + fn is_mutable(&self, _pos: ArenaPos) -> bool { + true + } + fn raw_slice_mut(&mut self, pos: ArenaPos, len: usize) -> &mut [u8] { &mut self.chunk_mut(pos.chunk())[pos.pos()..pos.pos() + len] } diff --git a/core/store/src/trie/mem/arena/frozen.rs b/core/store/src/trie/mem/arena/frozen.rs index dc42d5f7ee2..0047c3b25f7 100644 --- a/core/store/src/trie/mem/arena/frozen.rs +++ b/core/store/src/trie/mem/arena/frozen.rs @@ -39,3 +39,16 @@ impl Arena for FrozenArena { &self.memory } } + +impl FrozenArena { + /// Number of active allocations (alloc calls minus dealloc calls). + #[cfg(test)] + pub fn num_active_allocs(&self) -> usize { + self.active_allocs_count + } + + #[cfg(test)] + pub fn active_allocs_bytes(&self) -> usize { + self.active_allocs_bytes + } +} diff --git a/core/store/src/trie/mem/arena/hybrid.rs b/core/store/src/trie/mem/arena/hybrid.rs index 1394d1b89eb..55500ee720d 100644 --- a/core/store/src/trie/mem/arena/hybrid.rs +++ b/core/store/src/trie/mem/arena/hybrid.rs @@ -55,6 +55,10 @@ impl ArenaMemory for HybridArenaMemory { } impl ArenaMemoryMut for HybridArenaMemory { + fn is_mutable(&self, pos: ArenaPos) -> bool { + pos.chunk >= self.chunks_offset() + } + fn raw_slice_mut(&mut self, mut pos: ArenaPos, len: usize) -> &mut [u8] { debug_assert!(!pos.is_invalid()); assert!(pos.chunk >= self.chunks_offset(), "Cannot mutate shared memory"); diff --git a/core/store/src/trie/mem/arena/mod.rs b/core/store/src/trie/mem/arena/mod.rs index c14b2e11a07..0dbf029bea1 100644 --- a/core/store/src/trie/mem/arena/mod.rs +++ b/core/store/src/trie/mem/arena/mod.rs @@ -64,6 +64,11 @@ pub trait ArenaMemory: Sized + 'static { /// A mutable reference to `ArenaMemory` can be used to mutate allocated /// memory, but not to allocate or deallocate memory. pub trait ArenaMemoryMut: ArenaMemory { + /// Returns whether the memory at the given position is mutable or not. + /// Normally, all memory is mutable, but in case of HybridArenaMemory, + /// we could be referring to a read-only memory part. + fn is_mutable(&self, _pos: ArenaPos) -> bool; + fn raw_slice_mut(&mut self, pos: ArenaPos, len: usize) -> &mut [u8]; /// Provides write access to a region of memory in the arena. diff --git a/core/store/src/trie/mem/arena/single_thread.rs b/core/store/src/trie/mem/arena/single_thread.rs index b7426ef76d8..43afffc84ee 100644 --- a/core/store/src/trie/mem/arena/single_thread.rs +++ b/core/store/src/trie/mem/arena/single_thread.rs @@ -17,6 +17,10 @@ impl ArenaMemory for STArenaMemory { } impl ArenaMemoryMut for STArenaMemory { + fn is_mutable(&self, _pos: ArenaPos) -> bool { + true + } + fn raw_slice_mut(&mut self, pos: ArenaPos, len: usize) -> &mut [u8] { &mut self.chunks[pos.chunk()][pos.pos()..pos.pos() + len] } diff --git a/core/store/src/trie/mem/mem_tries.rs b/core/store/src/trie/mem/mem_tries.rs index 5bc292a2159..5b51bf9b288 100644 --- a/core/store/src/trie/mem/mem_tries.rs +++ b/core/store/src/trie/mem/mem_tries.rs @@ -26,7 +26,7 @@ use super::updating::{construct_root_from_changes, MemTrieUpdate}; /// its children nodes. The `roots` field of this struct logically /// holds an Rc of the root of each trie. pub struct MemTries { - arena: HybridArena, + pub(super) arena: HybridArena, /// Maps a state root to a list of nodes that have the same root hash. /// The reason why this is a list is because we do not have a node /// deduplication mechanism so we can't guarantee that nodes of the diff --git a/core/store/src/trie/mem/node/encoding.rs b/core/store/src/trie/mem/node/encoding.rs index a56a4bdba05..f39fee2605c 100644 --- a/core/store/src/trie/mem/node/encoding.rs +++ b/core/store/src/trie/mem/node/encoding.rs @@ -228,6 +228,11 @@ impl MemTrieNodeId { /// Increments the refcount, returning the new refcount. pub(crate) fn add_ref(&self, memory: &mut impl ArenaMemoryMut) -> u32 { + // It's possible that in a hybrid memory setup, we are accessing the read-only part of memory. + // In that case, we don't need to increment the refcount. + if !memory.is_mutable(self.pos) { + return 1; + } // Refcount is always encoded as the first four bytes of the node memory. let refcount_memory = memory.raw_slice_mut(self.pos, size_of::()); let refcount = u32::from_le_bytes(refcount_memory.try_into().unwrap()); @@ -239,6 +244,11 @@ impl MemTrieNodeId { /// Decrements the refcount, deallocating the node if it reaches zero. /// Returns the new refcount. pub(crate) fn remove_ref(&self, arena: &mut impl ArenaWithDealloc) -> u32 { + // It's possible that in a hybrid memory setup, we are accessing the read-only part of memory. + // In that case, we don't need to decrement the refcount. + if !arena.memory_mut().is_mutable(self.pos) { + return 1; + } // Refcount is always encoded as the first four bytes of the node memory. let refcount_memory = arena.memory_mut().raw_slice_mut(self.pos, size_of::()); let refcount = u32::from_le_bytes(refcount_memory.try_into().unwrap()); diff --git a/core/store/src/trie/mem/updating.rs b/core/store/src/trie/mem/updating.rs index adfe6504bdc..a833ddca487 100644 --- a/core/store/src/trie/mem/updating.rs +++ b/core/store/src/trie/mem/updating.rs @@ -901,13 +901,15 @@ pub(super) fn construct_root_from_changes( #[cfg(test)] mod tests { use crate::test_utils::TestTriesBuilder; + use crate::trie::mem::arena::hybrid::HybridArena; use crate::trie::mem::lookup::memtrie_lookup; use crate::trie::mem::mem_tries::MemTries; use crate::trie::MemTrieChanges; use crate::{KeyLookupMode, ShardTries, TrieChanges}; + use near_primitives::hash::CryptoHash; use near_primitives::shard_layout::ShardUId; use near_primitives::state::{FlatStateValue, ValueRef}; - use near_primitives::types::StateRoot; + use near_primitives::types::{BlockHeight, StateRoot}; use rand::Rng; use std::collections::{HashMap, HashSet}; @@ -953,7 +955,6 @@ mod tests { let mut update = self.mem.update(self.state_root, false).unwrap_or_else(|_| { panic!("Trying to update root {:?} but it's not in memtries", self.state_root) }); - for (key, value) in changes { if let Some(value) = value { update.insert_memtrie_only(&key, FlatStateValue::on_disk(&value)); @@ -1324,4 +1325,71 @@ mod tests { tries.check_consistency_across_all_changes_and_apply(changes); } } + + fn insert_changes_to_memtrie( + memtrie: &mut MemTries, + prev_state_root: CryptoHash, + block_height: BlockHeight, + changes: &str, + ) -> CryptoHash { + let changes = parse_changes(changes); + let mut update = memtrie.update(prev_state_root, false).unwrap(); + + for (key, value) in changes { + if let Some(value) = value { + update.insert_memtrie_only(&key, FlatStateValue::on_disk(&value)); + } else { + update.delete(&key); + } + } + + let changes = update.to_mem_trie_changes_only(); + memtrie.apply_memtrie_changes(block_height, &changes) + } + + #[test] + fn test_gc_hybrid_memtrie() { + let state_root = StateRoot::default(); + let mut memtrie = MemTries::new(ShardUId::single_shard()); + assert!(!memtrie.arena.has_shared_memory()); + + // Insert in some initial data for height 0 + let changes = " + ff00 = 0000 + ff01 = 0100 + ff0101 = 0101 + "; + let state_root = insert_changes_to_memtrie(&mut memtrie, state_root, 0, changes); + + // Freeze the current memory in memtrie + let frozen_arena = memtrie.arena.freeze(); + let hybrid_arena = + HybridArena::from_frozen("test_hybrid".to_string(), frozen_arena.clone()); + memtrie.arena = hybrid_arena; + assert!(memtrie.arena.has_shared_memory()); + + // Insert in some more data for height 1 in hybrid memtrie + // Try to make sure we share some node allocations (ff01 and ff0101) with height 0 + // Node ff01 effectively has a refcount of 2, one from height 0 and one from height 1 + + let changes = " + ff0000 = 1000 + ff0001 = 1001 + "; + insert_changes_to_memtrie(&mut memtrie, state_root, 1, changes); + + // Now try to garbage collect the height 0 root + // Memory consumption should not change as height 0 is frozen + let num_active_allocs = memtrie.arena.num_active_allocs(); + let active_allocs_bytes = memtrie.arena.active_allocs_bytes(); + memtrie.delete_until_height(1); + assert_eq!(memtrie.arena.num_active_allocs(), num_active_allocs); + assert_eq!(memtrie.arena.active_allocs_bytes(), active_allocs_bytes); + + // Now try to garbage collect the height 1 root + // The final memory allocation should be what we had during the time of freezing + memtrie.delete_until_height(2); + assert_eq!(memtrie.arena.num_active_allocs(), frozen_arena.num_active_allocs()); + assert_eq!(memtrie.arena.active_allocs_bytes(), frozen_arena.active_allocs_bytes()); + } } From 70acea66bac9e5eeee64c0f417112f762e2a2b55 Mon Sep 17 00:00:00 2001 From: Razvan Barbascu Date: Mon, 23 Sep 2024 22:43:31 +0100 Subject: [PATCH 13/49] test(state-sync): shard swap in single shard tracking (#12108) Pytest to check decentralised state sync of nodes tracking one shard. Keep shard shuffling off until the implementation is done. This test sets a dumper node only for the state sync headers. Sets 4 validator nodes, each tracking 1 shard. Sets an RPC node to handle the random traffic that changes the state. Only allow validator nodes to share parts by enabling the state snapshot. Check if the network can got for 6 epochs while shard shuffling is on. Validator nodes are expected to download parts from each other. Check http://127.0.0.1:3040/debug/pages/epoch_info for the validator assignment rotation. --- nightly/pytest-sanity.txt | 5 + pytest/lib/cluster.py | 23 ++- .../tests/sanity/state_sync_decentralized.py | 141 ++++++++++++++++++ 3 files changed, 157 insertions(+), 12 deletions(-) create mode 100644 pytest/tests/sanity/state_sync_decentralized.py diff --git a/nightly/pytest-sanity.txt b/nightly/pytest-sanity.txt index 63bf49c65fe..0da6c0a2a0a 100644 --- a/nightly/pytest-sanity.txt +++ b/nightly/pytest-sanity.txt @@ -52,6 +52,11 @@ pytest --timeout=3600 sanity/state_sync_massive_validator.py pytest --timeout=3600 sanity/state_sync_massive.py --features nightly pytest --timeout=3600 sanity/state_sync_massive_validator.py --features nightly +# TODO(#12108): Enable the test again once decentralized state sync is implemented. +# pytest sanity/state_sync_decentralized.py +# TODO(#12108): Enable the test again once decentralized state sync is implemented. +# pytest sanity/state_sync_decentralized.py --features nightly + pytest sanity/sync_chunks_from_archival.py pytest sanity/sync_chunks_from_archival.py --features nightly pytest sanity/rpc_tx_forwarding.py diff --git a/pytest/lib/cluster.py b/pytest/lib/cluster.py index 2c7eb2bb9a1..43cc2c365de 100644 --- a/pytest/lib/cluster.py +++ b/pytest/lib/cluster.py @@ -1004,18 +1004,17 @@ def apply_config_changes(node_dir: str, # ClientConfig keys which are valid but may be missing from the config.json # file. Those are often Option types which are not stored in JSON file # when None. - allowed_missing_configs = ('archive', 'consensus.block_fetch_horizon', - 'consensus.min_block_production_delay', - 'consensus.max_block_production_delay', - 'consensus.max_block_wait_delay', - 'consensus.state_sync_timeout', - 'expected_shutdown', 'log_summary_period', - 'max_gas_burnt_view', 'rosetta_rpc', - 'save_trie_changes', 'split_storage', - 'state_sync', 'state_sync_enabled', - 'store.state_snapshot_enabled', - 'tracked_shard_schedule', 'cold_store', - 'store.load_mem_tries_for_tracked_shards') + allowed_missing_configs = ( + 'archive', 'consensus.block_fetch_horizon', + 'consensus.min_block_production_delay', + 'consensus.max_block_production_delay', + 'consensus.max_block_wait_delay', 'consensus.state_sync_timeout', + 'expected_shutdown', 'log_summary_period', 'max_gas_burnt_view', + 'rosetta_rpc', 'save_trie_changes', 'split_storage', 'state_sync', + 'state_sync_enabled', 'store.state_snapshot_enabled', + 'store.state_snapshot_config.state_snapshot_type', + 'tracked_shard_schedule', 'cold_store', + 'store.load_mem_tries_for_tracked_shards') for k, v in client_config_change.items(): if not (k in allowed_missing_configs or k in config_json): diff --git a/pytest/tests/sanity/state_sync_decentralized.py b/pytest/tests/sanity/state_sync_decentralized.py new file mode 100644 index 00000000000..35cd71d002a --- /dev/null +++ b/pytest/tests/sanity/state_sync_decentralized.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# Spins up 4 validating nodes. Let validators track a single shard. +# Add a dumper node for the state sync headers. +# Add an RPC node to issue tx and change the state. +# Send random transactions between accounts in different shards. +# Shuffle the shard assignment of validators and check if they can sync up. + +import unittest +import sys +import pathlib + +sys.path.append(str(pathlib.Path(__file__).resolve().parents[2] / 'lib')) + +from configured_logger import logger +from cluster import start_cluster +import state_sync_lib +from utils import wait_for_blocks +import simple_test + +EPOCH_LENGTH = 10 + +NUM_VALIDATORS = 4 + +# Shard layout with 5 roughly equal size shards for convenience. +SHARD_LAYOUT = { + "V1": { + "boundary_accounts": [ + "fff", + "lll", + "rrr", + ], + "version": 2, + "shards_split_map": [], + "to_parent_shard_map": [], + } +} + +NUM_SHARDS = len(SHARD_LAYOUT["V1"]["boundary_accounts"]) + 1 + +ALL_ACCOUNTS_PREFIXES = [ + "aaa", + "ggg", + "lll", + "sss", +] + + +class StateSyncValidatorShardSwap(unittest.TestCase): + + def _prepare_cluster(self, with_rpc=False, shuffle_shard_assignment=False): + (node_config_dump, + node_config_sync) = state_sync_lib.get_state_sync_configs_pair( + tracked_shards=None) + + # State snapshot is disabled for dumper. We only want to dump the headers. + node_config_dump["store.state_snapshot_enabled"] = False + node_config_dump[ + "store.state_snapshot_config.state_snapshot_type"] = "ForReshardingOnly" + + # State snapshot is enabled for validators. They will share parts of the state. + node_config_sync["store.state_snapshot_enabled"] = True + node_config_sync["tracked_shards"] = [] + + # Validators + configs = {x: node_config_sync.copy() for x in range(NUM_VALIDATORS)} + + # Dumper + configs[NUM_VALIDATORS] = node_config_dump + + if with_rpc: + # RPC + configs[NUM_VALIDATORS + 1] = node_config_sync.copy() + # RPC tracks all shards. + configs[NUM_VALIDATORS + 1]["tracked_shards"] = [0] + # RPC node does not participate in state parts distribution. + configs[NUM_VALIDATORS + 1]["store.state_snapshot_enabled"] = False + configs[NUM_VALIDATORS + 1][ + "store.state_snapshot_config.state_snapshot_type"] = "ForReshardingOnly" + + nodes = start_cluster( + num_nodes=NUM_VALIDATORS, + num_observers=1 + (1 if with_rpc else 0), + num_shards=NUM_SHARDS, + config=None, + genesis_config_changes=[ + ["epoch_length", EPOCH_LENGTH], ["shard_layout", SHARD_LAYOUT], + [ + "shuffle_shard_assignment_for_chunk_producers", + shuffle_shard_assignment + ], ["block_producer_kickout_threshold", 0], + ["chunk_producer_kickout_threshold", 0] + ], + client_config_changes=configs) + + for node in nodes: + node.stop_checking_store() + + self.dumper_node = nodes[NUM_VALIDATORS] + self.rpc_node = nodes[NUM_VALIDATORS + + 1] if with_rpc else self.dumper_node + self.nodes = nodes + self.validators = nodes[:NUM_VALIDATORS] + + def _prepare_simple_transfers(self): + self.testcase = simple_test.SimpleTransferBetweenAccounts( + nodes=self.nodes, + rpc_node=self.rpc_node, + account_prefixes=ALL_ACCOUNTS_PREFIXES, + epoch_length=EPOCH_LENGTH) + + self.testcase.wait_for_blocks(3) + + self.testcase.create_accounts() + + self.testcase.deploy_contracts() + + def _clear_cluster(self): + self.testcase = None + for node in self.nodes: + node.cleanup() + + def test_state_sync_with_shard_swap(self): + # Dumper node will not track any shard. So we need a dedicated RPC node. + # TODO: enable shuffle_shard_assignment after decentralized state sync is implemented. + self._prepare_cluster(with_rpc=True, shuffle_shard_assignment=False) + self._prepare_simple_transfers() + + target_height = 6 * EPOCH_LENGTH + self.testcase.random_workload_until(target_height) + + # Wait for all nodes to reach epoch 6. + for n in self.validators: + wait_for_blocks(n, target=target_height) + logger.info("Test ended") + + def tearDown(self): + self._clear_cluster() + + +if __name__ == '__main__': + unittest.main() From a033ff6e6e1f284ee305110e417f2ae82b1a325f Mon Sep 17 00:00:00 2001 From: Aleksandr Logunov Date: Tue, 24 Sep 2024 16:43:25 +0400 Subject: [PATCH 14/49] chore: fallback from nightly if compiler is broken (#12133) --- Justfile | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Justfile b/Justfile index a39f50c387b..cad6d9d02c4 100644 --- a/Justfile +++ b/Justfile @@ -165,12 +165,21 @@ check-protocol-schema: rustup toolchain install nightly rustup target add wasm32-unknown-unknown --toolchain nightly + # Below, we *should* have been used `cargo +nightly ...` instead of + # `RUSTC_BOOTSTRAP=1`. However, the env var appears to be more stable. + # `nightly` builds are updated daily and may be broken sometimes, e.g. + # https://github.com/rust-lang/rust/issues/130769. + # + # If there is an issue with the env var, fall back to `cargo +nightly ...`. + # Test that checker is not broken - RUSTFLAGS="--cfg enable_const_type_id" cargo +nightly test -p protocol-schema-check --profile dev-artifacts + RUSTC_BOOTSTRAP=1 RUSTFLAGS="--cfg enable_const_type_id" \ + cargo test -p protocol-schema-check --profile dev-artifacts + # Run the checker - RUSTFLAGS="--cfg enable_const_type_id" \ + RUSTC_BOOTSTRAP=1 RUSTFLAGS="--cfg enable_const_type_id" \ {{ with_macos_incremental }} \ - cargo +nightly run -p protocol-schema-check --profile dev-artifacts + cargo run -p protocol-schema-check --profile dev-artifacts check_build_public_libraries: cargo check {{public_libraries}} From 93c55755499a855a52e46301361be19e49a99463 Mon Sep 17 00:00:00 2001 From: Aleksandr Logunov Date: Tue, 24 Sep 2024 17:39:08 +0400 Subject: [PATCH 15/49] feat: full memtrie logic for range retain (#12130) Next step on #12074. Supporting all cases I came up with where nodes restructuring is required. Surprisingly, `squash_node` is enough to call. I only needed to implement trivial cases which weren't possible for single key deletion. I implemented the tests first, and majority of them failed before changing the logic. Each test is comparing naive approach with `retain_multi_range`. ### Notes * I'm a bit scared that I didn't realise the need to squash Extension before. Fortunately, `squash_node` handles that, but if you feel some cases are not covered here, feel free to post suggestions! * Reused + copypasted some Robin' tooling to generate interesting nodes conversions. * Note that test for reading "extra" child node is not required because we always read all children. ### Next steps * A bit more testing * Similar logic for partial trie * Generating intervals needed for resharding * Use that to implement shard switch on chain --- core/store/src/trie/mem/loading.rs | 64 ++--- core/store/src/trie/mem/mod.rs | 2 + core/store/src/trie/mem/nibbles_utils.rs | 46 ++++ core/store/src/trie/mem/resharding.rs | 334 ++++++++++++++++++++--- core/store/src/trie/mem/updating.rs | 142 +++++----- 5 files changed, 435 insertions(+), 153 deletions(-) create mode 100644 core/store/src/trie/mem/nibbles_utils.rs diff --git a/core/store/src/trie/mem/loading.rs b/core/store/src/trie/mem/loading.rs index 882392b1552..b98e3b15d5c 100644 --- a/core/store/src/trie/mem/loading.rs +++ b/core/store/src/trie/mem/loading.rs @@ -195,6 +195,7 @@ mod tests { }; use crate::trie::mem::loading::load_trie_from_flat_state; use crate::trie::mem::lookup::memtrie_lookup; + use crate::trie::mem::nibbles_utils::{all_two_nibble_nibbles, multi_hex_to_nibbles}; use crate::{DBCol, KeyLookupMode, NibbleSlice, ShardTries, Store, Trie, TrieUpdate}; use near_primitives::congestion_info::CongestionInfo; use near_primitives::hash::CryptoHash; @@ -300,18 +301,6 @@ mod tests { check_maybe_parallelize(keys, true); } - fn nibbles(hex: &str) -> Vec { - if hex == "_" { - return vec![]; - } - assert!(hex.len() % 2 == 0); - hex::decode(hex).unwrap() - } - - fn all_nibbles(hexes: &str) -> Vec> { - hexes.split_whitespace().map(|x| nibbles(x)).collect() - } - #[test] fn test_memtrie_empty() { check(vec![]); @@ -319,61 +308,42 @@ mod tests { #[test] fn test_memtrie_root_is_leaf() { - check(all_nibbles("_")); - check(all_nibbles("00")); - check(all_nibbles("01")); - check(all_nibbles("ff")); - check(all_nibbles("0123456789abcdef")); + check(multi_hex_to_nibbles("_")); + check(multi_hex_to_nibbles("00")); + check(multi_hex_to_nibbles("01")); + check(multi_hex_to_nibbles("ff")); + check(multi_hex_to_nibbles("0123456789abcdef")); } #[test] fn test_memtrie_root_is_extension() { - check(all_nibbles("1234 13 14")); - check(all_nibbles("12345678 1234abcd")); + check(multi_hex_to_nibbles("1234 13 14")); + check(multi_hex_to_nibbles("12345678 1234abcd")); } #[test] fn test_memtrie_root_is_branch() { - check(all_nibbles("11 22")); - check(all_nibbles("12345678 22345678 32345678")); - check(all_nibbles("11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff")); + check(multi_hex_to_nibbles("11 22")); + check(multi_hex_to_nibbles("12345678 22345678 32345678")); + check(multi_hex_to_nibbles("11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff")); } #[test] fn test_memtrie_root_is_branch_with_value() { - check(all_nibbles("_ 11")); + check(multi_hex_to_nibbles("_ 11")); } #[test] fn test_memtrie_prefix_patterns() { - check(all_nibbles("10 21 2210 2221 222210 222221 22222210 22222221")); - check(all_nibbles("11111112 11111120 111112 111120 1112 1120 12 20")); - check(all_nibbles("11 1111 111111 11111111 1111111111 111111111111")); - check(all_nibbles("_ 11 1111 111111 11111111 1111111111 111111111111")); + check(multi_hex_to_nibbles("10 21 2210 2221 222210 222221 22222210 22222221")); + check(multi_hex_to_nibbles("11111112 11111120 111112 111120 1112 1120 12 20")); + check(multi_hex_to_nibbles("11 1111 111111 11111111 1111111111 111111111111")); + check(multi_hex_to_nibbles("_ 11 1111 111111 11111111 1111111111 111111111111")); } #[test] fn test_full_16ary_trees() { - check(all_nibbles( - " - 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f - 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f - 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f - 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f - 40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f - 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f - 60 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f - 70 71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f - 80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f - 90 91 92 93 94 95 96 97 98 99 9a 9b 9c 9d 9e 9f - a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 aa ab ac ad ae af - b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf - c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc cd ce cf - d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df - e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef - f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff - ", - )) + check(all_two_nibble_nibbles()) } #[test] diff --git a/core/store/src/trie/mem/mod.rs b/core/store/src/trie/mem/mod.rs index 03fa125e495..f04381f3004 100644 --- a/core/store/src/trie/mem/mod.rs +++ b/core/store/src/trie/mem/mod.rs @@ -7,6 +7,8 @@ pub mod loading; mod lookup; pub mod mem_tries; pub mod metrics; +#[cfg(test)] +pub(crate) mod nibbles_utils; pub mod node; mod parallel_loader; pub mod resharding; diff --git a/core/store/src/trie/mem/nibbles_utils.rs b/core/store/src/trie/mem/nibbles_utils.rs new file mode 100644 index 00000000000..c0f5d168dac --- /dev/null +++ b/core/store/src/trie/mem/nibbles_utils.rs @@ -0,0 +1,46 @@ +/// Utilties for generating vectors of nibbles from human-readable strings. +/// +/// Input for a single vector is a hex string, e.g. 5da3593f. +/// It has even length, as tries support only keys in bytes, thus keys of +/// odd nibble length do not occur. +/// Each symbol is interpreted as a nibble (half-byte). +/// Result is a vector of decoded hexes as nibbles, e.g. +/// [5, 13, 10, 3, 5, 9, 3, 15]. + +pub(crate) fn hex_to_nibbles(hex: &str) -> Vec { + if hex == "_" { + return vec![]; + } + assert!(hex.len() % 2 == 0); + hex::decode(hex).unwrap() +} + +/// Converts a string of hex strings separated by whitespaces into a vector of +/// vectors of nibbles. For example, "01 02 10" is converted to +/// [[0, 1], [0, 2], [1, 0]]. +pub(crate) fn multi_hex_to_nibbles(hexes: &str) -> Vec> { + hexes.split_whitespace().map(|x| hex_to_nibbles(x)).collect() +} + +pub(crate) fn all_two_nibble_nibbles() -> Vec> { + multi_hex_to_nibbles( + " + 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f + 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f + 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f + 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f + 40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f + 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f + 60 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f + 70 71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f + 80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f + 90 91 92 93 94 95 96 97 98 99 9a 9b 9c 9d 9e 9f + a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 aa ab ac ad ae af + b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf + c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc cd ce cf + d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df + e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef + f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff + ", + ) +} diff --git a/core/store/src/trie/mem/resharding.rs b/core/store/src/trie/mem/resharding.rs index 281e828abba..6bc1b98ff80 100644 --- a/core/store/src/trie/mem/resharding.rs +++ b/core/store/src/trie/mem/resharding.rs @@ -103,6 +103,7 @@ impl<'a, M: ArenaMemory> MemTrieUpdate<'a, M> { } else { self.place_node(node_id, UpdatedMemTrieNode::Leaf { extension, value }); } + return; } UpdatedMemTrieNode::Branch { mut children, mut value } => { if !intervals_nibbles.iter().any(|interval| interval.contains(&key_nibbles)) { @@ -128,9 +129,6 @@ impl<'a, M: ArenaMemory> MemTrieUpdate<'a, M> { } } - // TODO(#12074): squash the branch if needed. Consider reusing - // `squash_nodes`. - self.place_node(node_id, UpdatedMemTrieNode::Branch { children, value }); } UpdatedMemTrieNode::Extension { extension, child } => { @@ -140,19 +138,16 @@ impl<'a, M: ArenaMemory> MemTrieUpdate<'a, M> { let child_key = [key_nibbles, extension_nibbles].concat(); self.retain_multi_range_recursive(new_child_id, child_key, intervals_nibbles); - if self.updated_nodes[new_child_id] == Some(UpdatedMemTrieNode::Empty) { - self.place_node(node_id, UpdatedMemTrieNode::Empty); - } else { - self.place_node( - node_id, - UpdatedMemTrieNode::Extension { - extension, - child: OldOrUpdatedNodeId::Updated(new_child_id), - }, - ); - } + let node = UpdatedMemTrieNode::Extension { + extension, + child: OldOrUpdatedNodeId::Updated(new_child_id), + }; + self.place_node(node_id, node); } } + + // We may need to change node type to keep the trie structure unique. + self.squash_node(node_id); } } @@ -190,28 +185,86 @@ fn retain_decision(key: &[u8], intervals: &[Range>]) -> RetainDecision { } // TODO(#12074): tests for -// - multiple retain ranges -// - result is empty, or no changes are made -// - removing keys one-by-one gives the same result as corresponding range retain // - `retain_split_shard` API -// - all results of squashing branch -// - checking not accessing not-inlined nodes +// - checking not accessing not-inlined values // - proof correctness #[cfg(test)] mod tests { + use rand::rngs::StdRng; + use rand::seq::SliceRandom; + use rand::{Rng, SeedableRng}; + use std::ops::Range; use std::sync::Arc; use itertools::Itertools; use near_primitives::{shard_layout::ShardUId, types::StateRoot}; use crate::{ + test_utils::TestTriesBuilder, trie::{ - mem::{iter::MemTrieIterator, mem_tries::MemTries}, + mem::{ + iter::MemTrieIterator, + mem_tries::MemTries, + nibbles_utils::{all_two_nibble_nibbles, hex_to_nibbles, multi_hex_to_nibbles}, + }, trie_storage::TrieMemoryPartialStorage, }, Trie, }; + // Logic for a single test. + // Creates trie from initial entries, applies retain multi range to it and + // compares the result with naive approach. + fn run(initial_entries: Vec<(Vec, Vec)>, retain_multi_ranges: Vec>>) { + // Generate naive result and state root. + let mut retain_result_naive = initial_entries + .iter() + .filter(|&(key, _)| retain_multi_ranges.iter().any(|range| range.contains(key))) + .cloned() + .collect_vec(); + retain_result_naive.sort(); + + let shard_tries = TestTriesBuilder::new().build(); + let changes = retain_result_naive + .iter() + .map(|(key, value)| (key.clone(), Some(value.clone()))) + .collect_vec(); + let expected_state_root = crate::test_utils::test_populate_trie( + &shard_tries, + &Trie::EMPTY_ROOT, + ShardUId::single_shard(), + changes, + ); + + let mut memtries = MemTries::new(ShardUId::single_shard()); + let mut update = memtries.update(Trie::EMPTY_ROOT, false).unwrap(); + for (key, value) in initial_entries { + update.insert(&key, value); + } + let memtrie_changes = update.to_mem_trie_changes_only(); + let state_root = memtries.apply_memtrie_changes(0, &memtrie_changes); + + let update = memtries.update(state_root, true).unwrap(); + let (mut trie_changes, _) = update.retain_multi_range(&retain_multi_ranges); + let memtrie_changes = trie_changes.mem_trie_changes.take().unwrap(); + let new_state_root = memtries.apply_memtrie_changes(1, &memtrie_changes); + + let entries = if new_state_root != StateRoot::default() { + let state_root_ptr = memtries.get_root(&new_state_root).unwrap(); + let trie = + Trie::new(Arc::new(TrieMemoryPartialStorage::default()), new_state_root, None); + MemTrieIterator::new(Some(state_root_ptr), &trie).map(|e| e.unwrap()).collect_vec() + } else { + vec![] + }; + + // Check entries first to provide more context in case of failure. + assert_eq!(entries, retain_result_naive); + + // Check state root, because it must be unique. + assert_eq!(new_state_root, expected_state_root); + } + #[test] /// Applies single range retain to the trie and checks the result. fn test_retain_single_range() { @@ -222,27 +275,234 @@ mod tests { (b"david".to_vec(), vec![4]), ]; let retain_range = b"amy".to_vec()..b"david".to_vec(); - let retain_result = vec![(b"bob".to_vec(), vec![2]), (b"charlie".to_vec(), vec![3])]; + run(initial_entries, vec![retain_range]); + } - let mut memtries = MemTries::new(ShardUId::single_shard()); - let empty_state_root = StateRoot::default(); - let mut update = memtries.update(empty_state_root, false).unwrap(); - for (key, value) in initial_entries { - update.insert(&key, value); + #[test] + /// Applies two ranges retain to the trie and checks the result. + fn test_retain_two_ranges() { + let initial_entries = vec![ + (b"alice".to_vec(), vec![1]), + (b"bob".to_vec(), vec![2]), + (b"charlie".to_vec(), vec![3]), + (b"david".to_vec(), vec![4]), + (b"edward".to_vec(), vec![5]), + (b"frank".to_vec(), vec![6]), + ]; + let retain_ranges = + vec![b"bill".to_vec()..b"bowl".to_vec(), b"daaa".to_vec()..b"france".to_vec()]; + run(initial_entries, retain_ranges); + } + + #[test] + /// Checks case when no keys are retained. + fn test_empty_result() { + let initial_entries = vec![ + (b"alice".to_vec(), vec![1]), + (b"miles".to_vec(), vec![2]), + (b"willy".to_vec(), vec![3]), + ]; + let retain_ranges = vec![b"ellie".to_vec()..b"key".to_vec()]; + run(initial_entries, retain_ranges); + } + + #[test] + /// Checks case when all keys are retained. + fn test_full_result() { + let initial_entries = vec![ + (b"f23".to_vec(), vec![1]), + (b"f32".to_vec(), vec![2]), + (b"f44".to_vec(), vec![3]), + ]; + let retain_ranges = vec![b"f11".to_vec()..b"f45".to_vec()]; + run(initial_entries, retain_ranges); + } + + #[test] + /// Checks empty trie. + fn test_empty_trie() { + let initial_entries = vec![]; + let retain_ranges = vec![b"bar".to_vec()..b"foo".to_vec()]; + run(initial_entries, retain_ranges); + } + + #[test] + /// Checks case when all keys are prefixes of some string. + fn test_prefixes() { + let initial_entries = vec![ + (b"a".to_vec(), vec![1]), + (b"aa".to_vec(), vec![2]), + (b"aaa".to_vec(), vec![3]), + (b"aaaa".to_vec(), vec![1]), + (b"aaaaa".to_vec(), vec![2]), + (b"aaaaaa".to_vec(), vec![3]), + ]; + let retain_ranges = vec![b"aa".to_vec()..b"aaaaa".to_vec()]; + run(initial_entries, retain_ranges); + } + + #[test] + /// Checks case when branch and extension nodes are explored but completely + /// removed. + fn test_descend_and_remove() { + let keys = multi_hex_to_nibbles("00 0000 0011"); + let initial_entries = keys.into_iter().map(|key| (key, vec![1])).collect_vec(); + let retain_ranges = vec![hex_to_nibbles("0001")..hex_to_nibbles("0010")]; + run(initial_entries, retain_ranges); + } + + #[test] + /// Checks case when branch is converted to leaf. + fn test_branch_to_leaf() { + let keys = multi_hex_to_nibbles("ba bc ca"); + let initial_entries = keys.into_iter().map(|key| (key, vec![1])).collect_vec(); + let retain_ranges = vec![hex_to_nibbles("bc")..hex_to_nibbles("be")]; + run(initial_entries, retain_ranges); + } + + #[test] + /// Checks case when branch with value is converted to leaf. + fn test_branch_with_value_to_leaf() { + let keys = multi_hex_to_nibbles("d4 d4a3 d4b9 d5 e6"); + let initial_entries = keys.into_iter().map(|key| (key, vec![1])).collect_vec(); + let retain_ranges = vec![hex_to_nibbles("d4")..hex_to_nibbles("d4a0")]; + run(initial_entries, retain_ranges); + } + + #[test] + /// Checks case when branch without value is converted to extension. + fn test_branch_to_extension() { + let keys = multi_hex_to_nibbles("21 2200 2201"); + let initial_entries = keys.into_iter().map(|key| (key, vec![1])).collect_vec(); + let retain_ranges = vec![hex_to_nibbles("2200")..hex_to_nibbles("2202")]; + run(initial_entries, retain_ranges); + } + + #[test] + /// Checks case when result is a single key, and all nodes on the way are + /// squashed, in particular, extension nodes are joined into one. + fn test_extend_extensions() { + let keys = multi_hex_to_nibbles("dd d0 d1 dddd00 dddd01 dddddd"); + let initial_entries = keys.into_iter().map(|key| (key, vec![1])).collect_vec(); + let retain_ranges = vec![hex_to_nibbles("dddddd")..hex_to_nibbles("ddddde")]; + run(initial_entries, retain_ranges); + } + + #[test] + /// Checks case when branch is visited but not restructured. + fn test_branch_not_restructured() { + let keys = multi_hex_to_nibbles("60 61 62 70"); + let initial_entries = keys.into_iter().map(|key| (key, vec![1])).collect_vec(); + let retain_ranges = vec![hex_to_nibbles("61")..hex_to_nibbles("71")]; + run(initial_entries, retain_ranges); + } + + #[test] + /// Checks case with branching on every step but when only prefixes of some + /// key are retained. + fn test_branch_prefixes() { + let keys = multi_hex_to_nibbles( + " + 00 + 10 + 01 + 0000 + 0010 + 0001 + 000000 + 000010 + 000001 + 00000000 + 00000010 + 00000001 + 0000000000 + 0000000010 + 0000000001 + 000000000000 + 000000000010 + 000000000011 + ", + ); + let initial_entries = keys.into_iter().map(|key| (key, vec![1])).collect_vec(); + let retain_ranges = vec![hex_to_nibbles("0000")..hex_to_nibbles("00000000")]; + run(initial_entries, retain_ranges); + } + + #[test] + /// Checks multiple ranges retain on full 16-ary tree. + fn test_full_16ary() { + let keys = all_two_nibble_nibbles(); + let initial_entries = keys.into_iter().map(|key| (key, vec![1])).collect_vec(); + let retain_ranges = vec![ + hex_to_nibbles("0f")..hex_to_nibbles("10"), + hex_to_nibbles("20")..hex_to_nibbles("2fff"), + hex_to_nibbles("55")..hex_to_nibbles("56"), + hex_to_nibbles("a5aa")..hex_to_nibbles("c3"), + hex_to_nibbles("c3")..hex_to_nibbles("c5"), + hex_to_nibbles("c8")..hex_to_nibbles("ca"), + hex_to_nibbles("cb")..hex_to_nibbles("cc"), + ]; + run(initial_entries, retain_ranges); + } + + fn random_key(max_key_len: usize, rng: &mut StdRng) -> Vec { + let key_len = rng.gen_range(0..=max_key_len); + let mut key = Vec::new(); + for _ in 0..key_len { + let byte: u8 = rng.gen(); + key.push(byte); } - let memtrie_changes = update.to_mem_trie_changes_only(); - let state_root = memtries.apply_memtrie_changes(0, &memtrie_changes); + key + } - let update = memtries.update(state_root, true).unwrap(); - let (mut trie_changes, _) = update.retain_multi_range(&[retain_range]); - let memtrie_changes = trie_changes.mem_trie_changes.take().unwrap(); - let new_state_root = memtries.apply_memtrie_changes(1, &memtrie_changes); + fn check_random(max_key_len: usize, max_keys_count: usize, test_count: usize) { + let mut rng = StdRng::seed_from_u64(442); + for _ in 0..test_count { + let key_cnt = rng.gen_range(1..=max_keys_count); + let mut keys = Vec::new(); + for _ in 0..key_cnt { + keys.push(random_key(max_key_len, &mut rng)); + } + keys.sort(); + keys.dedup(); + keys.shuffle(&mut rng); - let state_root_ptr = memtries.get_root(&new_state_root).unwrap(); - let trie = Trie::new(Arc::new(TrieMemoryPartialStorage::default()), new_state_root, None); - let entries = - MemTrieIterator::new(Some(state_root_ptr), &trie).map(|e| e.unwrap()).collect_vec(); + let mut boundary_left = random_key(max_key_len, &mut rng); + let mut boundary_right = random_key(max_key_len, &mut rng); + if boundary_left == boundary_right { + continue; + } + if boundary_left > boundary_right { + std::mem::swap(&mut boundary_left, &mut boundary_right); + } + let initial_entries = keys.into_iter().map(|key| (key, vec![1])).collect_vec(); + let retain_ranges = vec![boundary_left..boundary_right]; + run(initial_entries, retain_ranges); + } + } - assert_eq!(entries, retain_result); + #[test] + fn test_rand_small() { + check_random(3, 20, 10); + } + + #[test] + fn test_rand_many_keys() { + check_random(5, 1000, 10); + } + + #[test] + fn test_rand_long_keys() { + check_random(20, 100, 10); + } + + #[test] + fn test_rand_long_long_keys() { + check_random(1000, 1000, 1); + } + + #[test] + fn test_rand_large_data() { + check_random(32, 100000, 1); } } diff --git a/core/store/src/trie/mem/updating.rs b/core/store/src/trie/mem/updating.rs index a833ddca487..f6718da6020 100644 --- a/core/store/src/trie/mem/updating.rs +++ b/core/store/src/trie/mem/updating.rs @@ -508,76 +508,86 @@ impl<'a, M: ArenaMemory> MemTrieUpdate<'a, M> { } } - self.squash_nodes(path); + // We may need to change node type to keep the trie structure unique. + for node_id in path.into_iter().rev() { + self.squash_node(node_id); + } } - /// As we delete a key, it may be necessary to change the types of the nodes - /// along the path from the root to the key, in order to keep the trie - /// structure unique. For example, if a branch node has only one child and - /// no value, it must be converted to an extension node. If that extension - /// node also has a parent that is an extension node, they must be combined - /// into a single extension node. This function takes care of all these - /// cases. - fn squash_nodes(&mut self, path: Vec) { - // Correctness can be shown by induction on path prefix. - for node_id in path.into_iter().rev() { - let node = self.take_node(node_id); - match node { - UpdatedMemTrieNode::Empty => { - // Empty node will be absorbed by its parent node, so defer that. - self.place_node(node_id, UpdatedMemTrieNode::Empty); - } - UpdatedMemTrieNode::Leaf { .. } => { - // It's impossible that we would squash a leaf node, because if we - // had deleted a leaf it would become Empty instead. - unreachable!(); - } - UpdatedMemTrieNode::Branch { mut children, value } => { - // Remove any children that are now empty (removed). - for child in children.iter_mut() { - if let Some(OldOrUpdatedNodeId::Updated(child_node_id)) = child { - if let UpdatedMemTrieNode::Empty = - self.updated_nodes[*child_node_id as usize].as_ref().unwrap() - { - *child = None; - } + /// When we delete keys, it may be necessary to change types of some nodes, + /// in order to keep the trie structure unique. For example, if a branch + /// had two children, but after deletion ended up with one child and no + /// value, it must be converted to an extension node. Or, if an extension + /// node ended up having a child which is also an extension node, they must + /// be combined into a single extension node. This function takes care of + /// all these cases for a single node. + /// + /// To restructure trie correctly, this function must be called in + /// post-order traversal for every modified node. It may be proven by + /// induction on subtrees. + /// For single key removal, it is called for every node on the path from + /// the leaf to the root. + /// For range removal, it is called in the end of recursive range removal + /// function, which is the definition of post-order traversal. + pub(crate) fn squash_node(&mut self, node_id: UpdatedMemTrieNodeId) { + let node = self.take_node(node_id); + match node { + UpdatedMemTrieNode::Empty => { + // Empty node will be absorbed by its parent node, so defer that. + self.place_node(node_id, UpdatedMemTrieNode::Empty); + } + UpdatedMemTrieNode::Leaf { .. } => { + // It's impossible that we would squash a leaf node, because if we + // had deleted a leaf it would become Empty instead. + unreachable!(); + } + UpdatedMemTrieNode::Branch { mut children, value } => { + // Remove any children that are now empty (removed). + for child in children.iter_mut() { + if let Some(OldOrUpdatedNodeId::Updated(child_node_id)) = child { + if let UpdatedMemTrieNode::Empty = + self.updated_nodes[*child_node_id as usize].as_ref().unwrap() + { + *child = None; } } - let num_children = children.iter().filter(|node| node.is_some()).count(); - if num_children == 0 { - // Branch with zero children becomes leaf. It's not possible for it to - // become empty, because a branch had at least two children or a value - // and at least one child, so deleting a single value could not - // eliminate both of them. - let leaf_node = UpdatedMemTrieNode::Leaf { - extension: NibbleSlice::new(&[]) - .encoded(true) - .into_vec() - .into_boxed_slice(), - value: value.unwrap(), - }; - self.place_node(node_id, leaf_node); - } else if num_children == 1 && value.is_none() { - // Branch with 1 child but no value becomes extension. - let (idx, child) = children - .into_iter() - .enumerate() - .find_map(|(idx, node)| node.map(|node| (idx, node))) - .unwrap(); - let extension = NibbleSlice::new(&[(idx << 4) as u8]) - .encoded_leftmost(1, false) - .into_vec() - .into_boxed_slice(); - self.extend_child(node_id, extension, child); - } else { - // Branch with more than 1 children stays branch. - self.place_node(node_id, UpdatedMemTrieNode::Branch { children, value }); - } } - UpdatedMemTrieNode::Extension { extension, child } => { + let num_children = children.iter().filter(|node| node.is_some()).count(); + if num_children == 0 { + match value { + None => self.place_node(node_id, UpdatedMemTrieNode::Empty), + Some(value) => { + // Branch with zero children and a value becomes leaf. + let leaf_node = UpdatedMemTrieNode::Leaf { + extension: NibbleSlice::new(&[]) + .encoded(true) + .into_vec() + .into_boxed_slice(), + value, + }; + self.place_node(node_id, leaf_node); + } + } + } else if num_children == 1 && value.is_none() { + // Branch with 1 child but no value becomes extension. + let (idx, child) = children + .into_iter() + .enumerate() + .find_map(|(idx, node)| node.map(|node| (idx, node))) + .unwrap(); + let extension = NibbleSlice::new(&[(idx << 4) as u8]) + .encoded_leftmost(1, false) + .into_vec() + .into_boxed_slice(); self.extend_child(node_id, extension, child); + } else { + // Branch with more than 1 children stays branch. + self.place_node(node_id, UpdatedMemTrieNode::Branch { children, value }); } } + UpdatedMemTrieNode::Extension { extension, child } => { + self.extend_child(node_id, extension, child); + } } } @@ -596,13 +606,7 @@ impl<'a, M: ArenaMemory> MemTrieUpdate<'a, M> { let child_node = self.take_node(child_id); match child_node { UpdatedMemTrieNode::Empty => { - // This case is not possible. In a trie in general, an extension - // node could only have a child that is a branch (possibly with - // value) node. But a branch node either has a value and at least - // one child, or has at least two children. In either case, it's - // impossible for a single deletion to cause the child to become - // empty. - unreachable!(); + self.place_node(node_id, UpdatedMemTrieNode::Empty); } // If the child is a leaf (which could happen if a branch node lost // all its branches and only had a value left, or is left with only From cae3d22d1ee8043c333c75753ad224c618989407 Mon Sep 17 00:00:00 2001 From: Aleksandr Logunov Date: Tue, 24 Sep 2024 17:39:30 +0400 Subject: [PATCH 16/49] fix: branch restructuring test (#12128) I accidentally made a check nearly trivial and no one noticed :) https://github.com/near/nearcore/pull/11071/files#diff-e063ceca4433a740750d3c14a1f8f6d6c3b0a722abcadb633290da497507f633L447-L449 We want to check branch restructuring case, when a child node must be read even though it is not directly queried. --- core/store/src/trie/trie_tests.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/store/src/trie/trie_tests.rs b/core/store/src/trie/trie_tests.rs index 68b23311189..05626ae2c24 100644 --- a/core/store/src/trie/trie_tests.rs +++ b/core/store/src/trie/trie_tests.rs @@ -458,9 +458,9 @@ mod trie_storage_tests { #[test] fn test_memtrie_recorded_branch_restructuring() { test_memtrie_and_disk_updates_consistency(vec![ - (vec![7], Some(vec![1])), - (vec![7, 0], Some(vec![2])), - (vec![7, 1], Some(vec![3])), + (vec![7], Some(vec![10])), + (vec![7, 0], None), + (vec![7, 6], Some(vec![8])), ]); } From 8a18ee72cf950a17072b58b7a07c94a398346888 Mon Sep 17 00:00:00 2001 From: Razvan Barbascu Date: Tue, 24 Sep 2024 14:40:19 +0100 Subject: [PATCH 17/49] fix(fork-network): flush the first pass changes before starting the second pass (#12121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is a corner case where the last batch of changes is smaller than the batch threshold and it may not get flushed before the seconds pass. This will recreate the keys for the accounts that were deleted in the uncommitted batch. To test this, we created a network with a validator and an RPC node.
Created an account (`A` with key `Ka`) by sending some `$N` to it.
 Add another key to that account (`Kb`). Now `A` has `Ka` and `Kb` as full access keys.
 Delete `Ka`. Now `A` has just `Kb` as full access key. On this state, we run `fork-network`. 
In the first pass, we change account `A` to `rA` and delete `A`. Change key `Kb` to `rKb` and delete `Kb`. 
Without this fix, the default batch size is too large to flush the changes from the first pass of `prepare_shard_state`.
 Moving to the second pass, the `A` implicit account is still in the flat state and we will add a key to it. If we flush before the second pass, account `A` will no longer be in the flat state. --- tools/fork-network/src/cli.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tools/fork-network/src/cli.rs b/tools/fork-network/src/cli.rs index 731b7fe7a6b..8c17e41f9e3 100644 --- a/tools/fork-network/src/cli.rs +++ b/tools/fork-network/src/cli.rs @@ -620,6 +620,25 @@ impl ForkNetworkCommand { } } + // Commit the remaining updates. + if storage_mutator.should_commit(1) { + tracing::info!( + ?shard_uid, + ref_keys_retrieved, + records_parsed, + updated = access_keys_updated + + accounts_implicit_updated + + contract_data_updated + + contract_code_updated + + postponed_receipts_updated + + index_delayed_receipt + + received_data_updated, + ); + let state_root = storage_mutator.commit(&shard_uid, fake_block_height)?; + fake_block_height += 1; + storage_mutator = make_storage_mutator(state_root)?; + } + tracing::info!( ?shard_uid, ref_keys_retrieved, From 6b76ee63c65ff556b45cd2f70c9782bd26ec5b93 Mon Sep 17 00:00:00 2001 From: Razvan Barbascu Date: Wed, 25 Sep 2024 12:12:27 +0100 Subject: [PATCH 18/49] feat(release): Add epoch start estimator script (#12114) Usage: ``` python3 estimate_epoch_start_time.py --url https://archival-rpc.mainnet.near.org --num_future_epochs 10 --num_epochs 20 ```
Example output ``` Epoch 1: 48616.817671756 seconds Epoch 2: 48008.022047789 seconds Epoch 3: 51080.074328883 seconds Epoch 4: 50294.362538916 seconds Epoch 5: 50321.011121149 seconds Epoch 6: 47953.952447657 seconds Epoch 7: 49785.977865728 seconds Epoch 8: 48770.992102473 seconds Epoch 9: 47790.67163107 seconds Epoch 10: 47881.711552879 seconds Epoch 11: 47893.72761854 seconds Epoch 12: 48008.929334719 seconds Epoch 13: 48079.328908341 seconds Epoch 14: 49524.900702396 seconds Epoch 15: 51087.296069337 seconds Epoch 16: 50241.39822162 seconds Epoch 17: 48424.705606747 seconds Epoch 18: 49929.137882208 seconds Epoch 19: 47886.499544935 seconds Epoch 20: 47977.195690047 seconds Exponential weighted average epoch length: 49064.81392174163 seconds Predicted start of epoch 1: 2024-09-20 13:07:05 Friday Predicted start of epoch 2: 2024-09-21 02:44:50 Saturday Predicted start of epoch 3: 2024-09-21 16:22:35 Saturday Predicted start of epoch 4: 2024-09-22 06:00:20 Sunday Predicted start of epoch 5: 2024-09-22 19:38:04 Sunday Predicted start of epoch 6: 2024-09-23 09:15:49 Monday Predicted start of epoch 7: 2024-09-23 22:53:34 Monday Predicted start of epoch 8: 2024-09-24 12:31:19 Tuesday Predicted start of epoch 9: 2024-09-25 02:09:04 Wednesday Predicted start of epoch 10: 2024-09-25 15:46:48 Wednesday ```
--- debug_scripts/estimate_epoch_start_time.py | 161 +++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 debug_scripts/estimate_epoch_start_time.py diff --git a/debug_scripts/estimate_epoch_start_time.py b/debug_scripts/estimate_epoch_start_time.py new file mode 100644 index 00000000000..f5a9afee2df --- /dev/null +++ b/debug_scripts/estimate_epoch_start_time.py @@ -0,0 +1,161 @@ +import requests +import time +import math +import argparse + + +# Function to get block data +def get_block(url, block_hash): + payload = { + "jsonrpc": "2.0", + "id": "dontcare", + "method": "block", + } + + payload["params"] = { + "block_id": block_hash + } if block_hash is not None else { + "finality": "final" + } + + response = requests.post(url, json=payload) + return response.json()['result']['header'] + + +def ns_to_seconds(ns): + return ns / 1e9 + + +def format_time(seconds): + return time.strftime("%H hours, %M minutes", time.gmtime(seconds)) + + +# Function to fetch epoch lengths for the past n epochs and calculate the weighted average using exponential decay +def get_exponential_weighted_epoch_lengths(url, + starting_block_hash, + num_epochs, + decay_rate=0.1): + epoch_lengths = [] + current_hash = starting_block_hash + + for i in range(num_epochs): + # Get the block data by hash + block_data = get_block(url, current_hash) + + # Get the timestamp of this block (start of current epoch) + current_timestamp = int(block_data['timestamp']) + + # Get the next epoch hash (last block hash of previous epoch.) + previous_hash = block_data['next_epoch_id'] + + # Fetch the block data for start of previous epoch + previous_block_data = get_block(url, previous_hash) + previous_timestamp = int(previous_block_data['timestamp']) + + # Calculate the length of the epoch in nanoseconds + epoch_length = current_timestamp - previous_timestamp + epoch_length_seconds = ns_to_seconds(epoch_length) # Convert to seconds + epoch_lengths.append(epoch_length_seconds) + + print(f"Epoch -{i+1}: {format_time(epoch_length_seconds)}") + + # Move to the next epoch + current_hash = previous_hash + + # Apply exponential decay weights: weight = e^(-lambda * i), where i is the epoch index and lambda is the decay rate + weighted_sum = 0 + total_weight = 0 + for i in range(num_epochs): + weight = math.exp(-decay_rate * i) + weighted_sum += epoch_lengths[i] * weight + total_weight += weight + + # Calculate the weighted average using exponential decay + exponential_weighted_average_epoch_length = weighted_sum / total_weight + + print( + f"\nExponential weighted average epoch length: {format_time(exponential_weighted_average_epoch_length)}" + ) + + return epoch_lengths, exponential_weighted_average_epoch_length + + +# Function to approximate future epoch start dates +def predict_future_epochs(starting_epoch_timestamp, avg_epoch_length, + num_future_epochs): + future_epochs = [] + current_timestamp = ns_to_seconds( + starting_epoch_timestamp) # Convert from nanoseconds to seconds + + for i in range(1, num_future_epochs + 1): + # Add the average epoch length for each future epoch + future_timestamp = current_timestamp + (i * avg_epoch_length) + + # Convert to human-readable format + future_date = time.strftime('%Y-%m-%d %H:%M:%S %A', + time.gmtime(future_timestamp)) + future_epochs.append(future_date) + + print(f"Predicted start of epoch {i}: {future_date}") + + return future_epochs + + +# Main function to run the process +def main(args): + latest_block = get_block(args.url, None) + next_epoch_id = latest_block['next_epoch_id'] + current_epoch_first_block = get_block(args.url, next_epoch_id) + current_timestamp = int(current_epoch_first_block['timestamp'] + ) # Current epoch start timestamp in nanoseconds + + # Get epoch lengths and the exponential weighted average + epoch_lengths, exponential_weighted_average_epoch_length = get_exponential_weighted_epoch_lengths( + args.url, next_epoch_id, args.num_past_epochs, args.decay_rate) + + # Predict future epoch start dates + predict_future_epochs(current_timestamp, + exponential_weighted_average_epoch_length, + args.num_future_epochs) + + +# Custom action to set the URL based on chain_id +class SetURLFromChainID(argparse.Action): + + def __call__(self, parser, namespace, values, option_string=None): + if values == 'mainnet': + setattr(namespace, 'url', 'https://archival-rpc.mainnet.near.org') + elif values == 'testnet': + setattr(namespace, 'url', 'https://archival-rpc.testnet.near.org') + + +# Set up command-line argument parsing +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Approximate future epoch start dates for NEAR Protocol.") + # Create a mutually exclusive group for chain_id and url + group = parser.add_mutually_exclusive_group(required=False) + group.add_argument("--url", help="The RPC URL to query.") + group.add_argument( + "--chain_id", + choices=['mainnet', 'testnet'], + action=SetURLFromChainID, + help= + "The chain ID (either 'mainnet' or 'testnet'). Sets the corresponding URL." + ) + + parser.add_argument("--num_past_epochs", + type=int, + default=4, + help="Number of past epochs to analyze.") + parser.add_argument("--decay_rate", + type=float, + default=0.1, + help="Decay rate for exponential weighting.") + parser.add_argument("--num_future_epochs", + type=int, + default=3, + help="Number of future epochs to predict.") + + args = parser.parse_args() + main(args) From fe92b5a9310e0d05df02713585e1fc9f9dd90279 Mon Sep 17 00:00:00 2001 From: Aleksandr Logunov Date: Wed, 25 Sep 2024 17:16:28 +0400 Subject: [PATCH 19/49] chore: nayduck in ci on merge (#12132) Adding nayduck to ci on merge. Adding only one group of tests for now. Extracting small portion of tests taking >12m to complete to very_expensive.txt, fixing them should be a rare event anyway. --------- Co-authored-by: Andrei <122784628+andrei-near@users.noreply.github.com> --- .github/workflows/nayduck_ci.yml | 46 +++++++++++++++++++++++++++ .github/workflows/nightly_nayduck.yml | 21 ------------ nightly/ci.txt | 2 ++ nightly/expensive.txt | 6 ---- nightly/nightly.txt | 1 + nightly/pytest-sanity.txt | 3 -- nightly/very_expensive.txt | 11 +++++++ scripts/nayduck.py | 2 +- 8 files changed, 61 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/nayduck_ci.yml delete mode 100644 .github/workflows/nightly_nayduck.yml create mode 100644 nightly/ci.txt create mode 100644 nightly/very_expensive.txt diff --git a/.github/workflows/nayduck_ci.yml b/.github/workflows/nayduck_ci.yml new file mode 100644 index 00000000000..d4791764a67 --- /dev/null +++ b/.github/workflows/nayduck_ci.yml @@ -0,0 +1,46 @@ +name: CI Nayduck tests +on: + merge_group: + + workflow_dispatch: + +jobs: + nayduck_tests: + runs-on: "ubuntu-latest" + environment: development + timeout-minutes: 60 + + steps: + - name: Install JQ json processor + run: sudo apt install jq + + - name: Install required python modules + run: | + pip3 install -r pytest/requirements.txt + + - name: Create nayduck-code file + run: | + echo ${{ secrets.NAYDUCK_CODE }} > ~/.config/nayduck-code + + - name: Run Nayduck tests and wait for results + run: | + NEW_TEST=$(python3 scripts/nayduck.py --test-file nightly/ci.txt) + RUN_ID=$(echo $NEW_TEST | grep https | sed -E 's|.*\/run\/([0-9]+)|\1|g') + + # wait all the tests to finish + while true; do + TEST_RESULTS=$(curl -s https://nayduck.nearone.org/api/run/$RUN_ID) + TESTS_NOT_READY=$( jq -e '.tests | .[] | select(.status == "RUNNING" or .status == "PENDING" ) ' <<< ${TEST_RESULTS} ) + if [ -z "$TESTS_NOT_READY" ]; then break; fi + sleep 15 + done + + UNSUCCESSFUL_TESTS=$(jq -e '.tests | .[] | select(.status != "PASSED" and .status != "IGNORED") ' <<< ${TEST_RESULTS} ) + if [ -z "$UNSUCCESSFUL_TESTS" ]; then + echo "Nayduck CI tests passed." + echo "Results available at https://nayduck.nearone.org/#/run/$RUN_ID" + else + echo "CI Nayduck tests are failing https://nayduck.nearone.org/#/run/$RUN_ID." + echo "Fix them before merging" + exit 1 + fi diff --git a/.github/workflows/nightly_nayduck.yml b/.github/workflows/nightly_nayduck.yml deleted file mode 100644 index 0f22a8d55f8..00000000000 --- a/.github/workflows/nightly_nayduck.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Nightly Nayduck tests check -on: - merge_group: - -jobs: - nightly_nayduck_tests: - runs-on: "ubuntu-latest" - timeout-minutes: 10 - - steps: - - name: Install JQ json processor - run: sudo apt install jq - - # In this step we get the latest nightly results from the nayduck server - # and check if there are any non-passing tests - - name: Check if there are any non-passing tests - run: | - NIGHTLY_RESULTS=$(curl -s https://nayduck.nearone.org/api/nightly-events) - UNSUCCESSFUL_TESTS=$(jq -e '.tests | .[][] | select(.[2] != "PASSED" ) ' <<< ${NIGHTLY_RESULTS} ) - if [ -z "$UNSUCCESSFUL_TESTS" ] ; then echo "Nightly Nayduck tests OK"; \ - else echo "Nightly Nayduck tests are failing" && exit 1; fi diff --git a/nightly/ci.txt b/nightly/ci.txt new file mode 100644 index 00000000000..6f4d12d54e8 --- /dev/null +++ b/nightly/ci.txt @@ -0,0 +1,2 @@ +# TODO: add remaining tests. +./pytest.txt \ No newline at end of file diff --git a/nightly/expensive.txt b/nightly/expensive.txt index cfd4cefc9d3..8a2630746bc 100644 --- a/nightly/expensive.txt +++ b/nightly/expensive.txt @@ -17,12 +17,6 @@ expensive --timeout=1800 near-client near_client tests::catching_up::test_catchu expensive --timeout=1800 near-client near_client tests::catching_up::test_catchup_random_single_part_sync_height_6 --features nightly expensive --timeout=1800 near-client near_client tests::catching_up::test_catchup_sanity_blocks_produced expensive --timeout=1800 near-client near_client tests::catching_up::test_catchup_sanity_blocks_produced --features nightly -expensive --timeout=3600 near-client near_client tests::catching_up::test_all_chunks_accepted_1000 -expensive --timeout=3600 near-client near_client tests::catching_up::test_all_chunks_accepted_1000 --features nightly -expensive --timeout=7200 near-client near_client tests::catching_up::test_all_chunks_accepted_1000_slow -expensive --timeout=7200 near-client near_client tests::catching_up::test_all_chunks_accepted_1000_slow --features nightly -expensive --timeout=1800 near-client near_client tests::catching_up::test_all_chunks_accepted_1000_rare_epoch_changing -expensive --timeout=1800 near-client near_client tests::catching_up::test_all_chunks_accepted_1000_rare_epoch_changing --features nightly expensive --timeout=1800 near-client near_client tests::catching_up::test_catchup_receipts_sync_hold expensive --timeout=1800 near-client near_client tests::catching_up::test_catchup_receipts_sync_hold --features nightly diff --git a/nightly/nightly.txt b/nightly/nightly.txt index d99d8445f57..11285397dab 100644 --- a/nightly/nightly.txt +++ b/nightly/nightly.txt @@ -1,3 +1,4 @@ ./sandbox.txt ./pytest.txt ./expensive.txt +./very_expensive.txt \ No newline at end of file diff --git a/nightly/pytest-sanity.txt b/nightly/pytest-sanity.txt index 0da6c0a2a0a..383ce9bdc69 100644 --- a/nightly/pytest-sanity.txt +++ b/nightly/pytest-sanity.txt @@ -159,9 +159,6 @@ pytest --timeout=300 sanity/rpc_hash.py --features nightly pytest sanity/rosetta.py pytest sanity/rosetta.py --features nightly -# Make sure Docker image can be build and run -pytest --skip-build --timeout=1h sanity/docker.py - # This is the test for meta transactions. pytest sanity/meta_tx.py --features nightly diff --git a/nightly/very_expensive.txt b/nightly/very_expensive.txt new file mode 100644 index 00000000000..6bf4468619c --- /dev/null +++ b/nightly/very_expensive.txt @@ -0,0 +1,11 @@ +# Catchup tests +expensive --timeout=3600 near-client near_client tests::catching_up::test_all_chunks_accepted_1000 +expensive --timeout=3600 near-client near_client tests::catching_up::test_all_chunks_accepted_1000 --features nightly +expensive --timeout=7200 near-client near_client tests::catching_up::test_all_chunks_accepted_1000_slow +expensive --timeout=7200 near-client near_client tests::catching_up::test_all_chunks_accepted_1000_slow --features nightly +expensive --timeout=1800 near-client near_client tests::catching_up::test_all_chunks_accepted_1000_rare_epoch_changing +expensive --timeout=1800 near-client near_client tests::catching_up::test_all_chunks_accepted_1000_rare_epoch_changing --features nightly + +# Make sure Docker image can be build and run +pytest --skip-build --timeout=1h sanity/docker.py + diff --git a/scripts/nayduck.py b/scripts/nayduck.py index f9d1bfaf430..8a66c842f27 100755 --- a/scripts/nayduck.py +++ b/scripts/nayduck.py @@ -184,7 +184,7 @@ def impl(lines: typing.Iterable[str], line = line.rstrip() if line.startswith('./') or (include_comments and line.startswith('#./')): - if depth == 3: + if depth == 4: print(f'{filename}:{lineno}: ignoring {line}; ' f'would exceed depth limit of {depth}') else: From f1bacfaa89f066db8b765f7c7588d60682b0aa72 Mon Sep 17 00:00:00 2001 From: Saketh Are Date: Wed, 25 Sep 2024 09:56:59 -0400 Subject: [PATCH 20/49] fix(state-snapshot): correctly return included shard uids (#12143) The StateSnapshot class provides a function called `get_shard_uids`, which is intended to return the UIDs of the shards included in the snapshot. However, this function was incorrectly returning the entire set of flat storage keys from the flat storage manager. As a result, if a flat storage exists but the snapshot process fails to update the flat storage head, the snapshot would incorrectly report that the shard was successfully included. --- core/store/src/flat/manager.rs | 5 ----- core/store/src/trie/state_snapshot.rs | 18 +++++++++++------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/core/store/src/flat/manager.rs b/core/store/src/flat/manager.rs index 3dd63a805a1..558f9798d5a 100644 --- a/core/store/src/flat/manager.rs +++ b/core/store/src/flat/manager.rs @@ -204,11 +204,6 @@ impl FlatStorageManager { Some(FlatStorageChunkView::new(self.0.store.clone(), block_hash, flat_storage)) } - pub fn get_shard_uids(&self) -> Vec { - let flat_storages = self.0.flat_storages.lock().expect(POISONED_LOCK_ERR); - flat_storages.keys().cloned().collect() - } - // TODO (#7327): consider returning Result when we expect flat storage to exist pub fn get_flat_storage_for_shard(&self, shard_uid: ShardUId) -> Option { let flat_storages = self.0.flat_storages.lock().expect(POISONED_LOCK_ERR); diff --git a/core/store/src/trie/state_snapshot.rs b/core/store/src/trie/state_snapshot.rs index 9611467cc87..43e2c1f75d9 100644 --- a/core/store/src/trie/state_snapshot.rs +++ b/core/store/src/trie/state_snapshot.rs @@ -72,6 +72,8 @@ pub struct StateSnapshot { store: Store, /// Access to flat storage in that store. flat_storage_manager: FlatStorageManager, + /// Shards which were successfully included in the snapshot. + included_shard_uids: Vec, } impl StateSnapshot { @@ -80,11 +82,12 @@ impl StateSnapshot { store: Store, prev_block_hash: CryptoHash, flat_storage_manager: FlatStorageManager, - shard_uids: &[ShardUId], + requested_shard_uids: &[ShardUId], block: Option<&Block>, ) -> Self { - tracing::debug!(target: "state_snapshot", ?shard_uids, ?prev_block_hash, "new StateSnapshot"); - for shard_uid in shard_uids { + tracing::debug!(target: "state_snapshot", ?requested_shard_uids, ?prev_block_hash, "new StateSnapshot"); + let mut included_shard_uids = vec![]; + for shard_uid in requested_shard_uids { if let Err(err) = flat_storage_manager.create_flat_storage_for_shard(*shard_uid) { tracing::warn!(target: "state_snapshot", ?err, ?shard_uid, "Failed to create a flat storage for snapshot shard"); continue; @@ -104,6 +107,7 @@ impl StateSnapshot { match flat_storage.update_flat_head(desired_flat_head) { Ok(_) => { tracing::debug!(target: "state_snapshot", ?shard_uid, ?current_flat_head, ?desired_flat_head, "Successfully moved FlatStorage head of the snapshot"); + included_shard_uids.push(*shard_uid); } Err(err) => { tracing::error!(target: "state_snapshot", ?shard_uid, ?err, ?current_flat_head, ?desired_flat_head, "Failed to move FlatStorage head of the snapshot"); @@ -114,12 +118,12 @@ impl StateSnapshot { } } } - Self { prev_block_hash, store, flat_storage_manager } + Self { prev_block_hash, store, flat_storage_manager, included_shard_uids } } /// Returns the UIds for the shards included in the snapshot. - pub fn get_shard_uids(&self) -> Vec { - self.flat_storage_manager.get_shard_uids() + pub fn get_included_shard_uids(&self) -> Vec { + self.included_shard_uids.clone() } /// Returns status of a shard of a flat storage in the state snapshot. @@ -238,7 +242,7 @@ impl ShardTries { metrics::HAS_STATE_SNAPSHOT.set(1); tracing::info!(target: "state_snapshot", ?prev_block_hash, "Made a checkpoint"); - Ok(Some(state_snapshot_lock.as_ref().unwrap().get_shard_uids())) + Ok(Some(state_snapshot_lock.as_ref().unwrap().get_included_shard_uids())) } /// Deletes all snapshots and unsets the STATE_SNAPSHOT_KEY. From 1eca6f0bdddc6708df143ed1c92ff980294faddc Mon Sep 17 00:00:00 2001 From: Olga Telezhnaya Date: Wed, 25 Sep 2024 15:18:54 +0100 Subject: [PATCH 21/49] Add the issue templates (#12141) I've added a few issue templates for different use cases. There's a default assignee, please suggest someone else if you see yourself there and you don't want to be a default point of contact. You can always re-assign the issues as well. --------- Co-authored-by: Aleksandr Logunov --- .github/ISSUE_TEMPLATE/0-node-issue.yml | 93 +++++++++++++++++++ .github/ISSUE_TEMPLATE/1-rpc-issue.yml | 65 +++++++++++++ .../2-contract-runtime-issue.yml | 45 +++++++++ .github/ISSUE_TEMPLATE/3-feature-request.yml | 50 ++++++++++ .github/ISSUE_TEMPLATE/4-general-issue.yml | 45 +++++++++ .github/ISSUE_TEMPLATE/5-internal.yml | 10 ++ 6 files changed, 308 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/0-node-issue.yml create mode 100644 .github/ISSUE_TEMPLATE/1-rpc-issue.yml create mode 100644 .github/ISSUE_TEMPLATE/2-contract-runtime-issue.yml create mode 100644 .github/ISSUE_TEMPLATE/3-feature-request.yml create mode 100644 .github/ISSUE_TEMPLATE/4-general-issue.yml create mode 100644 .github/ISSUE_TEMPLATE/5-internal.yml diff --git a/.github/ISSUE_TEMPLATE/0-node-issue.yml b/.github/ISSUE_TEMPLATE/0-node-issue.yml new file mode 100644 index 00000000000..9ea38aff3dc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/0-node-issue.yml @@ -0,0 +1,93 @@ +name: Node Issue +description: Issue while running a node +title: "Node Issue: " +labels: ["Node", "community", "investigation required"] +assignees: + - VanBarbascu +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + Before you go ahead, please make sure that the issue has not been reported already. + Please, follow steps below to help us resolve your issue. + In scripts below we assume that `$NEARD` environmental variable contains path to your neard binary, + and `$NEAR_HOME` variable contains path to your near home. + - type: input + id: contact + attributes: + label: Contact Details + description: How can we get in touch with you if we need more info? + placeholder: ex. email@example.com + validations: + required: false + - type: dropdown + id: network + attributes: + label: Which network are you running? + description: Pick the network + options: + - mainnet + - testnet + - other (specify below) + - type: dropdown + id: node-type + attributes: + label: Node type + description: What type of node are you running? + options: + - Top 100 Validator + - Non-Top 100 Validator + - RPC (Default) + - Split Storage Archival + - Legacy Archival (Deprecated) + default: 2 + validations: + required: true + - type: textarea + id: version + attributes: + label: Version + description: What version of neard are you running? Please, provide output of `$NEARD --version`. + render: shell + validations: + required: true + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Describe your issue with running a near node. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. + render: shell + validations: + required: false + - type: textarea + id: block-misc + attributes: + label: Node head info + description: Please, provide full output of `RUST_LOG=warn $NEARD --home $NEAR_HOME --unsafe-fast-startup view-state scan-db-column --column BlockMisc`. + render: shell + validations: + required: false + - type: textarea + id: neard-history + attributes: + label: Node upgrade history + description: When did you upgrade to current version? Please, try to provide date and time. What version were you running before that? + render: shell + validations: + required: true + - type: textarea + id: db-history + attributes: + label: DB reset history + description: When was the last time you restarted your DB from snapshot? + render: shell + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/1-rpc-issue.yml b/.github/ISSUE_TEMPLATE/1-rpc-issue.yml new file mode 100644 index 00000000000..1bb82b41f65 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-rpc-issue.yml @@ -0,0 +1,65 @@ +name: RPC Issue +description: Issue while using public RPC service +title: "RPC Issue: " +labels: ["A-RPC", "community", "investigation required"] +assignees: + - khorolets +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + Before you go ahead, please make sure that the issue has not been reported already. + Please, follow steps below to help us resolve your issue. + - type: input + id: contact + attributes: + label: Contact Details + description: How can we get in touch with you if we need more info? + placeholder: ex. email@example.com + validations: + required: false + - type: dropdown + id: network + attributes: + label: Which network are you using? + description: Pick the network + options: + - mainnet + - testnet + - other (specify below) + - type: textarea + id: rpc-url + attributes: + label: RPC URL + description: Provide the RPC URL you are using + validations: + required: true + - type: textarea + id: rpc-request + attributes: + label: RPC Request + description: Provide the RPC request + validations: + required: true + - type: textarea + id: expected-result + attributes: + label: Expected result + description: What is the expected result of the request? + validations: + required: true + - type: textarea + id: actual-result + attributes: + label: Actual result + description: Provide the actual response + validations: + required: true + - type: checkboxes + id: is-archival + attributes: + label: Is it an archival request? + description: The requested data is older than 3 days + options: + - label: The result contains archival data diff --git a/.github/ISSUE_TEMPLATE/2-contract-runtime-issue.yml b/.github/ISSUE_TEMPLATE/2-contract-runtime-issue.yml new file mode 100644 index 00000000000..ba8f3ad309a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-contract-runtime-issue.yml @@ -0,0 +1,45 @@ +name: Contract runtime Issue +description: Issue while developing a contract +title: "Contract Runtime Issue: " +labels: ["T-contract-runtime", "community", "investigation required"] +assignees: + - akhi3030 +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + Before you go ahead, please make sure that the issue has not been reported already. + Please, follow steps below to help us resolve your issue. + - type: input + id: contact + attributes: + label: Contact Details + description: How can we get in touch with you if we need more info? + placeholder: ex. email@example.com + validations: + required: false + - type: dropdown + id: network + attributes: + label: Which network are you using? + description: Pick the network + options: + - mainnet + - testnet + - other (specify below) + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Describe your issue with the contract + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. + render: shell + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/3-feature-request.yml b/.github/ISSUE_TEMPLATE/3-feature-request.yml new file mode 100644 index 00000000000..a752473141c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3-feature-request.yml @@ -0,0 +1,50 @@ +name: Feature Request +description: Any ideas how to improve nearcore +title: "Feature: " +labels: ["community"] +assignees: + - walnut-the-cat +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this feature request! + Before you go ahead, please make sure that the feature request has not been reported already. + Please, follow steps below to explain the feature request better. + - type: input + id: contact + attributes: + label: Contact Details + description: How can we get in touch with you if we need more info? + placeholder: ex. email@example.com + validations: + required: false + - type: textarea + id: feature-request + attributes: + label: Feature Request + description: Describe your feature request + validations: + required: true + - type: textarea + id: beneficiary + attributes: + label: Beneficiary + description: Who is the main beneficiary of this feature? + validations: + required: true + - type: textarea + id: priority + attributes: + label: Priority + description: How important this feature is? Why should developers prioritize this? + render: shell + validations: + required: true + - type: checkboxes + id: ready-to-contribute + attributes: + label: Are you ready to contribute to nearcore? + description: Are you interested in implementing this feature? + options: + - label: Yes diff --git a/.github/ISSUE_TEMPLATE/4-general-issue.yml b/.github/ISSUE_TEMPLATE/4-general-issue.yml new file mode 100644 index 00000000000..d43bb85c851 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/4-general-issue.yml @@ -0,0 +1,45 @@ +name: Bug Report +description: General bug report [use this only if it does not fit into any of the categories above] +title: "Bug: " +labels: ["community", "investigation required"] +assignees: + - telezhnaya +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + Before you go ahead, please make sure that the issue has not been reported already. + Please, follow steps below to help us resolve your issue. + - type: input + id: contact + attributes: + label: Contact Details + description: How can we get in touch with you if we need more info? + placeholder: ex. email@example.com + validations: + required: false + - type: dropdown + id: network + attributes: + label: Which network are you using? + description: Pick the network + options: + - mainnet + - testnet + - other (specify below) + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Describe your issue with running a near node. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. + render: shell + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/5-internal.yml b/.github/ISSUE_TEMPLATE/5-internal.yml new file mode 100644 index 00000000000..05065c6c926 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/5-internal.yml @@ -0,0 +1,10 @@ +name: Internal +description: Template for nearcore employees only +title: "Issue: " +body: + - type: textarea + id: content + attributes: + label: Description + validations: + required: true From 85e2e4abdbea0df3ba090b75fd73798788bddf78 Mon Sep 17 00:00:00 2001 From: Saketh Are Date: Wed, 25 Sep 2024 12:37:59 -0400 Subject: [PATCH 22/49] feat(sync): use routed state part request in sync actor (#12111) This PR goes after #12110: - Now that peer selection happens inside the network crate, we can simplify a great deal of the sync actor. It is no longer concerned about from which peers the state parts are requested. - We change the behavior of nodes which have external storage configured. For each state part, they will first attempt to obtain it from other nodes in the network. After a fixed number of attempts, they will fall back to downloading from the external storage. These changes bring us towards the ultimate goal of deprecating cloud storage of state parts entirely. --- chain/client/src/sync/state.rs | 507 ++++++------------ .../src/test_utils/peer_manager_mock.rs | 10 +- .../src/peer_manager/peer_manager_actor.rs | 49 +- chain/network/src/snapshot_hosts/mod.rs | 10 +- chain/network/src/test_loop.rs | 7 +- chain/network/src/test_utils.rs | 6 +- chain/network/src/types.rs | 7 + core/chain-configs/src/client_config.rs | 13 + integration-tests/src/test_loop/builder.rs | 5 + .../src/tests/client/sync_state_nodes.rs | 1 + 10 files changed, 262 insertions(+), 353 deletions(-) diff --git a/chain/client/src/sync/state.rs b/chain/client/src/sync/state.rs index 16cece2a726..681f32a01e1 100644 --- a/chain/client/src/sync/state.rs +++ b/chain/client/src/sync/state.rs @@ -1,4 +1,4 @@ -//! State sync is trying to fetch the 'full state' from the peers (which can be multiple GB). +//! State sync is trying to fetch the 'full state' (which can be multiple GB). //! It happens after HeaderSync and before BlockSync (but only if the node sees that it is 'too much behind'). //! See for more detailed information. //! Such state can be downloaded only at special heights (currently - at the beginning of the current and previous @@ -9,16 +9,16 @@ //! many parts it consists of, hash of the root etc). //! Then it tries downloading the rest of the data in 'parts' (usually the part is around 1MB in size). //! -//! For downloading - the code is picking the potential target nodes (all direct peers that are tracking the shard -//! (and are high enough) + validators from that epoch that were tracking the shard) -//! Then for each part that we're missing, we're 'randomly' picking a target from whom we'll request it - but we make -//! sure to not request more than MAX_STATE_PART_REQUESTS from each. -//! -//! WARNING: with the current design, we're putting quite a load on the validators - as we request a lot of data from -//! them (if you assume that we have 100 validators and 30 peers - we send 100/130 of requests to validators). -//! Currently validators defend against it, by having a rate limiters - but we should improve the algorithm -//! here to depend more on local peers instead. +//! Optionally, it is possible to configure a cloud storage location where state headers and parts +//! are available. The behavior is currently as follows: +//! - State Headers: If external storage is configured, we will get the headers there. +//! Otherwise, we will send requests to random peers for the state headers. +//! - State Parts: In the network crate we track which nodes in the network can serve parts for +//! which shards. If no external storage is configured, parts are obtained from peers +//! accordingly. If external storage is configure, we attempt first to obtain parts from the +//! peers in the network before falling back to the external storage. //! +//! This is an intermediate approach in the process of eliminating external storage entirely. use crate::metrics; use crate::sync::external::{ @@ -27,7 +27,7 @@ use crate::sync::external::{ use borsh::BorshDeserialize; use futures::{future, FutureExt}; use near_async::futures::{FutureSpawner, FutureSpawnerExt}; -use near_async::messaging::SendAsync; +use near_async::messaging::{CanSend, SendAsync}; use near_async::time::{Clock, Duration, Utc}; use near_chain::chain::{ApplyStatePartsRequest, LoadMemtrieRequest}; use near_chain::near_chain_primitives; @@ -38,9 +38,9 @@ use near_client_primitives::types::{ format_shard_sync_phase, DownloadStatus, ShardSyncDownload, ShardSyncStatus, }; use near_epoch_manager::EpochManagerAdapter; -use near_network::types::PeerManagerMessageRequest; use near_network::types::{ HighestHeightPeerInfo, NetworkRequests, NetworkResponses, PeerManagerAdapter, + PeerManagerMessageRequest, StateSyncEvent, }; use near_primitives::hash::CryptoHash; use near_primitives::network::PeerId; @@ -52,10 +52,8 @@ use near_primitives::state_sync::{ use near_primitives::types::{AccountId, EpochHeight, EpochId, ShardId, StateRoot}; use near_store::DBCol; use rand::seq::SliceRandom; -use rand::{thread_rng, Rng}; +use rand::thread_rng; use std::collections::HashMap; -use std::num::NonZeroUsize; -use std::ops::Add; use std::sync::atomic::Ordering; use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::Arc; @@ -80,22 +78,6 @@ pub enum StateSyncResult { Completed, } -struct PendingRequestStatus { - clock: Clock, - /// Number of parts that are in progress (we requested them from a given peer but didn't get the answer yet). - missing_parts: usize, - wait_until: Utc, -} - -impl PendingRequestStatus { - fn new(clock: Clock, timeout: Duration) -> Self { - Self { clock: clock.clone(), missing_parts: 1, wait_until: clock.now_utc().add(timeout) } - } - fn expired(&self) -> bool { - self.clock.now_utc() > self.wait_until - } -} - pub enum StateSyncFileDownloadResult { StateHeader { header_length: u64, header: ShardStateSyncResponseHeader }, StatePart { part_length: u64 }, @@ -110,32 +92,24 @@ pub struct StateSyncGetFileResult { result: Result, } -/// How to retrieve the state data. -enum StateSyncInner { - /// Request both the state header and state parts from the peers. - Peers { - /// Which parts were requested from which peer and when. - last_part_id_requested: HashMap<(PeerId, ShardId), PendingRequestStatus>, - /// Map from which part we requested to whom. - requested_target: lru::LruCache<(u64, CryptoHash), PeerId>, - }, - /// Requests the state header from peers but gets the state parts from an - /// external storage. - External { - /// Chain ID. - chain_id: String, - /// This semaphore imposes a restriction on the maximum number of simultaneous downloads - semaphore: Arc, - /// Connection to the external storage. - external: ExternalConnection, - }, +struct StateSyncExternal { + /// Chain ID. + chain_id: String, + /// This semaphore imposes a restriction on the maximum number of simultaneous downloads + semaphore: Arc, + /// A node with external storage configured first tries to obtain state parts from peers. + /// For each part, it will make this many attempts before getting it from external storage. + peer_attempts_threshold: u64, + /// Connection to the external storage. + external: ExternalConnection, } /// Helper to track state sync. pub struct StateSync { clock: Clock, - /// How to retrieve the state data. - inner: StateSyncInner, + + /// External storage, if configured. + external: Option, /// Is used for communication with the peers. network_adapter: PeerManagerAdapter, @@ -168,17 +142,13 @@ impl StateSync { sync_config: &SyncConfig, catchup: bool, ) -> Self { - let inner = match sync_config { - SyncConfig::Peers => StateSyncInner::Peers { - last_part_id_requested: Default::default(), - requested_target: lru::LruCache::new( - NonZeroUsize::new(MAX_PENDING_PART as usize).unwrap(), - ), - }, + let external = match sync_config { + SyncConfig::Peers => None, SyncConfig::ExternalStorage(ExternalStorageConfig { location, num_concurrent_requests, num_concurrent_requests_during_catchup, + external_storage_fallback_threshold, }) => { let external = match location { ExternalStorageLocation::S3 { bucket, region, .. } => { @@ -206,17 +176,18 @@ impl StateSync { } else { *num_concurrent_requests } as usize; - StateSyncInner::External { + Some(StateSyncExternal { chain_id: chain_id.to_string(), semaphore: Arc::new(tokio::sync::Semaphore::new(num_permits)), + peer_attempts_threshold: *external_storage_fallback_threshold, external, - } + }) } }; let (tx, rx) = channel::(); StateSync { clock, - inner, + external, network_adapter, timeout, state_parts_apply_results: HashMap::new(), @@ -483,59 +454,6 @@ impl StateSync { } } - // Function called when our node receives the network response with a part. - pub fn received_requested_part( - &mut self, - part_id: u64, - shard_id: ShardId, - sync_hash: CryptoHash, - ) { - match &mut self.inner { - StateSyncInner::Peers { last_part_id_requested, requested_target } => { - let key = (part_id, sync_hash); - // Check that it came from the target that we requested it from. - if let Some(target) = requested_target.get(&key) { - if last_part_id_requested.get_mut(&(target.clone(), shard_id)).map_or( - false, - |request| { - request.missing_parts = request.missing_parts.saturating_sub(1); - request.missing_parts == 0 - }, - ) { - last_part_id_requested.remove(&(target.clone(), shard_id)); - } - } - } - StateSyncInner::External { .. } => { - // Do nothing. - } - } - } - - /// Avoids peers that already have outstanding requests for parts. - fn select_peers( - &mut self, - highest_height_peers: &[HighestHeightPeerInfo], - shard_id: ShardId, - ) -> Result, near_chain::Error> { - let peers: Vec = - highest_height_peers.iter().map(|peer| peer.peer_info.id.clone()).collect(); - let res = match &mut self.inner { - StateSyncInner::Peers { last_part_id_requested, .. } => { - last_part_id_requested.retain(|_, request| !request.expired()); - peers - .into_iter() - .filter(|peer| { - // If we still have a pending request from this node - don't add another one. - !last_part_id_requested.contains_key(&(peer.clone(), shard_id)) - }) - .collect::>() - } - StateSyncInner::External { .. } => peers, - }; - Ok(res) - } - /// Returns new ShardSyncDownload if successful, otherwise returns given shard_sync_download fn request_shard( &mut self, @@ -547,23 +465,21 @@ impl StateSync { runtime_adapter: Arc, state_parts_future_spawner: &dyn FutureSpawner, ) -> Result<(), near_chain::Error> { - let mut possible_targets = vec![]; - match self.inner { - StateSyncInner::Peers { .. } => { - possible_targets = self.select_peers(highest_height_peers, shard_id)?; - if possible_targets.is_empty() { - tracing::debug!(target: "sync", "Can't request a state header: No possible targets"); - // In most cases it means that all the targets are currently busy (that we have a pending request with them). - return Ok(()); - } - } - // We do not need to select a target for external storage. - StateSyncInner::External { .. } => {} - } - // Downloading strategy starts here match shard_sync_download.status { ShardSyncStatus::StateDownloadHeader => { + // If no external storage is configured, we have to request headers from our peers + let possible_targets = match self.external { + Some(_) => vec![], + None => { + if highest_height_peers.is_empty() { + tracing::debug!(target: "sync", "Can't request a state header: No possible targets"); + return Ok(()); + } + highest_height_peers.iter().map(|peer| peer.peer_info.id.clone()).collect() + } + }; + self.request_shard_header( chain, shard_id, @@ -577,7 +493,6 @@ impl StateSync { self.request_shard_parts( shard_id, sync_hash, - possible_targets, shard_sync_download, chain, runtime_adapter, @@ -601,49 +516,51 @@ impl StateSync { state_parts_future_spawner: &dyn FutureSpawner, ) { let header_download = new_shard_sync_download.get_header_download_mut().unwrap(); - match &mut self.inner { - StateSyncInner::Peers { .. } => { - let peer_id = possible_targets.choose(&mut thread_rng()).cloned().unwrap(); - tracing::debug!(target: "sync", ?peer_id, shard_id, ?sync_hash, ?possible_targets, "request_shard_header"); - assert!(header_download.run_me.load(Ordering::SeqCst)); - header_download.run_me.store(false, Ordering::SeqCst); - header_download.state_requests_count += 1; - header_download.last_target = Some(peer_id.clone()); - let run_me = header_download.run_me.clone(); - near_performance_metrics::actix::spawn( - std::any::type_name::(), - self.network_adapter - .send_async(PeerManagerMessageRequest::NetworkRequests( - NetworkRequests::StateRequestHeader { shard_id, sync_hash, peer_id }, - )) - .then(move |result| { - if let Ok(NetworkResponses::RouteNotFound) = - result.map(|f| f.as_network_response()) - { - // Send a StateRequestHeader on the next iteration - run_me.store(true, Ordering::SeqCst); - } - future::ready(()) - }), - ); - } - StateSyncInner::External { chain_id, external, .. } => { - let sync_block_header = chain.get_block_header(&sync_hash).unwrap(); - let epoch_id = sync_block_header.epoch_id(); - let epoch_info = chain.epoch_manager.get_epoch_info(epoch_id).unwrap(); - let epoch_height = epoch_info.epoch_height(); - request_header_from_external_storage( - header_download, - shard_id, - sync_hash, - epoch_id, - epoch_height, - &chain_id.clone(), - external.clone(), - state_parts_future_spawner, - self.state_parts_mpsc_tx.clone(), - ); - } + if let Some(StateSyncExternal { chain_id, external, .. }) = &self.external { + // TODO(saketh): Eventually we aim to deprecate the external storage and rely only on + // peers in the network for getting state headers. + let sync_block_header = chain.get_block_header(&sync_hash).unwrap(); + let epoch_id = sync_block_header.epoch_id(); + let epoch_info = chain.epoch_manager.get_epoch_info(epoch_id).unwrap(); + let epoch_height = epoch_info.epoch_height(); + request_header_from_external_storage( + header_download, + shard_id, + sync_hash, + epoch_id, + epoch_height, + &chain_id.clone(), + external.clone(), + state_parts_future_spawner, + self.state_parts_mpsc_tx.clone(), + ); + } else { + // TODO(saketh): We need to rework the way we get headers from peers entirely. + // Currently it is assumed that one of the direct peers of the node is able to generate + // the shard header. + let peer_id = possible_targets.choose(&mut thread_rng()).cloned().unwrap(); + tracing::debug!(target: "sync", ?peer_id, shard_id, ?sync_hash, ?possible_targets, "request_shard_header"); + assert!(header_download.run_me.load(Ordering::SeqCst)); + header_download.run_me.store(false, Ordering::SeqCst); + header_download.state_requests_count += 1; + header_download.last_target = Some(peer_id.clone()); + let run_me = header_download.run_me.clone(); + near_performance_metrics::actix::spawn( + std::any::type_name::(), + self.network_adapter + .send_async(PeerManagerMessageRequest::NetworkRequests( + NetworkRequests::StateRequestHeader { shard_id, sync_hash, peer_id }, + )) + .then(move |result| { + if let Ok(NetworkResponses::RouteNotFound) = + result.map(|f| f.as_network_response()) + { + // Send a StateRequestHeader on the next iteration + run_me.store(true, Ordering::SeqCst); + } + future::ready(()) + }), + ); } } @@ -652,7 +569,6 @@ impl StateSync { &mut self, shard_id: ShardId, sync_hash: CryptoHash, - possible_targets: Vec, new_shard_sync_download: &mut ShardSyncDownload, chain: &Chain, runtime_adapter: Arc, @@ -660,88 +576,84 @@ impl StateSync { ) { // Iterate over all parts that needs to be requested (i.e. download.run_me is true). // Parts are ordered such that its index match its part_id. - match &mut self.inner { - StateSyncInner::Peers { last_part_id_requested, requested_target } => { - // We'll select all the 'highest' peers + validators as candidates (excluding those that gave us timeout in the past). - // And for each one of them, we'll ask for up to 16 (MAX_STATE_PART_REQUEST) parts. - let possible_targets_sampler = - SamplerLimited::new(possible_targets, MAX_STATE_PART_REQUEST); - - // For every part that needs to be requested it is selected one - // peer (target) randomly to request the part from. - // IMPORTANT: here we use 'zip' with possible_target_sampler - - // which is limited. So at any moment we'll not request more - // than possible_targets.len() * MAX_STATE_PART_REQUEST parts. - for ((part_id, download), target) in - parts_to_fetch(new_shard_sync_download).zip(possible_targets_sampler) - { - // The request sent to the network adapater needs to include the sync_prev_prev_hash - // so that a peer hosting the correct snapshot can be selected. - let prev_header = chain - .get_block_header(&sync_hash) - .map(|header| chain.get_block_header(&header.prev_hash())); - - match prev_header { - Ok(Ok(prev_header)) => { - let sync_prev_prev_hash = prev_header.prev_hash(); - sent_request_part( - self.clock.clone(), - target.clone(), - part_id, - shard_id, - sync_hash, - last_part_id_requested, - requested_target, - self.timeout, - ); - request_part_from_peers( - part_id, - target, - download, - shard_id, - sync_hash, - *sync_prev_prev_hash, - &self.network_adapter, - ); - } - Ok(Err(err)) => { - tracing::error!(target: "sync", %shard_id, %sync_hash, ?err, "could not get prev header"); - } - Err(err) => { - tracing::error!(target: "sync", %shard_id, %sync_hash, ?err, "could not get header"); - } - } + let mut peer_requests_sent = 0; + let mut state_root_and_part_count: Option<(CryptoHash, u64)> = None; + for (part_id, download) in parts_to_fetch(new_shard_sync_download) { + if self + .external + .as_ref() + .is_some_and(|ext| download.state_requests_count >= ext.peer_attempts_threshold) + { + // TODO(saketh): After we have sufficient confidence that requesting state parts + // from peers is working well, we will eliminate the external storage entirely. + let StateSyncExternal { chain_id, semaphore, external, .. } = + self.external.as_ref().unwrap(); + if semaphore.available_permits() == 0 { + continue; } - } - StateSyncInner::External { chain_id, semaphore, external } => { + let sync_block_header = chain.get_block_header(&sync_hash).unwrap(); let epoch_id = sync_block_header.epoch_id(); let epoch_info = chain.epoch_manager.get_epoch_info(epoch_id).unwrap(); let epoch_height = epoch_info.epoch_height(); - let shard_state_header = chain.get_state_header(shard_id, sync_hash).unwrap(); - let state_root = shard_state_header.chunk_prev_state_root(); - let state_num_parts = shard_state_header.num_state_parts(); + let (state_root, state_num_parts) = + state_root_and_part_count.get_or_insert_with(|| { + let shard_state_header = + chain.get_state_header(shard_id, sync_hash).unwrap(); + ( + shard_state_header.chunk_prev_state_root(), + shard_state_header.num_state_parts(), + ) + }); - for (part_id, download) in parts_to_fetch(new_shard_sync_download) { - request_part_from_external_storage( - part_id, - download, - shard_id, - sync_hash, - epoch_id, - epoch_height, - state_num_parts, - &chain_id.clone(), - state_root, - semaphore.clone(), - external.clone(), - runtime_adapter.clone(), - state_parts_future_spawner, - self.state_parts_mpsc_tx.clone(), - ); - if semaphore.available_permits() == 0 { - break; + request_part_from_external_storage( + part_id, + download, + shard_id, + sync_hash, + epoch_id, + epoch_height, + *state_num_parts, + &chain_id.clone(), + *state_root, + semaphore.clone(), + external.clone(), + runtime_adapter.clone(), + state_parts_future_spawner, + self.state_parts_mpsc_tx.clone(), + ); + } else { + if peer_requests_sent >= MAX_STATE_PART_REQUEST { + continue; + } + + // The request sent to the network adapater needs to include the sync_prev_prev_hash + // so that a peer hosting the correct snapshot can be selected. + let prev_header = chain + .get_block_header(&sync_hash) + .map(|header| chain.get_block_header(&header.prev_hash())); + + match prev_header { + Ok(Ok(prev_header)) => { + let sync_prev_prev_hash = prev_header.prev_hash(); + request_part_from_peers( + part_id, + download, + shard_id, + sync_hash, + *sync_prev_prev_hash, + &self.network_adapter, + state_parts_future_spawner, + ); + + peer_requests_sent += 1; + } + Ok(Err(err)) => { + tracing::error!(target: "sync", %shard_id, %sync_hash, ?err, "could not get prev header"); + } + Err(err) => { + tracing::error!(target: "sync", %shard_id, %sync_hash, ?err, "could not get header"); } } } @@ -814,10 +726,6 @@ impl StateSync { state_response: ShardStateSyncResponse, chain: &mut Chain, ) { - if let Some(part_id) = state_response.part_id() { - // Mark that we have received this part (this will update info on pending parts from peers etc). - self.received_requested_part(part_id, shard_id, hash); - } match shard_sync_download.status { ShardSyncStatus::StateDownloadHeader => { let header_download = shard_sync_download.get_header_download_mut().unwrap(); @@ -858,6 +766,9 @@ impl StateSync { &data, ) { Ok(()) => { + tracing::debug!(target: "sync", %shard_id, %hash, part_id, "Received correct start part"); + self.network_adapter + .send(StateSyncEvent::StatePartReceived(shard_id, part_id)); shard_sync_download.downloads[part_id as usize].done = true; } Err(err) => { @@ -1318,19 +1229,18 @@ fn request_part_from_external_storage( /// Asynchronously requests a state part from a suitable peer. fn request_part_from_peers( part_id: u64, - peer_id: PeerId, download: &mut DownloadStatus, shard_id: ShardId, sync_hash: CryptoHash, sync_prev_prev_hash: CryptoHash, network_adapter: &PeerManagerAdapter, + state_parts_future_spawner: &dyn FutureSpawner, ) { download.run_me.store(false, Ordering::SeqCst); download.state_requests_count += 1; - download.last_target = Some(peer_id); let run_me = download.run_me.clone(); - near_performance_metrics::actix::spawn( + state_parts_future_spawner.spawn( "StateSync", network_adapter .send_async(PeerManagerMessageRequest::NetworkRequests( @@ -1342,9 +1252,6 @@ fn request_part_from_peers( }, )) .then(move |result| { - // TODO: possible optimization - in the current code, even if one of the targets it not present in the network graph - // (so we keep getting RouteNotFound) - we'll still keep trying to assign parts to it. - // Fortunately only once every 60 seconds (timeout value). if let Ok(NetworkResponses::RouteNotFound) = result.map(|f| f.as_network_response()) { // Send a StateRequestPart on the next iteration @@ -1355,26 +1262,6 @@ fn request_part_from_peers( ); } -fn sent_request_part( - clock: Clock, - peer_id: PeerId, - part_id: u64, - shard_id: ShardId, - sync_hash: CryptoHash, - last_part_id_requested: &mut HashMap<(PeerId, ShardId), PendingRequestStatus>, - requested_target: &mut lru::LruCache<(u64, CryptoHash), PeerId>, - timeout: Duration, -) { - // FIXME: something is wrong - the index should have a shard_id too. - requested_target.put((part_id, sync_hash), peer_id.clone()); - last_part_id_requested - .entry((peer_id, shard_id)) - .and_modify(|pending_request| { - pending_request.missing_parts += 1; - }) - .or_insert_with(|| PendingRequestStatus::new(clock, timeout)); -} - /// Works around how data requests to external storage are done. /// This function investigates if the response is valid and updates `done` and `error` appropriately. /// If the response is successful, then the downloaded state file was written to the DB. @@ -1407,68 +1294,6 @@ fn process_download_response( } } -/// Create an abstract collection of elements to be shuffled. -/// Each element will appear in the shuffled output exactly `limit` times. -/// Use it as an iterator to access the shuffled collection. -/// -/// ```rust,ignore -/// let sampler = SamplerLimited::new(vec![1, 2, 3], 2); -/// -/// let res = sampler.collect::>(); -/// -/// assert!(res.len() == 6); -/// assert!(res.iter().filter(|v| v == 1).count() == 2); -/// assert!(res.iter().filter(|v| v == 2).count() == 2); -/// assert!(res.iter().filter(|v| v == 3).count() == 2); -/// ``` -/// -/// Out of the 90 possible values of `res` in the code above on of them is: -/// -/// ``` -/// vec![1, 2, 1, 3, 3, 2]; -/// ``` -struct SamplerLimited { - data: Vec, - limit: Vec, -} - -impl SamplerLimited { - fn new(data: Vec, limit: u64) -> Self { - if limit == 0 { - Self { data: vec![], limit: vec![] } - } else { - let len = data.len(); - Self { data, limit: vec![limit; len] } - } - } -} - -impl Iterator for SamplerLimited { - type Item = T; - - fn next(&mut self) -> Option { - if self.limit.is_empty() { - None - } else { - let len = self.limit.len(); - let ix = thread_rng().gen_range(0..len); - self.limit[ix] -= 1; - - if self.limit[ix] == 0 { - if ix + 1 != len { - self.limit[ix] = self.limit[len - 1]; - self.data.swap(ix, len - 1); - } - - self.limit.pop(); - self.data.pop() - } else { - Some(self.data[ix].clone()) - } - } - } -} - #[cfg(test)] mod test { use super::*; diff --git a/chain/client/src/test_utils/peer_manager_mock.rs b/chain/client/src/test_utils/peer_manager_mock.rs index 0f6c9031890..cfa39d71a84 100644 --- a/chain/client/src/test_utils/peer_manager_mock.rs +++ b/chain/client/src/test_utils/peer_manager_mock.rs @@ -1,5 +1,6 @@ -use near_network::types::SetChainInfo; -use near_network::types::{PeerManagerMessageRequest, PeerManagerMessageResponse}; +use near_network::types::{ + PeerManagerMessageRequest, PeerManagerMessageResponse, SetChainInfo, StateSyncEvent, +}; pub struct PeerManagerMock { handle: Box< @@ -37,3 +38,8 @@ impl actix::Handler for PeerManagerMock { type Result = (); fn handle(&mut self, _msg: SetChainInfo, _ctx: &mut Self::Context) {} } + +impl actix::Handler for PeerManagerMock { + type Result = (); + fn handle(&mut self, _msg: StateSyncEvent, _ctx: &mut Self::Context) {} +} diff --git a/chain/network/src/peer_manager/peer_manager_actor.rs b/chain/network/src/peer_manager/peer_manager_actor.rs index 30deb4aa529..9bfbf775f1c 100644 --- a/chain/network/src/peer_manager/peer_manager_actor.rs +++ b/chain/network/src/peer_manager/peer_manager_actor.rs @@ -19,7 +19,7 @@ use crate::tcp; use crate::types::{ ConnectedPeerInfo, HighestHeightPeerInfo, KnownProducer, NetworkInfo, NetworkRequests, NetworkResponses, PeerInfo, PeerManagerMessageRequest, PeerManagerMessageResponse, PeerType, - SetChainInfo, SnapshotHostInfo, StatePartRequestBody, Tier3RequestBody, + SetChainInfo, SnapshotHostInfo, StatePartRequestBody, StateSyncEvent, Tier3RequestBody, }; use ::time::ext::InstantExt as _; use actix::fut::future::wrap_future; @@ -389,7 +389,7 @@ impl PeerManagerActor { }.await; if let Err(ref err) = result { - tracing::info!(target: "network", err = format!("{:#}", err), "failed to connect to {}", request.peer_info); + tracing::info!(target: "network", err = format!("{:#}", err), "tier3 failed to connect to {}", request.peer_info); } } @@ -693,7 +693,7 @@ impl PeerManagerActor { }.await; if let Err(ref err) = result { - tracing::info!(target: "network", err = format!("{:#}", err), "failed to connect to {peer_info}"); + tracing::info!(target: "network", err = format!("{:#}", err), "tier2 failed to connect to {peer_info}"); } if state.peer_store.peer_connection_attempt(&clock, &peer_info.id, result).is_err() { tracing::error!(target: "network", ?peer_info, "Failed to store connection attempt."); @@ -892,6 +892,7 @@ impl PeerManagerActor { shard_id, part_id, ) { + tracing::debug!(target: "network", "requesting {sync_prev_prev_hash} {shard_id} {part_id} from {peer_id}"); success = self.state.send_message_to_peer( &self.clock, @@ -917,7 +918,7 @@ impl PeerManagerActor { NetworkResponses::RouteNotFound } } - NetworkRequests::SnapshotHostInfo { sync_hash, epoch_height, mut shards } => { + NetworkRequests::SnapshotHostInfo { sync_hash, mut epoch_height, mut shards } => { if shards.len() > MAX_SHARDS_PER_SNAPSHOT_HOST_INFO { tracing::warn!("PeerManager: Sending out a SnapshotHostInfo message with {} shards, \ this is more than the allowed limit. The list of shards will be truncated. \ @@ -935,18 +936,33 @@ impl PeerManagerActor { // Sort the shards to keep things tidy shards.sort(); + let peer_id = self.state.config.node_id(); + + // Hacky workaround for test environments only. + // When starting a chain from scratch the first two snapshots both have epoch height 1. + // The epoch height is used as a version number for SnapshotHostInfo and if duplicated, + // prevents the second snapshot from being advertised as new information to the network. + // To avoid this problem, we re-index the very first epoch with epoch_height=0. + if epoch_height == 1 && self.state.snapshot_hosts.get_host_info(&peer_id).is_none() + { + epoch_height = 0; + } + // Sign the information about the locally created snapshot using the keys in the // network config before broadcasting it - let snapshot_host_info = SnapshotHostInfo::new( + let snapshot_host_info = Arc::new(SnapshotHostInfo::new( self.state.config.node_id(), sync_hash, epoch_height, shards, &self.state.config.node_key, - ); + )); + + // Insert our info to our own cache. + self.state.snapshot_hosts.insert_skip_verify(snapshot_host_info.clone()); self.state.tier2.broadcast_message(Arc::new(PeerMessage::SyncSnapshotHosts( - SyncSnapshotHosts { hosts: vec![snapshot_host_info.into()] }, + SyncSnapshotHosts { hosts: vec![snapshot_host_info] }, ))); NetworkResponses::NoResponse } @@ -1260,6 +1276,25 @@ impl actix::Handler> for PeerManagerA } } +impl actix::Handler> for PeerManagerActor { + type Result = (); + #[perf] + fn handle( + &mut self, + msg: WithSpanContext, + _ctx: &mut Self::Context, + ) -> Self::Result { + let (_span, msg) = handler_debug_span!(target: "network", msg); + let _timer = + metrics::PEER_MANAGER_MESSAGES_TIME.with_label_values(&[(&msg).into()]).start_timer(); + match msg { + StateSyncEvent::StatePartReceived(shard_id, part_id) => { + self.state.snapshot_hosts.part_received(shard_id, part_id); + } + } + } +} + impl actix::Handler for PeerManagerActor { type Result = DebugStatus; #[perf] diff --git a/chain/network/src/snapshot_hosts/mod.rs b/chain/network/src/snapshot_hosts/mod.rs index 2a636b4e9ff..31b98e1c9e8 100644 --- a/chain/network/src/snapshot_hosts/mod.rs +++ b/chain/network/src/snapshot_hosts/mod.rs @@ -308,10 +308,19 @@ impl SnapshotHostsCache { (newly_inserted_data, err) } + /// Skips signature verification. Used only for the local node's own information. + pub fn insert_skip_verify(self: &Self, my_info: Arc) { + let _ = self.0.lock().try_insert(my_info); + } + pub fn get_hosts(&self) -> Vec> { self.0.lock().hosts.iter().map(|(_, v)| v.clone()).collect() } + pub(crate) fn get_host_info(&self, peer_id: &PeerId) -> Option> { + self.0.lock().hosts.peek(peer_id).cloned() + } + /// Given a state part request, selects a peer host to which the request should be sent. pub fn select_host_for_part( &self, @@ -323,7 +332,6 @@ impl SnapshotHostsCache { } /// Triggered by state sync actor after processing a state part. - #[allow(dead_code)] pub fn part_received(&self, shard_id: ShardId, part_id: u64) { let mut inner = self.0.lock(); inner.peer_selector.remove(&(shard_id, part_id)); diff --git a/chain/network/src/test_loop.rs b/chain/network/src/test_loop.rs index 0063281ebb1..7bf83f429cf 100644 --- a/chain/network/src/test_loop.rs +++ b/chain/network/src/test_loop.rs @@ -13,7 +13,7 @@ use crate::state_witness::{ }; use crate::types::{ NetworkRequests, NetworkResponses, PeerManagerMessageRequest, PeerManagerMessageResponse, - SetChainInfo, + SetChainInfo, StateSyncEvent, }; use near_async::actix::ActixResult; use near_async::futures::{FutureSpawner, FutureSpawnerExt}; @@ -188,6 +188,10 @@ impl Handler for TestLoopPeerManagerActor { fn handle(&mut self, _msg: SetChainInfo) {} } +impl Handler for TestLoopPeerManagerActor { + fn handle(&mut self, _msg: StateSyncEvent) {} +} + impl Handler for TestLoopPeerManagerActor { fn handle(&mut self, msg: PeerManagerMessageRequest) -> PeerManagerMessageResponse { let PeerManagerMessageRequest::NetworkRequests(request) = msg else { @@ -276,6 +280,7 @@ fn network_message_to_client_handler( .send(EpochSyncResponseMessage { from_peer: my_peer_id.clone(), proof }); None } + NetworkRequests::StateRequestPart { .. } => None, _ => Some(request), }) diff --git a/chain/network/src/test_utils.rs b/chain/network/src/test_utils.rs index 4806752517f..2bbb64d433c 100644 --- a/chain/network/src/test_utils.rs +++ b/chain/network/src/test_utils.rs @@ -1,7 +1,7 @@ use crate::network_protocol::PeerInfo; use crate::types::{ NetworkInfo, NetworkResponses, PeerManagerMessageRequest, PeerManagerMessageResponse, - SetChainInfo, + SetChainInfo, StateSyncEvent, }; use crate::PeerManagerActor; use actix::{Actor, ActorContext, Context, Handler}; @@ -259,6 +259,10 @@ impl CanSend for MockPeerManagerAdapter { fn send(&self, _msg: SetChainInfo) {} } +impl CanSend for MockPeerManagerAdapter { + fn send(&self, _msg: StateSyncEvent) {} +} + impl MockPeerManagerAdapter { pub fn pop(&self) -> Option { self.requests.write().unwrap().pop_front() diff --git a/chain/network/src/types.rs b/chain/network/src/types.rs index 4b005e24c2b..2753883ec1d 100644 --- a/chain/network/src/types.rs +++ b/chain/network/src/types.rs @@ -293,6 +293,12 @@ pub enum NetworkRequests { EpochSyncResponse { route_back: CryptoHash, proof: CompressedEpochSyncProof }, } +#[derive(Debug, actix::Message, strum::IntoStaticStr)] +#[rtype(result = "()")] +pub enum StateSyncEvent { + StatePartReceived(ShardId, u64), +} + /// Combines peer address info, chain. #[derive(Debug, Clone, Eq, PartialEq)] pub struct FullPeerInfo { @@ -404,6 +410,7 @@ pub struct PeerManagerAdapter { pub async_request_sender: AsyncSender, pub request_sender: Sender, pub set_chain_info_sender: Sender, + pub state_sync_event_sender: Sender, } #[cfg(test)] diff --git a/core/chain-configs/src/client_config.rs b/core/chain-configs/src/client_config.rs index 9f2a6885216..db8ad582f32 100644 --- a/core/chain-configs/src/client_config.rs +++ b/core/chain-configs/src/client_config.rs @@ -32,6 +32,10 @@ pub const DEFAULT_GC_NUM_EPOCHS_TO_KEEP: u64 = 5; pub const DEFAULT_STATE_SYNC_NUM_CONCURRENT_REQUESTS_EXTERNAL: u32 = 25; pub const DEFAULT_STATE_SYNC_NUM_CONCURRENT_REQUESTS_ON_CATCHUP_EXTERNAL: u32 = 5; +/// The default number of attempts to obtain a state part from peers in the network +/// before giving up and downloading it from external storage. +pub const DEFAULT_EXTERNAL_STORAGE_FALLBACK_THRESHOLD: u64 = 5; + /// Configuration for garbage collection. #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(default)] @@ -77,6 +81,10 @@ fn default_num_concurrent_requests_during_catchup() -> u32 { DEFAULT_STATE_SYNC_NUM_CONCURRENT_REQUESTS_ON_CATCHUP_EXTERNAL } +fn default_external_storage_fallback_threshold() -> u64 { + DEFAULT_EXTERNAL_STORAGE_FALLBACK_THRESHOLD +} + #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub struct ExternalStorageConfig { /// Location of state parts. @@ -89,6 +97,10 @@ pub struct ExternalStorageConfig { /// to reduce the performance impact of state sync. #[serde(default = "default_num_concurrent_requests_during_catchup")] pub num_concurrent_requests_during_catchup: u32, + /// The number of attempts the node will make to obtain a part from peers in + /// the network before it fetches from external storage. + #[serde(default = "default_external_storage_fallback_threshold")] + pub external_storage_fallback_threshold: u64, } #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] @@ -161,6 +173,7 @@ impl StateSyncConfig { num_concurrent_requests: DEFAULT_STATE_SYNC_NUM_CONCURRENT_REQUESTS_EXTERNAL, num_concurrent_requests_during_catchup: DEFAULT_STATE_SYNC_NUM_CONCURRENT_REQUESTS_ON_CATCHUP_EXTERNAL, + external_storage_fallback_threshold: DEFAULT_EXTERNAL_STORAGE_FALLBACK_THRESHOLD, }), } } diff --git a/integration-tests/src/test_loop/builder.rs b/integration-tests/src/test_loop/builder.rs index e086d883ded..6559afd669d 100644 --- a/integration-tests/src/test_loop/builder.rs +++ b/integration-tests/src/test_loop/builder.rs @@ -250,6 +250,11 @@ impl TestLoopBuilder { location: external_storage_location, num_concurrent_requests: 1, num_concurrent_requests_during_catchup: 1, + // We go straight to storage here because the network layer basically + // doesn't exist in testloop. We could mock a bunch of stuff to make + // the clients transfer state parts "peer to peer" but we wouldn't really + // gain anything over having them dump parts to a tempdir. + external_storage_fallback_threshold: 0, }), }; diff --git a/integration-tests/src/tests/client/sync_state_nodes.rs b/integration-tests/src/tests/client/sync_state_nodes.rs index 280a91fda35..d7473ce51fe 100644 --- a/integration-tests/src/tests/client/sync_state_nodes.rs +++ b/integration-tests/src/tests/client/sync_state_nodes.rs @@ -494,6 +494,7 @@ fn sync_state_dump() { }, num_concurrent_requests: 1, num_concurrent_requests_during_catchup: 1, + external_storage_fallback_threshold: 0, }); let nearcore::NearNode { From 163b48621ade38063ff1ab5c06961aa7505e3998 Mon Sep 17 00:00:00 2001 From: Andrei <122784628+andrei-near@users.noreply.github.com> Date: Wed, 25 Sep 2024 20:19:52 +0300 Subject: [PATCH 23/49] Chores: Adjust nayduck CI (#12145) --- .github/workflows/nayduck_ci.yml | 14 +++++++++----- .github/workflows/neard_release.yml | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/nayduck_ci.yml b/.github/workflows/nayduck_ci.yml index d4791764a67..ce1aac36a13 100644 --- a/.github/workflows/nayduck_ci.yml +++ b/.github/workflows/nayduck_ci.yml @@ -13,10 +13,13 @@ jobs: steps: - name: Install JQ json processor run: sudo apt install jq - + + - name: Checkout nearcore repository + uses: actions/checkout@v4 + - name: Install required python modules run: | - pip3 install -r pytest/requirements.txt + pip3 install -r ./pytest/requirements.txt - name: Create nayduck-code file run: | @@ -24,8 +27,10 @@ jobs: - name: Run Nayduck tests and wait for results run: | - NEW_TEST=$(python3 scripts/nayduck.py --test-file nightly/ci.txt) - RUN_ID=$(echo $NEW_TEST | grep https | sed -E 's|.*\/run\/([0-9]+)|\1|g') + NEW_TEST=$(python3 ./scripts/nayduck.py --test-file nightly/ci.txt) + RUN_ID="$(echo $NEW_TEST | grep https | sed -E 's|.*\/run\/([0-9]+)|\1|' | sed 's/\x1B\[[0-9;]\{1,\}[A-Za-z]//g')" + URL="https://nayduck.nearone.org/api/run/$RUN_ID" + sleep 10 # wait all the tests to finish while true; do @@ -42,5 +47,4 @@ jobs: else echo "CI Nayduck tests are failing https://nayduck.nearone.org/#/run/$RUN_ID." echo "Fix them before merging" - exit 1 fi diff --git a/.github/workflows/neard_release.yml b/.github/workflows/neard_release.yml index 9155f3df05a..d874ff37bc5 100644 --- a/.github/workflows/neard_release.yml +++ b/.github/workflows/neard_release.yml @@ -44,7 +44,7 @@ jobs: with: fetch-depth: 0 - - name: Checkout repository for master branch + - name: Checkout repository for master branch # In case of master branch we want to checkout with depth 1 if: ${{ github.event_name != 'workflow_dispatch' && github.event_name != 'release'}} uses: actions/checkout@v4 From 16db6ec85e2fa51e2e00d8c2b97fa752152e5e77 Mon Sep 17 00:00:00 2001 From: Marcelo Diop-Gonzalez Date: Wed, 25 Sep 2024 17:03:28 -0400 Subject: [PATCH 24/49] =?UTF-8?q?Revert=20"Panic=20in=20`CreateSnapshotReq?= =?UTF-8?q?uest`=20handle=20if=20snapshot=20creation=20=E2=80=A6=20(#12147?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …failed (#10765)" This reverts commit 8a09a2afe136b233e33572d65bdbeeed8decaef9. When the network was resharded, we added this panic in order to exit early if there was some failure, because the node would not be able to reshard without a valid snapshot. But now that decentralized state sync is going to be using these snapshots (and since we're reworking how resharding works anyway), this is no longer an error that requires a panic, since a node should continue to operate correctly even if this part fails. --- chain/chain/src/state_snapshot_actor.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/chain/chain/src/state_snapshot_actor.rs b/chain/chain/src/state_snapshot_actor.rs index b1b2d8a36d8..27756e4796f 100644 --- a/chain/chain/src/state_snapshot_actor.rs +++ b/chain/chain/src/state_snapshot_actor.rs @@ -97,9 +97,7 @@ impl StateSnapshotActor { )); } Err(err) => { - tracing::error!(target: "state_snapshot", ?err, "State snapshot creation failed.\ - State snapshot is needed for correct node performance if it is required by config."); - panic!("State snapshot creation failed") + tracing::error!(target: "state_snapshot", ?err, "State snapshot creation failed") } } } From 96a7c3be4e6e104495de8ee0be7a012b4a4caec3 Mon Sep 17 00:00:00 2001 From: Shreyan Gupta Date: Wed, 25 Sep 2024 15:38:16 -0700 Subject: [PATCH 25/49] [fix] Fix lychee workflow (#12148) This was getting irritating... --------- Co-authored-by: Aleksandr Logunov --- lychee.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lychee.toml b/lychee.toml index 774845a3241..0c55d94200d 100644 --- a/lychee.toml +++ b/lychee.toml @@ -16,4 +16,8 @@ exclude = [ # StackOverflow bans GHA runners "^https://stackoverflow.com/", + + # GNU websites exist but fail with "Too Many Requests" + "^http://www.gnu.org/licenses/", + "^https://www.gnu.org/licenses/why-not-lgpl.en.html", ] From 3e9245769913ee12a2647d5ecf369071aeaad582 Mon Sep 17 00:00:00 2001 From: Andrei <122784628+andrei-near@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:07:12 +0300 Subject: [PATCH 26/49] CI Nayduck fix (#12152) removing -e jq argument as it's causing jq to return non-null exit codes. --- .github/workflows/nayduck_ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/nayduck_ci.yml b/.github/workflows/nayduck_ci.yml index ce1aac36a13..28e149bd4c7 100644 --- a/.github/workflows/nayduck_ci.yml +++ b/.github/workflows/nayduck_ci.yml @@ -35,12 +35,13 @@ jobs: # wait all the tests to finish while true; do TEST_RESULTS=$(curl -s https://nayduck.nearone.org/api/run/$RUN_ID) - TESTS_NOT_READY=$( jq -e '.tests | .[] | select(.status == "RUNNING" or .status == "PENDING" ) ' <<< ${TEST_RESULTS} ) + TESTS_NOT_READY=$(jq '.tests | .[] | select(.status == "RUNNING" or .status == "PENDING") ' <<< ${TEST_RESULTS} ) if [ -z "$TESTS_NOT_READY" ]; then break; fi - sleep 15 + echo "Tests are not ready yet. Sleeping 1 minute..." + sleep 60 done - UNSUCCESSFUL_TESTS=$(jq -e '.tests | .[] | select(.status != "PASSED" and .status != "IGNORED") ' <<< ${TEST_RESULTS} ) + UNSUCCESSFUL_TESTS=$(jq '.tests | .[] | select(.status != "PASSED" and .status != "IGNORED") ' <<< ${TEST_RESULTS} ) if [ -z "$UNSUCCESSFUL_TESTS" ]; then echo "Nayduck CI tests passed." echo "Results available at https://nayduck.nearone.org/#/run/$RUN_ID" From 3f5556b9b9f46a1098722de35e63142e45f17020 Mon Sep 17 00:00:00 2001 From: Aleksandr Logunov Date: Thu, 26 Sep 2024 14:08:10 +0400 Subject: [PATCH 27/49] chore: extract flaky tests from nayduck ci (#12154) These tests should be run on day-to-day basis to monitor progress. But merge on CI should be blocked only by reliable tests. --- nightly/nightly.txt | 23 ++++++++++++++++++++++- nightly/pytest-sanity.txt | 10 ---------- nightly/very_expensive.txt | 11 ----------- 3 files changed, 22 insertions(+), 22 deletions(-) delete mode 100644 nightly/very_expensive.txt diff --git a/nightly/nightly.txt b/nightly/nightly.txt index 11285397dab..9f52404749f 100644 --- a/nightly/nightly.txt +++ b/nightly/nightly.txt @@ -1,4 +1,25 @@ ./sandbox.txt ./pytest.txt ./expensive.txt -./very_expensive.txt \ No newline at end of file + +# Very expensive catchup tests +expensive --timeout=3600 near-client near_client tests::catching_up::test_all_chunks_accepted_1000 +expensive --timeout=3600 near-client near_client tests::catching_up::test_all_chunks_accepted_1000 --features nightly +expensive --timeout=7200 near-client near_client tests::catching_up::test_all_chunks_accepted_1000_slow +expensive --timeout=7200 near-client near_client tests::catching_up::test_all_chunks_accepted_1000_slow --features nightly +expensive --timeout=1800 near-client near_client tests::catching_up::test_all_chunks_accepted_1000_rare_epoch_changing +expensive --timeout=1800 near-client near_client tests::catching_up::test_all_chunks_accepted_1000_rare_epoch_changing --features nightly + +# Very expensive test: make sure Docker image can be build and run +pytest --skip-build --timeout=1h sanity/docker.py + +### Flaky tests. Should be fixed to be added to CI and added back to pytest-sanity.txt +pytest --timeout=120 sanity/garbage_collection.py +pytest --timeout=120 sanity/garbage_collection.py --features nightly +pytest --timeout=120 sanity/validator_switch_key_quick.py +pytest --timeout=120 sanity/validator_switch_key_quick.py --features nightly +pytest --timeout=600 sanity/state_sync_routed.py manytx 115 +pytest --timeout=600 sanity/state_sync_routed.py manytx 115 --features nightly +# Tests for split storage and split storage migration +pytest --timeout=600 sanity/split_storage.py +pytest --timeout=600 sanity/split_storage.py --features nightly diff --git a/nightly/pytest-sanity.txt b/nightly/pytest-sanity.txt index 383ce9bdc69..7b05f4983e3 100644 --- a/nightly/pytest-sanity.txt +++ b/nightly/pytest-sanity.txt @@ -35,8 +35,6 @@ pytest --timeout=240 sanity/state_sync4.py pytest --timeout=240 sanity/state_sync4.py --features nightly pytest --timeout=240 sanity/state_sync5.py pytest --timeout=240 sanity/state_sync5.py --features nightly -pytest --timeout=600 sanity/state_sync_routed.py manytx 115 -pytest --timeout=600 sanity/state_sync_routed.py manytx 115 --features nightly # TODO(#4618): Those tests are currently broken. Comment out while we’re # working on a fix / deciding whether to remove them. #pytest --timeout=300 sanity/state_sync_late.py notx @@ -89,8 +87,6 @@ pytest sanity/rpc_max_gas_burnt.py pytest sanity/rpc_max_gas_burnt.py --features nightly pytest sanity/rpc_tx_status.py pytest sanity/rpc_tx_status.py --features nightly -pytest --timeout=120 sanity/garbage_collection.py -pytest --timeout=120 sanity/garbage_collection.py --features nightly pytest --timeout=120 sanity/garbage_collection1.py pytest --timeout=120 sanity/garbage_collection1.py --features nightly pytest --timeout=180 sanity/garbage_collection_intense.py @@ -113,8 +109,6 @@ pytest sanity/repro_2916.py pytest sanity/repro_2916.py --features nightly pytest --timeout=240 sanity/switch_node_key.py pytest --timeout=240 sanity/switch_node_key.py --features nightly -pytest --timeout=120 sanity/validator_switch_key_quick.py -pytest --timeout=120 sanity/validator_switch_key_quick.py --features nightly pytest --timeout=120 sanity/validator_remove_key_quick.py pytest --timeout=120 sanity/validator_remove_key_quick.py --features nightly pytest --timeout=60 sanity/shadow_tracking.py @@ -162,10 +156,6 @@ pytest sanity/rosetta.py --features nightly # This is the test for meta transactions. pytest sanity/meta_tx.py --features nightly -# Tests for split storage and split storage migration -pytest --timeout=600 sanity/split_storage.py -pytest --timeout=600 sanity/split_storage.py --features nightly - # Tests for resharding # TODO(resharding) Tests for resharding are disabled because resharding is not # compatible with stateless validation, state sync and congestion control. diff --git a/nightly/very_expensive.txt b/nightly/very_expensive.txt deleted file mode 100644 index 6bf4468619c..00000000000 --- a/nightly/very_expensive.txt +++ /dev/null @@ -1,11 +0,0 @@ -# Catchup tests -expensive --timeout=3600 near-client near_client tests::catching_up::test_all_chunks_accepted_1000 -expensive --timeout=3600 near-client near_client tests::catching_up::test_all_chunks_accepted_1000 --features nightly -expensive --timeout=7200 near-client near_client tests::catching_up::test_all_chunks_accepted_1000_slow -expensive --timeout=7200 near-client near_client tests::catching_up::test_all_chunks_accepted_1000_slow --features nightly -expensive --timeout=1800 near-client near_client tests::catching_up::test_all_chunks_accepted_1000_rare_epoch_changing -expensive --timeout=1800 near-client near_client tests::catching_up::test_all_chunks_accepted_1000_rare_epoch_changing --features nightly - -# Make sure Docker image can be build and run -pytest --skip-build --timeout=1h sanity/docker.py - From ec3b627ad77c90a437d51ee8554acce0b0041cee Mon Sep 17 00:00:00 2001 From: Olga Telezhnaya Date: Thu, 26 Sep 2024 14:47:39 +0100 Subject: [PATCH 28/49] Cleanup in issue templates (#12144) Continue working on https://github.com/near/nearcore/pull/12141 - Dropped the prev template, it's redundant now - Fixed feature request template, "Yes" is not a valid string according to the rules - Polishing some details here and there --- .github/ISSUE_TEMPLATE/0-node-issue.yml | 33 +++++++++-------- .github/ISSUE_TEMPLATE/1-rpc-issue.yml | 3 ++ .../2-contract-runtime-issue.yml | 3 ++ .github/ISSUE_TEMPLATE/3-feature-request.yml | 4 +-- .github/ISSUE_TEMPLATE/4-general-issue.yml | 7 ++-- .github/ISSUE_TEMPLATE/5-internal.yml | 1 - .github/ISSUE_TEMPLATE/bug_report.md | 36 ------------------- 7 files changed, 31 insertions(+), 56 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/0-node-issue.yml b/.github/ISSUE_TEMPLATE/0-node-issue.yml index 9ea38aff3dc..c7009210cba 100644 --- a/.github/ISSUE_TEMPLATE/0-node-issue.yml +++ b/.github/ISSUE_TEMPLATE/0-node-issue.yml @@ -21,15 +21,6 @@ body: placeholder: ex. email@example.com validations: required: false - - type: dropdown - id: network - attributes: - label: Which network are you running? - description: Pick the network - options: - - mainnet - - testnet - - other (specify below) - type: dropdown id: node-type attributes: @@ -38,18 +29,22 @@ body: options: - Top 100 Validator - Non-Top 100 Validator - - RPC (Default) + - RPC - Split Storage Archival - Legacy Archival (Deprecated) default: 2 validations: required: true - - type: textarea - id: version + - type: dropdown + id: network attributes: - label: Version - description: What version of neard are you running? Please, provide output of `$NEARD --version`. - render: shell + label: Which network are you running? + description: Pick the network + options: + - mainnet + - testnet + - other (specify below) + default: 0 validations: required: true - type: textarea @@ -59,6 +54,14 @@ body: description: Describe your issue with running a near node. validations: required: true + - type: textarea + id: version + attributes: + label: Version + description: What version of neard are you running? Please, provide output of `$NEARD --version`. + render: shell + validations: + required: true - type: textarea id: logs attributes: diff --git a/.github/ISSUE_TEMPLATE/1-rpc-issue.yml b/.github/ISSUE_TEMPLATE/1-rpc-issue.yml index 1bb82b41f65..76d0786cfcf 100644 --- a/.github/ISSUE_TEMPLATE/1-rpc-issue.yml +++ b/.github/ISSUE_TEMPLATE/1-rpc-issue.yml @@ -28,6 +28,9 @@ body: - mainnet - testnet - other (specify below) + default: 0 + validations: + required: true - type: textarea id: rpc-url attributes: diff --git a/.github/ISSUE_TEMPLATE/2-contract-runtime-issue.yml b/.github/ISSUE_TEMPLATE/2-contract-runtime-issue.yml index ba8f3ad309a..155d1f9c573 100644 --- a/.github/ISSUE_TEMPLATE/2-contract-runtime-issue.yml +++ b/.github/ISSUE_TEMPLATE/2-contract-runtime-issue.yml @@ -28,6 +28,9 @@ body: - mainnet - testnet - other (specify below) + default: 0 + validations: + required: true - type: textarea id: what-happened attributes: diff --git a/.github/ISSUE_TEMPLATE/3-feature-request.yml b/.github/ISSUE_TEMPLATE/3-feature-request.yml index a752473141c..2b882208905 100644 --- a/.github/ISSUE_TEMPLATE/3-feature-request.yml +++ b/.github/ISSUE_TEMPLATE/3-feature-request.yml @@ -44,7 +44,7 @@ body: - type: checkboxes id: ready-to-contribute attributes: - label: Are you ready to contribute to nearcore? + label: Contributing description: Are you interested in implementing this feature? options: - - label: Yes + - label: Yes, I am interested diff --git a/.github/ISSUE_TEMPLATE/4-general-issue.yml b/.github/ISSUE_TEMPLATE/4-general-issue.yml index d43bb85c851..6876188c264 100644 --- a/.github/ISSUE_TEMPLATE/4-general-issue.yml +++ b/.github/ISSUE_TEMPLATE/4-general-issue.yml @@ -1,5 +1,5 @@ name: Bug Report -description: General bug report [use this only if it does not fit into any of the categories above] +description: General bug report. Use this only if it does not fit into any of the categories above. title: "Bug: " labels: ["community", "investigation required"] assignees: @@ -28,6 +28,9 @@ body: - mainnet - testnet - other (specify below) + default: 0 + validations: + required: true - type: textarea id: what-happened attributes: @@ -42,4 +45,4 @@ body: description: Please copy and paste any relevant log output. render: shell validations: - required: false + required: true diff --git a/.github/ISSUE_TEMPLATE/5-internal.yml b/.github/ISSUE_TEMPLATE/5-internal.yml index 05065c6c926..5007e7c1c7e 100644 --- a/.github/ISSUE_TEMPLATE/5-internal.yml +++ b/.github/ISSUE_TEMPLATE/5-internal.yml @@ -1,6 +1,5 @@ name: Internal description: Template for nearcore employees only -title: "Issue: " body: - type: textarea id: content diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 3c138020a20..00000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - - - -**Describe the bug** -Please provide a short description of the bug. - -**To Reproduce** -Steps to reproduce the behavior. - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Version (please complete the following information):** -- nearcore commit/branch -- rust version (if local) -- docker (if using docker) -- mainnet/testnet/betanet/local - -**Additional context** -Add any other context about the problem here. From c9def1635ec7b7d4230d275c0f31342cbb35f7dc Mon Sep 17 00:00:00 2001 From: Shreyan Gupta Date: Thu, 26 Sep 2024 11:19:46 -0700 Subject: [PATCH 29/49] [store] Introduce Flat store adapter (#12123) This is the first concept PR of having adapters on top of store. Most of the details for how it works can be found in core/store/src/adapter/mod.rs The functions in core/store/src/adapter/flat_store.rs are moved from store_helper file. --- chain/chain/src/chain.rs | 8 +- chain/chain/src/chain_update.rs | 8 +- chain/chain/src/flat_storage_creator.rs | 83 ++--- chain/chain/src/garbage_collection.rs | 4 +- chain/chain/src/runtime/mod.rs | 5 +- chain/chain/src/runtime/tests.rs | 4 +- chain/client/src/sync_jobs_actor.rs | 3 +- core/store/src/adapter/flat_store.rs | 332 ++++++++++++++++++ core/store/src/adapter/mod.rs | 104 ++++++ core/store/src/flat/chunk_view.rs | 16 +- core/store/src/flat/delta.rs | 13 +- core/store/src/flat/manager.rs | 38 +- core/store/src/flat/mod.rs | 1 - core/store/src/flat/storage.rs | 275 ++++++--------- core/store/src/flat/store_helper.rs | 285 --------------- core/store/src/genesis/initialization.rs | 8 +- core/store/src/genesis/state_applier.rs | 3 +- core/store/src/lib.rs | 14 + core/store/src/test_utils.rs | 18 +- core/store/src/trie/from_flat.rs | 9 +- core/store/src/trie/mem/loading.rs | 30 +- core/store/src/trie/resharding_v2.rs | 5 +- core/store/src/trie/shard_tries.rs | 14 +- core/store/src/trie/state_parts.rs | 8 +- core/store/src/trie/state_snapshot.rs | 5 +- genesis-tools/genesis-populate/src/lib.rs | 3 +- .../src/tests/client/flat_storage.rs | 99 ++---- .../src/tests/client/process_blocks.rs | 3 +- .../src/tests/client/state_dump.rs | 9 +- .../src/tests/client/state_snapshot.rs | 3 +- .../src/tests/client/sync_state_nodes.rs | 6 +- integration-tests/src/user/runtime_user.rs | 3 +- nearcore/src/entity_debug.rs | 2 +- .../src/estimator_context.rs | 16 +- runtime/runtime/src/prefetch.rs | 3 +- tools/database/src/analyze_delayed_receipt.rs | 3 +- tools/database/src/corrupt.rs | 8 +- tools/database/src/state_perf.rs | 16 +- tools/flat-storage/src/commands.rs | 41 ++- tools/fork-network/src/cli.rs | 9 +- .../src/single_shard_storage_mutator.rs | 5 +- tools/state-viewer/src/apply_chain_range.rs | 6 +- tools/state-viewer/src/commands.rs | 4 +- tools/state-viewer/src/scan_db.rs | 4 +- 44 files changed, 800 insertions(+), 736 deletions(-) create mode 100644 core/store/src/adapter/flat_store.rs create mode 100644 core/store/src/adapter/mod.rs delete mode 100644 core/store/src/flat/store_helper.rs diff --git a/chain/chain/src/chain.rs b/chain/chain/src/chain.rs index 1be35c74e95..f2e8be752f5 100644 --- a/chain/chain/src/chain.rs +++ b/chain/chain/src/chain.rs @@ -90,8 +90,9 @@ use near_primitives::views::{ FinalExecutionOutcomeView, FinalExecutionOutcomeWithReceiptView, FinalExecutionStatus, LightClientBlockView, SignedTransactionView, }; +use near_store::adapter::StoreUpdateAdapter; use near_store::config::StateSnapshotType; -use near_store::flat::{store_helper, FlatStorageReadyStatus, FlatStorageStatus}; +use near_store::flat::{FlatStorageReadyStatus, FlatStorageStatus}; use near_store::trie::mem::resharding::RetainMode; use near_store::DBCol; use near_store::{get_genesis_state_roots, PartialStorage}; @@ -485,7 +486,7 @@ impl Chain { let mut tmp_store_update = store_update.store().store_update(); for shard_uid in epoch_manager.get_shard_layout(genesis_epoch_id)?.shard_uids() { flat_storage_manager.set_flat_storage_for_genesis( - &mut tmp_store_update, + &mut tmp_store_update.flat_store_update(), shard_uid, genesis.hash(), genesis.header().height(), @@ -3022,8 +3023,7 @@ impl Chain { tracing::debug!(target: "store", ?shard_uid, ?flat_head_hash, flat_head_height, "set_state_finalize - initialized flat storage"); let mut store_update = self.runtime_adapter.store().store_update(); - store_helper::set_flat_storage_status( - &mut store_update, + store_update.flat_store_update().set_flat_storage_status( shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: near_store::flat::BlockInfo { diff --git a/chain/chain/src/chain_update.rs b/chain/chain/src/chain_update.rs index 6130e5a4729..9d0a089d767 100644 --- a/chain/chain/src/chain_update.rs +++ b/chain/chain/src/chain_update.rs @@ -134,7 +134,7 @@ impl<'a> ChainUpdate<'a> { shard_uid, apply_result.trie_changes.state_changes(), )?; - self.chain_store_update.merge(store_update); + self.chain_store_update.merge(store_update.into()); self.chain_store_update.save_trie_changes(apply_result.trie_changes); self.chain_store_update.save_outgoing_receipt( @@ -174,7 +174,7 @@ impl<'a> ChainUpdate<'a> { shard_uid, apply_result.trie_changes.state_changes(), )?; - self.chain_store_update.merge(store_update); + self.chain_store_update.merge(store_update.into()); self.chain_store_update.save_chunk_extra(block_hash, &shard_uid, new_extra); self.chain_store_update.save_trie_changes(apply_result.trie_changes); @@ -544,7 +544,7 @@ impl<'a> ChainUpdate<'a> { shard_uid, apply_result.trie_changes.state_changes(), )?; - self.chain_store_update.merge(store_update); + self.chain_store_update.merge(store_update.into()); self.chain_store_update.save_trie_changes(apply_result.trie_changes); @@ -643,7 +643,7 @@ impl<'a> ChainUpdate<'a> { shard_uid, apply_result.trie_changes.state_changes(), )?; - self.chain_store_update.merge(store_update); + self.chain_store_update.merge(store_update.into()); self.chain_store_update.save_trie_changes(apply_result.trie_changes); // The chunk is missing but some fields may need to be updated diff --git a/chain/chain/src/flat_storage_creator.rs b/chain/chain/src/flat_storage_creator.rs index 70064dc908a..edda5a5e6b9 100644 --- a/chain/chain/src/flat_storage_creator.rs +++ b/chain/chain/src/flat_storage_creator.rs @@ -20,12 +20,13 @@ use near_primitives::shard_layout::ShardUId; use near_primitives::state::FlatStateValue; use near_primitives::state_part::PartId; use near_primitives::types::{BlockHeight, StateRoot}; +use near_store::adapter::flat_store::FlatStoreAdapter; +use near_store::adapter::StoreAdapter; use near_store::flat::{ - store_helper, BlockInfo, FetchingStateStatus, FlatStateChanges, FlatStorageCreationMetrics, + BlockInfo, FetchingStateStatus, FlatStateChanges, FlatStorageCreationMetrics, FlatStorageCreationStatus, FlatStorageManager, FlatStorageReadyStatus, FlatStorageStatus, NUM_PARTS_IN_ONE_STEP, STATE_PART_MEMORY_LIMIT, }; -use near_store::Store; use near_store::{Trie, TrieDBStorage, TrieTraversalItem}; use std::collections::HashMap; use std::sync::atomic::AtomicU64; @@ -88,14 +89,14 @@ impl FlatStorageShardCreator { /// Fetch state part, write all state items to flat storage and send the number of items to the given channel. fn fetch_state_part( - store: Store, + store: FlatStoreAdapter, shard_uid: ShardUId, state_root: StateRoot, part_id: PartId, progress: Arc, result_sender: Sender, ) { - let trie_storage = TrieDBStorage::new(store.clone(), shard_uid); + let trie_storage = TrieDBStorage::new(store.store(), shard_uid); let trie = Trie::new(Arc::new(trie_storage), state_root, None); let path_begin = trie.find_state_part_boundary(part_id.idx, part_id.total).unwrap(); let path_end = trie.find_state_part_boundary(part_id.idx + 1, part_id.total).unwrap(); @@ -110,12 +111,7 @@ impl FlatStorageShardCreator { { if let Some(key) = key { let value = trie.retrieve_value(&hash).unwrap(); - store_helper::set_flat_state_value( - &mut store_update, - shard_uid, - key, - Some(FlatStateValue::value_ref(&value)), - ); + store_update.set(shard_uid, key, Some(FlatStateValue::value_ref(&value))); num_items += 1; } } @@ -149,16 +145,16 @@ impl FlatStorageShardCreator { chain_store: &ChainStore, thread_pool: &rayon::ThreadPool, ) -> Result { + let store = chain_store.store().flat_store(); let shard_id = self.shard_uid.shard_id(); - let current_status = - store_helper::get_flat_storage_status(chain_store.store(), self.shard_uid) - .expect("failed to read flat storage status"); + let current_status = store + .get_flat_storage_status(self.shard_uid) + .expect("failed to read flat storage status"); self.metrics.set_status(¤t_status); match ¤t_status { FlatStorageStatus::Empty => { - let mut store_update = chain_store.store().store_update(); - store_helper::set_flat_storage_status( - &mut store_update, + let mut store_update = store.store_update(); + store_update.set_flat_storage_status( self.shard_uid, FlatStorageStatus::Creation(FlatStorageCreationStatus::SavingDeltas), ); @@ -179,11 +175,7 @@ impl FlatStorageShardCreator { for hash in hashes { debug!(target: "store", %shard_id, %height, %hash, "Checking delta existence"); assert_matches!( - store_helper::get_delta_changes( - chain_store.store(), - self.shard_uid, - *hash - ), + store.get_delta(self.shard_uid, *hash), Ok(Some(_)) ); } @@ -192,10 +184,9 @@ impl FlatStorageShardCreator { // We continue saving deltas, and also start fetching state. let block_hash = final_head.last_block_hash; - let store = self.runtime.store().clone(); let epoch_id = self.epoch_manager.get_epoch_id(&block_hash)?; let shard_uid = self.epoch_manager.shard_id_to_uid(shard_id, &epoch_id)?; - let trie_storage = TrieDBStorage::new(store, shard_uid); + let trie_storage = TrieDBStorage::new(store.store(), shard_uid); let state_root = *chain_store.get_chunk_extra(&block_hash, &shard_uid)?.state_root(); let trie = Trie::new(Arc::new(trie_storage), state_root, None); @@ -210,10 +201,9 @@ impl FlatStorageShardCreator { }; info!(target: "store", %shard_id, %final_height, ?status, "Switching status to fetching state"); - let mut store_update = chain_store.store().store_update(); + let mut store_update = store.store_update(); self.metrics.set_flat_head_height(final_head.height); - store_helper::set_flat_storage_status( - &mut store_update, + store_update.set_flat_storage_status( self.shard_uid, FlatStorageStatus::Creation(FlatStorageCreationStatus::FetchingState( status, @@ -225,7 +215,6 @@ impl FlatStorageShardCreator { FlatStorageStatus::Creation(FlatStorageCreationStatus::FetchingState( fetching_state_status, )) => { - let store = self.runtime.store().clone(); let block_hash = fetching_state_status.block_hash; let start_part_id = fetching_state_status.part_id; let num_parts_in_step = fetching_state_status.num_parts_in_step; @@ -280,7 +269,7 @@ impl FlatStorageShardCreator { // Mark that we don't wait for new state parts. self.remaining_state_parts = None; - let mut store_update = chain_store.store().store_update(); + let mut store_update = store.store_update(); if next_start_part_id < num_parts { // If there are still remaining state parts, switch status to the new range of state parts. // We will spawn new rayon tasks on the next status update. @@ -291,8 +280,7 @@ impl FlatStorageShardCreator { num_parts, }; debug!(target: "chain", %shard_id, %block_hash, ?new_status); - store_helper::set_flat_storage_status( - &mut store_update, + store_update.set_flat_storage_status( self.shard_uid, FlatStorageStatus::Creation( FlatStorageCreationStatus::FetchingState(new_status), @@ -302,13 +290,8 @@ impl FlatStorageShardCreator { // If all parts were fetched, we can start catchup. info!(target: "chain", %shard_id, %block_hash, "Finished fetching state"); self.metrics.set_remaining_state_parts(0); - store_helper::remove_delta( - &mut store_update, - self.shard_uid, - block_hash, - ); - store_helper::set_flat_storage_status( - &mut store_update, + store_update.remove_delta(self.shard_uid, block_hash); + store_update.set_flat_storage_status( self.shard_uid, FlatStorageStatus::Creation(FlatStorageCreationStatus::CatchingUp( block_hash, @@ -320,11 +303,10 @@ impl FlatStorageShardCreator { } } FlatStorageStatus::Creation(FlatStorageCreationStatus::CatchingUp(old_flat_head)) => { - let store = self.runtime.store(); let mut flat_head = *old_flat_head; let chain_final_head = chain_store.final_head()?; let mut merged_changes = FlatStateChanges::default(); - let mut store_update = self.runtime.store().store_update(); + let mut store_update = store.store_update(); // Merge up to 50 deltas of the next blocks until we reach chain final head. // TODO: consider merging 10 deltas at once to limit memory usage @@ -338,11 +320,9 @@ impl FlatStorageShardCreator { break; } flat_head = chain_store.get_next_block_hash(&flat_head).unwrap(); - let changes = store_helper::get_delta_changes(store, self.shard_uid, flat_head) - .unwrap() - .unwrap(); + let changes = store.get_delta(self.shard_uid, flat_head).unwrap().unwrap(); merged_changes.merge(changes); - store_helper::remove_delta(&mut store_update, self.shard_uid, flat_head); + store_update.remove_delta(self.shard_uid, flat_head); } if (old_flat_head != &flat_head) || (flat_head == chain_final_head.last_block_hash) @@ -356,8 +336,7 @@ impl FlatStorageShardCreator { debug!(target: "chain", %shard_id, %old_flat_head, %old_height, %flat_head, %height, "Catching up flat head"); self.metrics.set_flat_head_height(height); merged_changes.apply_to_flat_state(&mut store_update, shard_uid); - store_helper::set_flat_storage_status( - &mut store_update, + store_update.set_flat_storage_status( shard_uid, FlatStorageStatus::Creation(FlatStorageCreationStatus::CatchingUp( flat_head, @@ -370,26 +349,22 @@ impl FlatStorageShardCreator { // GC deltas from forks which could have appeared on chain during catchup. // Assuming that flat storage creation finishes in < 2 days, all deltas metadata cannot occupy // more than 2 * (Blocks per day = 48 * 60 * 60) * (BlockInfo size = 72) ~= 12.4 MB. - let mut store_update = self.runtime.store().store_update(); - let deltas_metadata = store_helper::get_all_deltas_metadata(&store, shard_uid) + let mut store_update = store.store_update(); + let deltas_metadata = store.get_all_deltas_metadata(shard_uid) .unwrap_or_else(|_| { panic!("Cannot read flat state deltas metadata for shard {shard_id} from storage") }); let mut gc_count = 0; for delta_metadata in deltas_metadata { if delta_metadata.block.height <= chain_final_head.height { - store_helper::remove_delta( - &mut store_update, - self.shard_uid, - delta_metadata.block.hash, - ); + store_update + .remove_delta(self.shard_uid, delta_metadata.block.hash); gc_count += 1; } } // If we reached chain final head, we can finish catchup and finally create flat storage. - store_helper::set_flat_storage_status( - &mut store_update, + store_update.set_flat_storage_status( self.shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: BlockInfo { diff --git a/chain/chain/src/garbage_collection.rs b/chain/chain/src/garbage_collection.rs index 00da87b8ffe..1b08236231e 100644 --- a/chain/chain/src/garbage_collection.rs +++ b/chain/chain/src/garbage_collection.rs @@ -11,7 +11,7 @@ use near_primitives::shard_layout::get_block_shard_uid; use near_primitives::state_sync::{StateHeaderKey, StatePartKey}; use near_primitives::types::{BlockHeight, BlockHeightDelta, EpochId, NumBlocks, ShardId}; use near_primitives::utils::{get_block_shard_id, get_outcome_id_block_hash, index_to_bytes}; -use near_store::flat::store_helper; +use near_store::adapter::StoreUpdateAdapter; use near_store::{DBCol, KeyForStateChanges, ShardTries, ShardUId}; use crate::types::RuntimeAdapter; @@ -711,7 +711,7 @@ impl<'a> ChainStoreUpdate<'a> { // delete flat storage columns: FlatStateChanges and FlatStateDeltaMetadata let mut store_update = self.store().store_update(); - store_helper::remove_delta(&mut store_update, shard_uid, block_hash); + store_update.flat_store_update().remove_delta(shard_uid, block_hash); self.merge(store_update); } diff --git a/chain/chain/src/runtime/mod.rs b/chain/chain/src/runtime/mod.rs index e79b8e8ea8e..1fd28b915fe 100644 --- a/chain/chain/src/runtime/mod.rs +++ b/chain/chain/src/runtime/mod.rs @@ -37,6 +37,7 @@ use near_primitives::views::{ AccessKeyInfoView, CallResult, ContractCodeView, QueryRequest, QueryResponse, QueryResponseKind, ViewStateResult, }; +use near_store::adapter::{StoreAdapter, StoreUpdateAdapter}; use near_store::config::StateSnapshotType; use near_store::flat::FlatStorageManager; use near_store::metadata::DbKind; @@ -99,7 +100,7 @@ impl NightshadeRuntime { let runtime = Runtime::new(); let trie_viewer = TrieViewer::new(trie_viewer_state_size_limit, max_gas_burnt_view); - let flat_storage_manager = FlatStorageManager::new(store.clone()); + let flat_storage_manager = FlatStorageManager::new(store.flat_store()); let shard_uids: Vec<_> = genesis_config.shard_layout.shard_uids().collect(); let tries = ShardTries::new( store.clone(), @@ -1244,7 +1245,7 @@ impl RuntimeAdapter for NightshadeRuntime { debug!(target: "chain", %shard_id, "Inserting {} values to flat storage", flat_state_delta.len()); // TODO: `apply_to_flat_state` inserts values with random writes, which can be time consuming. // Optimize taking into account that flat state values always correspond to a consecutive range of keys. - flat_state_delta.apply_to_flat_state(&mut store_update, shard_uid); + flat_state_delta.apply_to_flat_state(&mut store_update.flat_store_update(), shard_uid); self.precompile_contracts(epoch_id, contract_codes)?; Ok(store_update.commit()?) } diff --git a/chain/chain/src/runtime/tests.rs b/chain/chain/src/runtime/tests.rs index a852bbdf51e..47d25c449cd 100644 --- a/chain/chain/src/runtime/tests.rs +++ b/chain/chain/src/runtime/tests.rs @@ -150,7 +150,7 @@ impl TestEnv { { let mut store_update = store.store_update(); flat_storage_manager.set_flat_storage_for_genesis( - &mut store_update, + &mut store_update.flat_store_update(), shard_uid, &genesis_hash, 0, @@ -301,7 +301,7 @@ impl TestEnv { }, }; let new_store_update = flat_storage.add_delta(delta).unwrap(); - store_update.merge(new_store_update); + store_update.merge(new_store_update.into()); } store_update.commit().unwrap(); diff --git a/chain/client/src/sync_jobs_actor.rs b/chain/client/src/sync_jobs_actor.rs index f19370ba73c..176151823ad 100644 --- a/chain/client/src/sync_jobs_actor.rs +++ b/chain/client/src/sync_jobs_actor.rs @@ -10,6 +10,7 @@ use near_performance_metrics_macros::perf; use near_primitives::state_part::PartId; use near_primitives::state_sync::StatePartKey; use near_primitives::types::ShardId; +use near_store::adapter::StoreUpdateAdapter; use near_store::DBCol; // Set the mailbox capacity for the SyncJobsActor from default 16 to 100. @@ -100,7 +101,7 @@ impl SyncJobsActor { let success = msg .runtime_adapter .get_flat_storage_manager() - .remove_flat_storage_for_shard(msg.shard_uid, &mut store_update)?; + .remove_flat_storage_for_shard(msg.shard_uid, &mut store_update.flat_store_update())?; store_update.commit()?; Ok(success) } diff --git a/core/store/src/adapter/flat_store.rs b/core/store/src/adapter/flat_store.rs new file mode 100644 index 00000000000..07eabea27e6 --- /dev/null +++ b/core/store/src/adapter/flat_store.rs @@ -0,0 +1,332 @@ +use std::io; + +use borsh::BorshDeserialize; +use near_primitives::hash::CryptoHash; +use near_primitives::shard_layout::ShardUId; +use near_primitives::state::FlatStateValue; + +use crate::flat::delta::{BlockWithChangesInfo, KeyForFlatStateDelta}; +use crate::flat::{ + FlatStateChanges, FlatStateDelta, FlatStateDeltaMetadata, FlatStateIterator, FlatStorageError, + FlatStorageReadyStatus, FlatStorageStatus, +}; +use crate::{DBCol, Store, StoreUpdate}; + +use super::{StoreAdapter, StoreUpdateAdapter, StoreUpdateHolder}; + +#[derive(Clone)] +pub struct FlatStoreAdapter { + store: Store, +} + +impl StoreAdapter for FlatStoreAdapter { + fn store(&self) -> Store { + self.store.clone() + } +} + +impl FlatStoreAdapter { + pub fn new(store: Store) -> Self { + Self { store } + } + + pub fn store_update(&self) -> FlatStoreUpdateAdapter<'static> { + FlatStoreUpdateAdapter { store_update: StoreUpdateHolder::Owned(self.store.store_update()) } + } + + pub fn exists(&self, shard_uid: ShardUId, key: &[u8]) -> Result { + let db_key = encode_flat_state_db_key(shard_uid, key); + self.store.exists(DBCol::FlatState, &db_key).map_err(|err| { + FlatStorageError::StorageInternalError(format!("failed to read FlatState value: {err}")) + }) + } + + pub fn get( + &self, + shard_uid: ShardUId, + key: &[u8], + ) -> Result, FlatStorageError> { + let db_key = encode_flat_state_db_key(shard_uid, key); + self.store.get_ser(DBCol::FlatState, &db_key).map_err(|err| { + FlatStorageError::StorageInternalError(format!("failed to read FlatState value: {err}")) + }) + } + + pub fn get_flat_storage_status( + &self, + shard_uid: ShardUId, + ) -> Result { + self.store + .get_ser(DBCol::FlatStorageStatus, &shard_uid.to_bytes()) + .map(|status| status.unwrap_or(FlatStorageStatus::Empty)) + .map_err(|err| { + FlatStorageError::StorageInternalError(format!( + "failed to read flat storage status: {err}" + )) + }) + } + + pub fn get_delta( + &self, + shard_uid: ShardUId, + block_hash: CryptoHash, + ) -> Result, FlatStorageError> { + let key = KeyForFlatStateDelta { shard_uid, block_hash }; + self.store.get_ser::(DBCol::FlatStateChanges, &key.to_bytes()).map_err( + |err| { + FlatStorageError::StorageInternalError(format!( + "failed to read delta changes for {key:?}: {err}" + )) + }, + ) + } + + pub fn get_all_deltas_metadata( + &self, + shard_uid: ShardUId, + ) -> Result, FlatStorageError> { + self.store + .iter_prefix_ser(DBCol::FlatStateDeltaMetadata, &shard_uid.to_bytes()) + .map(|res| { + res.map(|(_, value)| value).map_err(|err| { + FlatStorageError::StorageInternalError(format!( + "failed to read delta metadata: {err}" + )) + }) + }) + .collect() + } + + pub fn get_prev_block_with_changes( + &self, + shard_uid: ShardUId, + block_hash: CryptoHash, + prev_hash: CryptoHash, + ) -> Result, FlatStorageError> { + let key = KeyForFlatStateDelta { shard_uid, block_hash: prev_hash }.to_bytes(); + let prev_delta_metadata: Option = + self.store.get_ser(DBCol::FlatStateDeltaMetadata, &key).map_err(|err| { + FlatStorageError::StorageInternalError(format!( + "failed to read delta metadata for {key:?}: {err}" + )) + })?; + + let prev_block_with_changes = match prev_delta_metadata { + None => { + // DeltaMetadata not found, which means the prev block is the flat head. + let flat_storage_status = self.get_flat_storage_status(shard_uid)?; + match flat_storage_status { + FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head }) => { + if flat_head.hash == prev_hash { + Some(BlockWithChangesInfo { hash: prev_hash, height: flat_head.height }) + } else { + tracing::error!(target: "store", ?block_hash, ?prev_hash, "Missing delta metadata"); + None + } + } + // Don't do any performance optimizations while flat storage is not ready. + _ => None, + } + } + Some(metadata) => { + // If the prev block contains `prev_block_with_changes`, then use that value. + // Otherwise reference the prev block. + Some(metadata.prev_block_with_changes.unwrap_or(BlockWithChangesInfo { + hash: metadata.block.hash, + height: metadata.block.height, + })) + } + }; + Ok(prev_block_with_changes) + } + + /// Returns iterator over entire range of flat storage entries. + /// It reads data only from `FlatState` column which represents the state at + /// flat storage head. Reads only committed changes. + pub fn iter<'a>(&'a self, shard_uid: ShardUId) -> FlatStateIterator<'a> { + self.iter_range(shard_uid, None, None) + } + + /// Returns iterator over flat storage entries for a given shard and range of state keys. + /// It reads data only from `FlatState` column which represents the state at + /// flat storage head. Reads only committed changes. + pub fn iter_range<'a>( + &'a self, + shard_uid: ShardUId, + from: Option<&[u8]>, + to: Option<&[u8]>, + ) -> FlatStateIterator<'a> { + // If left direction is unbounded, encoded `shard_uid` serves as the + // smallest possible key in DB for the shard. + let db_key_from = match from { + Some(from) => encode_flat_state_db_key(shard_uid, from), + None => shard_uid.to_bytes().to_vec(), + }; + // If right direction is unbounded, `ShardUId::next_shard_prefix` serves as + // the key which is strictly bigger than all keys in DB for this shard and + // still doesn't include keys from other shards. + let db_key_to = match to { + Some(to) => encode_flat_state_db_key(shard_uid, to), + None => ShardUId::next_shard_prefix(&shard_uid.to_bytes()).to_vec(), + }; + let iter = self + .store + .iter_range(DBCol::FlatState, Some(&db_key_from), Some(&db_key_to)) + .map(|result| match result { + Ok((key, value)) => Ok(( + decode_flat_state_db_key(&key) + .map_err(|err| { + FlatStorageError::StorageInternalError(format!( + "invalid FlatState key format: {err}" + )) + })? + .1, + FlatStateValue::try_from_slice(&value).map_err(|err| { + FlatStorageError::StorageInternalError(format!( + "invalid FlatState value format: {err}" + )) + })?, + )), + Err(err) => Err(FlatStorageError::StorageInternalError(format!( + "FlatState iterator error: {err}" + ))), + }); + Box::new(iter) + } +} + +pub struct FlatStoreUpdateAdapter<'a> { + store_update: StoreUpdateHolder<'a>, +} + +impl Into for FlatStoreUpdateAdapter<'static> { + fn into(self) -> StoreUpdate { + self.store_update.into() + } +} + +impl FlatStoreUpdateAdapter<'static> { + pub fn commit(self) -> io::Result<()> { + let store_update: StoreUpdate = self.into(); + store_update.commit() + } +} + +impl<'a> StoreUpdateAdapter for FlatStoreUpdateAdapter<'a> { + fn store_update(&mut self) -> &mut StoreUpdate { + &mut self.store_update + } +} + +impl<'a> FlatStoreUpdateAdapter<'a> { + pub fn new(store_update: &'a mut StoreUpdate) -> Self { + Self { store_update: StoreUpdateHolder::Reference(store_update) } + } + + pub fn set(&mut self, shard_uid: ShardUId, key: Vec, value: Option) { + let db_key = encode_flat_state_db_key(shard_uid, &key); + match value { + Some(value) => self + .store_update + .set_ser(DBCol::FlatState, &db_key, &value) + .expect("Borsh should not have failed here"), + None => self.store_update.delete(DBCol::FlatState, &db_key), + } + } + + pub fn remove_all(&mut self, shard_uid: ShardUId) { + self.remove_range_by_shard_uid(shard_uid, DBCol::FlatState); + } + + pub fn set_flat_storage_status(&mut self, shard_uid: ShardUId, status: FlatStorageStatus) { + self.store_update + .set_ser(DBCol::FlatStorageStatus, &shard_uid.to_bytes(), &status) + .expect("Borsh should not have failed here") + } + + pub fn set_delta(&mut self, shard_uid: ShardUId, delta: &FlatStateDelta) { + let key = + KeyForFlatStateDelta { shard_uid, block_hash: delta.metadata.block.hash }.to_bytes(); + self.store_update + .set_ser(DBCol::FlatStateChanges, &key, &delta.changes) + .expect("Borsh should not have failed here"); + self.store_update + .set_ser(DBCol::FlatStateDeltaMetadata, &key, &delta.metadata) + .expect("Borsh should not have failed here"); + } + + pub fn remove_delta(&mut self, shard_uid: ShardUId, block_hash: CryptoHash) { + let key = KeyForFlatStateDelta { shard_uid, block_hash }.to_bytes(); + self.store_update.delete(DBCol::FlatStateChanges, &key); + self.store_update.delete(DBCol::FlatStateDeltaMetadata, &key); + } + + pub fn remove_all_deltas(&mut self, shard_uid: ShardUId) { + self.remove_range_by_shard_uid(shard_uid, DBCol::FlatStateChanges); + self.remove_range_by_shard_uid(shard_uid, DBCol::FlatStateDeltaMetadata); + } + + // helper + fn remove_range_by_shard_uid(&mut self, shard_uid: ShardUId, col: DBCol) { + let key_from = shard_uid.to_bytes(); + let key_to = ShardUId::next_shard_prefix(&key_from); + self.store_update.delete_range(col, &key_from, &key_to); + } +} + +pub fn encode_flat_state_db_key(shard_uid: ShardUId, key: &[u8]) -> Vec { + let mut buffer = vec![]; + buffer.extend_from_slice(&shard_uid.to_bytes()); + buffer.extend_from_slice(key); + buffer +} + +pub fn decode_flat_state_db_key(key: &[u8]) -> io::Result<(ShardUId, Vec)> { + let (shard_uid_bytes, trie_key) = key.split_at_checked(8).ok_or_else(|| { + io::Error::other(format!("expected FlatState key length to be at least 8: {key:?}")) + })?; + let shard_uid = shard_uid_bytes.try_into().map_err(|err| { + io::Error::other(format!("failed to decode shard_uid as part of FlatState key: {err}")) + })?; + Ok((shard_uid, trie_key.to_vec())) +} + +#[cfg(test)] +mod tests { + use near_primitives::shard_layout::ShardUId; + use near_primitives::state::FlatStateValue; + + use crate::adapter::{StoreAdapter, StoreUpdateAdapter}; + use crate::test_utils::create_test_store; + + #[test] + fn iter_flat_state_entries() { + // Setup shards and store + let store = create_test_store().flat_store(); + let shard_uids = [0, 1, 2].map(|id| ShardUId { version: 0, shard_id: id }); + + for (i, shard_uid) in shard_uids.iter().enumerate() { + let mut store_update = store.store_update(); + let key: Vec = vec![0, 1, i as u8]; + let val: Vec = vec![0, 1, 2, i as u8]; + + // Add value to FlatState + store_update.flat_store_update().set( + *shard_uid, + key.clone(), + Some(FlatStateValue::inlined(&val)), + ); + + store_update.commit().unwrap(); + } + + for (i, shard_uid) in shard_uids.iter().enumerate() { + let entries: Vec<_> = store.iter(*shard_uid).collect(); + assert_eq!(entries.len(), 1); + let key: Vec = vec![0, 1, i as u8]; + let val: Vec = vec![0, 1, 2, i as u8]; + + assert_eq!(entries, vec![Ok((key, FlatStateValue::inlined(&val)))]); + } + } +} diff --git a/core/store/src/adapter/mod.rs b/core/store/src/adapter/mod.rs new file mode 100644 index 00000000000..4475d866489 --- /dev/null +++ b/core/store/src/adapter/mod.rs @@ -0,0 +1,104 @@ +pub mod flat_store; + +use std::ops::{Deref, DerefMut}; + +use crate::{Store, StoreUpdate}; + +/// Internal enum that can store either an owned StoreUpdate to a reference to StoreUpdate. +/// +/// While dealing with store update, the typical pattern is to do something like: +/// ```rust, ignore +/// let store_update: StoreUpdate = store.store_update(); +/// +/// store_update.set_foo("bar"); +/// some_large_update_function(&mut store_update); +/// +/// store_update.commit()?; +/// ``` +/// Now with StoreAdapters, store could be of any of the type of the adapters, example `FlatStoreAdapter`. +/// In that case, we expect the above pattern to look similar, however we would expect calls to +/// `flat_store.store_update()` to return type `FlatStoreUpdateAdapter` instead of `StoreUpdate`. +/// +/// At the same time we would like to allow conversion of `StoreUpdate` to `FlatStoreUpdateAdapter`. +/// +/// ```rust, ignore +/// fn update_flat_store(flat_store_update: &mut FlatStoreUpdateAdapter) { +/// ... +/// } +/// +/// // Pattern 1: reference to store_update +/// let store_update: StoreUpdate = store.store_update(); +/// update_flat_store(&mut store_update.flat_store_update()); +/// store_update.commit()?; +/// +/// // Pattern 2: owned store_update +/// let flat_store: FlatStoreAdapter = store.flat_store(); +/// let flat_store_update: FlatStoreUpdateAdapter<'static> = flat_store.store_update(); +/// update_flat_store(&mut flat_store_update); +/// flat_store_update.commit()?; +/// ``` +/// +/// To make both these patterns possible, where in pattern 1, flat_store_update holds a reference to store_update +/// and in pattern 2, flat_store_update owns the instance of store_update, we use this enum. +/// +/// Note that owned versions of flat_store_update have a static lifetime as compared to borrowed versions. +enum StoreUpdateHolder<'a> { + Reference(&'a mut StoreUpdate), + Owned(StoreUpdate), +} + +// Seamless conversion from &store_update_holder to &store_update. +impl Deref for StoreUpdateHolder<'_> { + type Target = StoreUpdate; + + fn deref(&self) -> &Self::Target { + match self { + StoreUpdateHolder::Reference(store_update) => store_update, + StoreUpdateHolder::Owned(store_update) => store_update, + } + } +} + +// Seamless conversion from &mut store_update_holder to &mut store_update. +impl DerefMut for StoreUpdateHolder<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + match self { + StoreUpdateHolder::Reference(store_update) => store_update, + StoreUpdateHolder::Owned(store_update) => store_update, + } + } +} + +// Static instances of StoreUpdateHolder always hold an owned StoreUpdate instance. +// In such case it should be possible to convert it to StoreUpdate. +impl Into for StoreUpdateHolder<'static> { + fn into(self) -> StoreUpdate { + match self { + StoreUpdateHolder::Reference(_) => panic!("converting borrowed store update"), + StoreUpdateHolder::Owned(store_update) => store_update, + } + } +} + +/// Simple adapter wrapper on top of Store to provide a more ergonomic interface for different store types. +/// We provide simple inter-convertibility between different store types like FlatStoreAdapter and TrieStoreAdapter. +pub trait StoreAdapter { + fn store(&self) -> Store; + + fn flat_store(&self) -> flat_store::FlatStoreAdapter { + flat_store::FlatStoreAdapter::new(self.store()) + } +} + +/// Simple adapter wrapper on top of StoreUpdate to provide a more ergonomic interface for +/// different store update types. +/// We provide simple inter-convertibility between different store update types like FlatStoreUpdateAdapter +/// and TrieStoreUpdateAdapter, however these are conversions by reference only. +/// The underlying StoreUpdate instance remains the same. +pub trait StoreUpdateAdapter: Sized { + fn store_update(&mut self) -> &mut StoreUpdate; + + fn flat_store_update(&mut self) -> flat_store::FlatStoreUpdateAdapter { + flat_store::FlatStoreUpdateAdapter::new(self.store_update()) + } +} diff --git a/core/store/src/flat/chunk_view.rs b/core/store/src/flat/chunk_view.rs index c7cc583dcfb..d704e2a013c 100644 --- a/core/store/src/flat/chunk_view.rs +++ b/core/store/src/flat/chunk_view.rs @@ -1,10 +1,8 @@ -use crate::flat::store_helper; +use crate::adapter::flat_store::FlatStoreAdapter; use near_primitives::hash::CryptoHash; use near_primitives::shard_layout::ShardUId; use near_primitives::state::FlatStateValue; -use crate::Store; - use super::types::FlatStateIterator; use super::FlatStorage; @@ -21,7 +19,7 @@ pub struct FlatStorageChunkView { /// Used to access flat state stored at the head of flat storage. /// It should store all trie keys and values/value refs for the state on top of /// flat_storage.head, except for delayed receipt keys. - store: Store, + store: FlatStoreAdapter, /// The block for which key-value pairs of its state will be retrieved. The flat state /// will reflect the state AFTER the block is applied. block_hash: CryptoHash, @@ -31,7 +29,7 @@ pub struct FlatStorageChunkView { } impl FlatStorageChunkView { - pub fn new(store: Store, block_hash: CryptoHash, flat_storage: FlatStorage) -> Self { + pub fn new(store: FlatStoreAdapter, block_hash: CryptoHash, flat_storage: FlatStorage) -> Self { Self { store, block_hash, flat_storage } } /// Returns value reference using raw trie key, taken from the state @@ -49,12 +47,8 @@ impl FlatStorageChunkView { self.flat_storage.contains_key(&self.block_hash, key) } - pub fn iter_flat_state_entries<'a>( - &'a self, - from: Option<&[u8]>, - to: Option<&[u8]>, - ) -> FlatStateIterator<'a> { - store_helper::iter_flat_state_entries(self.flat_storage.shard_uid(), &self.store, from, to) + pub fn iter_range(&self, from: Option<&[u8]>, to: Option<&[u8]>) -> FlatStateIterator { + self.store.iter_range(self.flat_storage.shard_uid(), from, to) } pub fn get_head_hash(&self) -> CryptoHash { diff --git a/core/store/src/flat/delta.rs b/core/store/src/flat/delta.rs index 1fb086c3501..6cc5363782f 100644 --- a/core/store/src/flat/delta.rs +++ b/core/store/src/flat/delta.rs @@ -9,8 +9,9 @@ use near_schema_checker_lib::ProtocolSchema; use std::collections::HashMap; use std::sync::Arc; -use super::{store_helper, BlockInfo}; -use crate::{CryptoHash, StoreUpdate}; +use super::BlockInfo; +use crate::adapter::flat_store::FlatStoreUpdateAdapter; +use crate::CryptoHash; #[derive(Debug)] pub struct FlatStateDelta { @@ -132,9 +133,13 @@ impl FlatStateChanges { } /// Applies delta to the flat state. - pub fn apply_to_flat_state(self, store_update: &mut StoreUpdate, shard_uid: ShardUId) { + pub fn apply_to_flat_state( + self, + store_update: &mut FlatStoreUpdateAdapter, + shard_uid: ShardUId, + ) { for (key, value) in self.0.into_iter() { - store_helper::set_flat_state_value(store_update, shard_uid, key, value); + store_update.set(shard_uid, key, value); } } } diff --git a/core/store/src/flat/manager.rs b/core/store/src/flat/manager.rs index 558f9798d5a..47168512acb 100644 --- a/core/store/src/flat/manager.rs +++ b/core/store/src/flat/manager.rs @@ -1,6 +1,5 @@ -use crate::flat::{ - store_helper, BlockInfo, FlatStorageReadyStatus, FlatStorageStatus, POISONED_LOCK_ERR, -}; +use crate::adapter::flat_store::{FlatStoreAdapter, FlatStoreUpdateAdapter}; +use crate::flat::{BlockInfo, FlatStorageReadyStatus, FlatStorageStatus, POISONED_LOCK_ERR}; use near_primitives::errors::StorageError; use near_primitives::hash::CryptoHash; use near_primitives::shard_layout::ShardUId; @@ -9,8 +8,6 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex}; use tracing::debug; -use crate::{Store, StoreUpdate}; - use super::chunk_view::FlatStorageChunkView; use super::{ FlatStateChanges, FlatStateDelta, FlatStateDeltaMetadata, FlatStorage, FlatStorageError, @@ -23,7 +20,7 @@ use super::{ pub struct FlatStorageManager(Arc); pub struct FlatStorageManagerInner { - store: Store, + store: FlatStoreAdapter, /// Here we store the flat_storage per shard. The reason why we don't use the same /// FlatStorage for all shards is that there are two modes of block processing, /// normal block processing and block catchups. Since these are performed on different range @@ -36,7 +33,7 @@ pub struct FlatStorageManagerInner { } impl FlatStorageManager { - pub fn new(store: Store) -> Self { + pub fn new(store: FlatStoreAdapter) -> Self { Self(Arc::new(FlatStorageManagerInner { store, flat_storages: Default::default() })) } @@ -47,15 +44,14 @@ impl FlatStorageManager { /// an empty database. pub fn set_flat_storage_for_genesis( &self, - store_update: &mut StoreUpdate, + store_update: &mut FlatStoreUpdateAdapter, shard_uid: ShardUId, genesis_block: &CryptoHash, genesis_height: BlockHeight, ) { let flat_storages = self.0.flat_storages.lock().expect(POISONED_LOCK_ERR); assert!(!flat_storages.contains_key(&shard_uid)); - store_helper::set_flat_storage_status( - store_update, + store_update.set_flat_storage_status( shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: BlockInfo::genesis(*genesis_block, genesis_height), @@ -136,18 +132,15 @@ impl FlatStorageManager { height: BlockHeight, shard_uid: ShardUId, state_changes: &[RawStateChangesWithTrieKey], - ) -> Result { + ) -> Result, StorageError> { let prev_block_with_changes = if state_changes.is_empty() { // The current block has no flat state changes. // Find the last block with flat state changes by looking it up in // the prev block. - store_helper::get_prev_block_with_changes( - &self.0.store, - shard_uid, - block_hash, - prev_hash, - ) - .map_err(|e| StorageError::from(e))? + self.0 + .store + .get_prev_block_with_changes(shard_uid, block_hash, prev_hash) + .map_err(|e| StorageError::from(e))? } else { // The current block has flat state changes. None @@ -167,8 +160,8 @@ impl FlatStorageManager { } else { // Otherwise, save delta to disk so it will be used for flat storage creation later. debug!(target: "store", %shard_uid, "Add delta for flat storage creation"); - let mut store_update: StoreUpdate = self.0.store.store_update(); - store_helper::set_delta(&mut store_update, shard_uid, &delta); + let mut store_update = self.0.store.store_update(); + store_update.set_delta(shard_uid, &delta); store_update }; @@ -176,8 +169,7 @@ impl FlatStorageManager { } pub fn get_flat_storage_status(&self, shard_uid: ShardUId) -> FlatStorageStatus { - store_helper::get_flat_storage_status(&self.0.store, shard_uid) - .expect("failed to read flat storage status") + self.0.store.get_flat_storage_status(shard_uid).expect("failed to read flat storage status") } /// Creates `FlatStorageChunkView` to access state for `shard_uid` and block `block_hash`. @@ -216,7 +208,7 @@ impl FlatStorageManager { pub fn remove_flat_storage_for_shard( &self, shard_uid: ShardUId, - store_update: &mut StoreUpdate, + store_update: &mut FlatStoreUpdateAdapter, ) -> Result { let mut flat_storages = self.0.flat_storages.lock().expect(POISONED_LOCK_ERR); if let Some(flat_store) = flat_storages.remove(&shard_uid) { diff --git a/core/store/src/flat/mod.rs b/core/store/src/flat/mod.rs index ff714ace1e6..1e0b1d967ab 100644 --- a/core/store/src/flat/mod.rs +++ b/core/store/src/flat/mod.rs @@ -30,7 +30,6 @@ pub mod delta; mod manager; mod metrics; mod storage; -pub mod store_helper; #[cfg(test)] pub mod test_utils; mod types; diff --git a/core/store/src/flat/storage.rs b/core/store/src/flat/storage.rs index e16d4a6113a..172928d35eb 100644 --- a/core/store/src/flat/storage.rs +++ b/core/store/src/flat/storage.rs @@ -8,14 +8,13 @@ use near_primitives::state::FlatStateValue; use near_primitives::types::BlockHeight; use tracing::{debug, warn}; +use crate::adapter::flat_store::{FlatStoreAdapter, FlatStoreUpdateAdapter}; use crate::flat::delta::{BlockWithChangesInfo, CachedFlatStateChanges}; use crate::flat::BlockInfo; use crate::flat::{FlatStorageReadyStatus, FlatStorageStatus}; -use crate::{Store, StoreUpdate}; use super::delta::{CachedFlatStateDelta, FlatStateDelta}; use super::metrics::FlatStorageMetrics; -use super::store_helper; use super::types::FlatStorageError; /// FlatStorage stores information on which blocks flat storage current supports key lookups on. @@ -35,7 +34,7 @@ pub struct FlatStorage(pub(crate) Arc>); // This makes sure that when a node restarts, FlatStorage can load changes for all blocks // after the `flat_head` block successfully. pub(crate) struct FlatStorageInner { - store: Store, + store: FlatStoreAdapter, /// UId of the shard which state is accessed by this flat storage. shard_uid: ShardUId, /// The block for which we store the key value pairs of the state after it is applied. @@ -235,9 +234,9 @@ impl FlatStorage { /// Create a new FlatStorage for `shard_uid` using flat head if it is stored on storage. /// We also load all blocks with height between flat head to `latest_block_height` /// including those on forks into the returned FlatStorage. - pub fn new(store: Store, shard_uid: ShardUId) -> Result { + pub fn new(store: FlatStoreAdapter, shard_uid: ShardUId) -> Result { let shard_id = shard_uid.shard_id(); - let flat_head = match store_helper::get_flat_storage_status(&store, shard_uid) { + let flat_head = match store.get_flat_storage_status(shard_uid) { Ok(FlatStorageStatus::Ready(ready_status)) => ready_status.flat_head, status => { return Err(StorageError::StorageInconsistentState(format!( @@ -248,15 +247,15 @@ impl FlatStorage { let metrics = FlatStorageMetrics::new(shard_uid); metrics.set_flat_head_height(flat_head.height); - let deltas_metadata = store_helper::get_all_deltas_metadata(&store, shard_uid) - .unwrap_or_else(|_| { - panic!("Cannot read flat state deltas metadata for shard {shard_id} from storage") - }); + let deltas_metadata = store.get_all_deltas_metadata(shard_uid).unwrap_or_else(|_| { + panic!("Cannot read flat state deltas metadata for shard {shard_id} from storage") + }); let mut deltas = HashMap::new(); for delta_metadata in deltas_metadata { let block_hash = delta_metadata.block.hash; let changes: CachedFlatStateChanges = if delta_metadata.has_changes() { - store_helper::get_delta_changes(&store, shard_uid, block_hash) + store + .get_delta(shard_uid, block_hash) .expect("failed to read flat state delta changes") .unwrap_or_else(|| { panic!("cannot find block delta for block {block_hash:?} shard {shard_id}") @@ -301,8 +300,7 @@ impl FlatStorage { key: &[u8], ) -> Result, crate::StorageError> { let guard = self.0.read().expect(super::POISONED_LOCK_ERR); - let blocks_to_head = - guard.get_blocks_to_head(block_hash).map_err(|e| StorageError::from(e))?; + let blocks_to_head = guard.get_blocks_to_head(block_hash)?; for block_hash in blocks_to_head.iter() { // If we found a key in changes, we can return a value because it is the most recent key update. let changes = guard.get_block_changes(block_hash)?; @@ -314,7 +312,7 @@ impl FlatStorage { }; } - let value = store_helper::get_flat_state_value(&guard.store, guard.shard_uid, key)?; + let value = guard.store.get(guard.shard_uid, key)?; Ok(value) } @@ -336,10 +334,7 @@ impl FlatStorage { }; } - let db_key = store_helper::encode_flat_state_db_key(guard.shard_uid, key); - Ok(guard.store.exists(crate::DBCol::FlatState, &db_key).map_err(|err| { - FlatStorageError::StorageInternalError(format!("failed to read FlatState value: {err}")) - })?) + Ok(guard.store.exists(guard.shard_uid, key)?) } // TODO(#11601): Direct call is DEPRECATED, consider removing non-strict mode. @@ -389,10 +384,12 @@ impl FlatStorage { let blocks = guard.get_blocks_to_head(&new_head)?; for block_hash in blocks.into_iter().rev() { - let mut store_update = StoreUpdate::new(guard.store.storage.clone()); + let mut store_update = guard.store.store_update(); // Delta must exist because flat storage is locked and we could retrieve // path from old to new head. Otherwise we return internal error. - let changes = store_helper::get_delta_changes(&guard.store, shard_uid, block_hash)? + let changes = guard + .store + .get_delta(shard_uid, block_hash)? .ok_or_else(|| missing_delta_error(&block_hash))?; changes.apply_to_flat_state(&mut store_update, guard.shard_uid); let metadata = guard @@ -402,8 +399,7 @@ impl FlatStorage { .metadata; let block = metadata.block; let block_height = block.height; - store_helper::set_flat_storage_status( - &mut store_update, + store_update.set_flat_storage_status( shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: block }), ); @@ -425,7 +421,7 @@ impl FlatStorage { .cloned() .collect(); for hash in hashes_to_remove { - store_helper::remove_delta(&mut store_update, shard_uid, hash); + store_update.remove_delta(shard_uid, hash); guard.deltas.remove(&hash); } @@ -450,7 +446,10 @@ impl FlatStorage { /// committed to disk in one db transaction together with the rest of changes caused by block, /// in case the node stopped or crashed in between and a block is on chain but its delta is not /// stored or vice versa. - pub fn add_delta(&self, delta: FlatStateDelta) -> Result { + pub fn add_delta( + &self, + delta: FlatStateDelta, + ) -> Result, FlatStorageError> { let mut guard = self.0.write().expect(super::POISONED_LOCK_ERR); let shard_uid = guard.shard_uid; let block = &delta.metadata.block; @@ -460,8 +459,8 @@ impl FlatStorage { if block.prev_hash != guard.flat_head.hash && !guard.deltas.contains_key(&block.prev_hash) { return Err(guard.create_block_not_supported_error(&block_hash)); } - let mut store_update = StoreUpdate::new(guard.store.storage.clone()); - store_helper::set_delta(&mut store_update, shard_uid, &delta); + let mut store_update = guard.store.store_update(); + store_update.set_delta(shard_uid, &delta); let cached_changes: CachedFlatStateChanges = delta.changes.into(); guard.deltas.insert( block_hash, @@ -473,12 +472,15 @@ impl FlatStorage { } /// Clears all State key-value pairs from flat storage. - pub fn clear_state(&self, store_update: &mut StoreUpdate) -> Result<(), StorageError> { + pub fn clear_state( + &self, + store_update: &mut FlatStoreUpdateAdapter, + ) -> Result<(), StorageError> { let guard = self.0.write().expect(super::POISONED_LOCK_ERR); let shard_uid = guard.shard_uid; - store_helper::remove_all_flat_state_values(store_update, shard_uid); - store_helper::remove_all_deltas(store_update, shard_uid); - store_helper::set_flat_storage_status(store_update, shard_uid, FlatStorageStatus::Empty); + store_update.remove_all(shard_uid); + store_update.remove_all_deltas(shard_uid); + store_update.set_flat_storage_status(shard_uid, FlatStorageStatus::Empty); guard.update_delta_metrics(); Ok(()) } @@ -511,6 +513,7 @@ fn missing_delta_error(block_hash: &CryptoHash) -> FlatStorageError { #[cfg(test)] mod tests { + use crate::adapter::StoreAdapter; use crate::flat::delta::{ BlockWithChangesInfo, FlatStateChanges, FlatStateDelta, FlatStateDeltaMetadata, }; @@ -518,7 +521,7 @@ mod tests { use crate::flat::storage::FlatStorageInner; use crate::flat::test_utils::MockChain; use crate::flat::types::FlatStorageError; - use crate::flat::{store_helper, FlatStorageReadyStatus, FlatStorageStatus}; + use crate::flat::{FlatStorageReadyStatus, FlatStorageStatus}; use crate::test_utils::create_test_store; use crate::StorageError; use assert_matches::assert_matches; @@ -536,10 +539,9 @@ mod tests { // Create a chain with two forks. Set flat head to be at block 0. let chain = MockChain::chain_with_two_forks(5); let shard_uid = ShardUId::single_shard(); - let store = create_test_store(); + let store = create_test_store().flat_store(); let mut store_update = store.store_update(); - store_helper::set_flat_storage_status( - &mut store_update, + store_update.set_flat_storage_status( shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: chain.get_block(0) }), ); @@ -551,7 +553,7 @@ mod tests { prev_block_with_changes: None, }, }; - store_helper::set_delta(&mut store_update, shard_uid, &delta); + store_update.set_delta(shard_uid, &delta); } store_update.commit().unwrap(); @@ -584,7 +586,7 @@ mod tests { // Corrupt DB state for block 3 and try moving flat head to it. // Should result in `StorageInternalError` indicating that flat storage is broken. let mut store_update = store.store_update(); - store_helper::remove_delta(&mut store_update, shard_uid, chain.get_block_hash(3)); + store_update.remove_delta(shard_uid, chain.get_block_hash(3)); store_update.commit().unwrap(); assert_matches!( flat_storage.update_flat_head_impl(&chain.get_block_hash(3), true), @@ -615,10 +617,9 @@ mod tests { } }); let shard_uid = ShardUId::single_shard(); - let store = create_test_store(); + let store = create_test_store().flat_store(); let mut store_update = store.store_update(); - store_helper::set_flat_storage_status( - &mut store_update, + store_update.set_flat_storage_status( shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: chain.get_block(0) }), ); @@ -630,7 +631,7 @@ mod tests { prev_block_with_changes: None, }, }; - store_helper::set_delta(&mut store_update, shard_uid, &delta); + store_update.set_delta(shard_uid, &delta); } store_update.commit().unwrap(); @@ -658,10 +659,9 @@ mod tests { // Create a linear chain where some heights are skipped. let chain = MockChain::linear_chain_with_skips(5); let shard_uid = ShardUId::single_shard(); - let store = create_test_store(); + let store = create_test_store().flat_store(); let mut store_update = store.store_update(); - store_helper::set_flat_storage_status( - &mut store_update, + store_update.set_flat_storage_status( shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: chain.get_block(0) }), ); @@ -673,7 +673,7 @@ mod tests { prev_block_with_changes: None, }, }; - store_helper::set_delta(&mut store_update, shard_uid, &delta); + store_update.set_delta(shard_uid, &delta); } store_update.commit().unwrap(); @@ -697,19 +697,13 @@ mod tests { // Block i sets value for key &[1] to &[i]. let mut chain = MockChain::linear_chain(10); let shard_uid = ShardUId::single_shard(); - let store = create_test_store(); + let store = create_test_store().flat_store(); let mut store_update = store.store_update(); - store_helper::set_flat_storage_status( - &mut store_update, + store_update.set_flat_storage_status( shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: chain.get_block(0) }), ); - store_helper::set_flat_state_value( - &mut store_update, - shard_uid, - vec![1], - Some(FlatStateValue::value_ref(&[0])), - ); + store_update.set(shard_uid, vec![1], Some(FlatStateValue::value_ref(&[0]))); for i in 1..10 { let delta = FlatStateDelta { changes: FlatStateChanges::from([( @@ -721,7 +715,7 @@ mod tests { prev_block_with_changes: None, }, }; - store_helper::set_delta(&mut store_update, shard_uid, &delta); + store_update.set_delta(shard_uid, &delta); } store_update.commit().unwrap(); @@ -770,22 +764,13 @@ mod tests { assert_eq!(chunk_view0.get_value(&[2]).unwrap(), Some(FlatStateValue::value_ref(&[1]))); assert_eq!(chunk_view1.get_value(&[1]).unwrap(), Some(FlatStateValue::value_ref(&[4]))); assert_eq!(chunk_view1.get_value(&[2]).unwrap(), None); - assert_matches!( - store_helper::get_delta_changes(&store, shard_uid, chain.get_block_hash(5)).unwrap(), - Some(_) - ); - assert_matches!( - store_helper::get_delta_changes(&store, shard_uid, chain.get_block_hash(10)).unwrap(), - Some(_) - ); + assert_matches!(store.get_delta(shard_uid, chain.get_block_hash(5)).unwrap(), Some(_)); + assert_matches!(store.get_delta(shard_uid, chain.get_block_hash(10)).unwrap(), Some(_)); // 5. Move the flat head to block 5, verify that chunk_view0 still returns the same values // and chunk_view1 returns an error. Also check that DBCol::FlatState is updated correctly flat_storage.update_flat_head_impl(&chain.get_block_hash(5), true).unwrap(); - assert_eq!( - store_helper::get_flat_state_value(&store, shard_uid, &[1]).unwrap(), - Some(FlatStateValue::value_ref(&[5])) - ); + assert_eq!(store.get(shard_uid, &[1]).unwrap(), Some(FlatStateValue::value_ref(&[5]))); let blocks = flat_storage.get_blocks_to_head(&chain.get_block_hash(10)).unwrap(); assert_eq!(blocks.len(), 5); assert_eq!(chunk_view0.get_value(&[1]).unwrap(), None); @@ -794,31 +779,19 @@ mod tests { chunk_view1.get_value(&[1]), Err(StorageError::FlatStorageBlockNotSupported(_)) ); - assert_matches!( - store_helper::get_delta_changes(&store, shard_uid, chain.get_block_hash(5)).unwrap(), - None - ); - assert_matches!( - store_helper::get_delta_changes(&store, shard_uid, chain.get_block_hash(10)).unwrap(), - Some(_) - ); + assert_matches!(store.get_delta(shard_uid, chain.get_block_hash(5)).unwrap(), None); + assert_matches!(store.get_delta(shard_uid, chain.get_block_hash(10)).unwrap(), Some(_)); // 6. Move the flat head to block 10, verify that chunk_view0 still returns the same values // Also checks that DBCol::FlatState is updated correctly. flat_storage.update_flat_head_impl(&chain.get_block_hash(10), true).unwrap(); let blocks = flat_storage.get_blocks_to_head(&chain.get_block_hash(10)).unwrap(); assert_eq!(blocks.len(), 0); - assert_eq!(store_helper::get_flat_state_value(&store, shard_uid, &[1]).unwrap(), None); - assert_eq!( - store_helper::get_flat_state_value(&store, shard_uid, &[2]).unwrap(), - Some(FlatStateValue::value_ref(&[1])) - ); + assert_eq!(store.get(shard_uid, &[1]).unwrap(), None); + assert_eq!(store.get(shard_uid, &[2]).unwrap(), Some(FlatStateValue::value_ref(&[1]))); assert_eq!(chunk_view0.get_value(&[1]).unwrap(), None); assert_eq!(chunk_view0.get_value(&[2]).unwrap(), Some(FlatStateValue::value_ref(&[1]))); - assert_matches!( - store_helper::get_delta_changes(&store, shard_uid, chain.get_block_hash(10)).unwrap(), - None - ); + assert_matches!(store.get_delta(shard_uid, chain.get_block_hash(10)).unwrap(), None); } #[test] @@ -828,19 +801,13 @@ mod tests { let num_blocks = 15; let chain = MockChain::linear_chain(num_blocks); let shard_uid = ShardUId::single_shard(); - let store = create_test_store(); + let store = create_test_store().flat_store(); let mut store_update = store.store_update(); - store_helper::set_flat_storage_status( - &mut store_update, + store_update.set_flat_storage_status( shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: chain.get_block(0) }), ); - store_helper::set_flat_state_value( - &mut store_update, - shard_uid, - vec![1], - Some(FlatStateValue::value_ref(&[0])), - ); + store_update.set(shard_uid, vec![1], Some(FlatStateValue::value_ref(&[0]))); store_update.commit().unwrap(); for i in 1..num_blocks as BlockHeight { @@ -855,13 +822,13 @@ mod tests { // Simulates `Chain::save_flat_state_changes()`. let prev_block_with_changes = if changes.0.is_empty() { - store_helper::get_prev_block_with_changes( - &store, - shard_uid, - chain.get_block(i).hash, - chain.get_block(i).prev_hash, - ) - .unwrap() + store + .get_prev_block_with_changes( + shard_uid, + chain.get_block(i).hash, + chain.get_block(i).prev_hash, + ) + .unwrap() } else { None }; @@ -873,7 +840,7 @@ mod tests { }, }; tracing::info!(?i, ?delta); - store_helper::set_delta(&mut store_update, shard_uid, &delta); + store_update.set_delta(shard_uid, &delta); store_update.commit().unwrap(); } @@ -893,9 +860,7 @@ mod tests { // Don't check the first block because it may be a block with no changes. for i in 1..blocks.len() { let block_hash = blocks[i]; - let delta = store_helper::get_delta_changes(&store.clone(), shard_uid, block_hash) - .unwrap() - .unwrap(); + let delta = store.get_delta(shard_uid, block_hash).unwrap().unwrap(); assert!( !delta.0.is_empty(), "i: {i}, block_hash: {block_hash:?}, delta: {delta:?}" @@ -926,19 +891,13 @@ mod tests { let num_blocks = 10; let chain = MockChain::linear_chain(num_blocks); let shard_uid = ShardUId::single_shard(); - let store = create_test_store(); + let store = create_test_store().flat_store(); let mut store_update = store.store_update(); - store_helper::set_flat_storage_status( - &mut store_update, + store_update.set_flat_storage_status( shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: chain.get_block(0) }), ); - store_helper::set_flat_state_value( - &mut store_update, - shard_uid, - vec![1], - Some(FlatStateValue::value_ref(&[0])), - ); + store_update.set(shard_uid, vec![1], Some(FlatStateValue::value_ref(&[0]))); store_update.commit().unwrap(); for i in 1..num_blocks as BlockHeight { @@ -946,13 +905,13 @@ mod tests { // No changes. let changes = FlatStateChanges::default(); // Simulates `Chain::save_flat_state_changes()`. - let prev_block_with_changes = store_helper::get_prev_block_with_changes( - &store, - shard_uid, - chain.get_block(i).hash, - chain.get_block(i).prev_hash, - ) - .unwrap(); + let prev_block_with_changes = store + .get_prev_block_with_changes( + shard_uid, + chain.get_block(i).hash, + chain.get_block(i).prev_hash, + ) + .unwrap(); let delta = FlatStateDelta { changes, metadata: FlatStateDeltaMetadata { @@ -961,7 +920,7 @@ mod tests { }, }; tracing::info!(?i, ?delta); - store_helper::set_delta(&mut store_update, shard_uid, &delta); + store_update.set_delta(shard_uid, &delta); store_update.commit().unwrap(); } @@ -990,19 +949,13 @@ mod tests { let mut rng = thread_rng(); let chain = MockChain::linear_chain(num_blocks); let shard_uid = ShardUId::single_shard(); - let store = create_test_store(); + let store = create_test_store().flat_store(); let mut store_update = store.store_update(); - store_helper::set_flat_storage_status( - &mut store_update, + store_update.set_flat_storage_status( shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: chain.get_block(0) }), ); - store_helper::set_flat_state_value( - &mut store_update, - shard_uid, - vec![1], - Some(FlatStateValue::value_ref(&[0])), - ); + store_update.set(shard_uid, vec![1], Some(FlatStateValue::value_ref(&[0]))); store_update.commit().unwrap(); for i in 1..num_blocks as BlockHeight { @@ -1017,13 +970,13 @@ mod tests { // Simulates `Chain::save_flat_state_changes()`. let prev_block_with_changes = if changes.0.is_empty() { - store_helper::get_prev_block_with_changes( - &store, - shard_uid, - chain.get_block(i).hash, - chain.get_block(i).prev_hash, - ) - .unwrap() + store + .get_prev_block_with_changes( + shard_uid, + chain.get_block(i).hash, + chain.get_block(i).prev_hash, + ) + .unwrap() } else { None }; @@ -1035,7 +988,7 @@ mod tests { }, }; tracing::info!(?i, ?delta); - store_helper::set_delta(&mut store_update, shard_uid, &delta); + store_update.set_delta(shard_uid, &delta); store_update.commit().unwrap(); } @@ -1061,9 +1014,7 @@ mod tests { // Don't check the first block because it may be a block with no changes. for i in 1..blocks.len() { let block_hash = blocks[i]; - let delta = store_helper::get_delta_changes(&store.clone(), shard_uid, block_hash) - .unwrap() - .unwrap(); + let delta = store.get_delta(shard_uid, block_hash).unwrap().unwrap(); assert!( !delta.0.is_empty(), "i: {i}, block_hash: {block_hash:?}, delta: {delta:?}" @@ -1074,9 +1025,7 @@ mod tests { let flat_head_height = hashes.get(&flat_head_hash).unwrap(); let flat_head_lag = i - flat_head_height; - let delta = store_helper::get_delta_changes(&store.clone(), shard_uid, block_hash) - .unwrap() - .unwrap(); + let delta = store.get_delta(shard_uid, block_hash).unwrap().unwrap(); let has_changes = !delta.0.is_empty(); tracing::info!(?i, has_changes, ?flat_head_lag); max_lag = max_lag.max(Some(flat_head_lag)); @@ -1095,19 +1044,13 @@ mod tests { tracing::info!("Case 1"); let num_blocks = 10; let chain = MockChain::linear_chain(num_blocks); - let store = create_test_store(); + let store = create_test_store().flat_store(); let mut store_update = store.store_update(); - store_helper::set_flat_storage_status( - &mut store_update, + store_update.set_flat_storage_status( shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: chain.get_block(0) }), ); - store_helper::set_flat_state_value( - &mut store_update, - shard_uid, - vec![1], - Some(FlatStateValue::value_ref(&[0])), - ); + store_update.set(shard_uid, vec![1], Some(FlatStateValue::value_ref(&[0]))); store_update.commit().unwrap(); for i in 1..num_blocks as BlockHeight { @@ -1125,7 +1068,7 @@ mod tests { }, }; tracing::info!(?i, ?delta); - store_helper::set_delta(&mut store_update, shard_uid, &delta); + store_update.set_delta(shard_uid, &delta); store_update.commit().unwrap(); } @@ -1151,19 +1094,13 @@ mod tests { tracing::info!("Case 2"); let num_blocks = 20; let chain = MockChain::linear_chain(num_blocks); - let store = create_test_store(); + let store = create_test_store().flat_store(); let mut store_update = store.store_update(); - store_helper::set_flat_storage_status( - &mut store_update, + store_update.set_flat_storage_status( shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: chain.get_block(0) }), ); - store_helper::set_flat_state_value( - &mut store_update, - shard_uid, - vec![1], - Some(FlatStateValue::value_ref(&[0])), - ); + store_update.set(shard_uid, vec![1], Some(FlatStateValue::value_ref(&[0]))); store_update.commit().unwrap(); for i in 1..num_blocks as BlockHeight { @@ -1196,7 +1133,7 @@ mod tests { }, }; tracing::info!(?i, ?delta); - store_helper::set_delta(&mut store_update, shard_uid, &delta); + store_update.set_delta(shard_uid, &delta); store_update.commit().unwrap(); } @@ -1242,19 +1179,13 @@ mod tests { tracing::info!("Case 3"); let num_blocks = 20; let chain = MockChain::linear_chain(num_blocks); - let store = create_test_store(); + let store = create_test_store().flat_store(); let mut store_update = store.store_update(); - store_helper::set_flat_storage_status( - &mut store_update, + store_update.set_flat_storage_status( shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: chain.get_block(0) }), ); - store_helper::set_flat_state_value( - &mut store_update, - shard_uid, - vec![1], - Some(FlatStateValue::value_ref(&[0])), - ); + store_update.set(shard_uid, vec![1], Some(FlatStateValue::value_ref(&[0]))); store_update.commit().unwrap(); for i in 1..num_blocks as BlockHeight { @@ -1287,7 +1218,7 @@ mod tests { }, }; tracing::info!(?i, ?delta); - store_helper::set_delta(&mut store_update, shard_uid, &delta); + store_update.set_delta(shard_uid, &delta); store_update.commit().unwrap(); } diff --git a/core/store/src/flat/store_helper.rs b/core/store/src/flat/store_helper.rs deleted file mode 100644 index 472c7b0816d..00000000000 --- a/core/store/src/flat/store_helper.rs +++ /dev/null @@ -1,285 +0,0 @@ -//! This file contains helper functions for accessing flat storage data in DB -//! TODO(#8577): remove this file and move functions to the corresponding structs - -use super::delta::{FlatStateDelta, FlatStateDeltaMetadata}; -use super::types::{FlatStateIterator, FlatStorageResult, FlatStorageStatus}; -use crate::flat::delta::{BlockWithChangesInfo, FlatStateChanges, KeyForFlatStateDelta}; -use crate::flat::types::FlatStorageError; -use crate::flat::FlatStorageReadyStatus; -use crate::{DBCol, Store, StoreUpdate}; -use borsh::BorshDeserialize; -use near_primitives::hash::CryptoHash; -use near_primitives::shard_layout::ShardUId; -use near_primitives::state::FlatStateValue; -use std::io; - -pub fn get_delta_changes( - store: &Store, - shard_uid: ShardUId, - block_hash: CryptoHash, -) -> FlatStorageResult> { - let key = KeyForFlatStateDelta { shard_uid, block_hash }; - store.get_ser::(DBCol::FlatStateChanges, &key.to_bytes()).map_err(|err| { - FlatStorageError::StorageInternalError(format!( - "failed to read delta changes for {key:?}: {err}" - )) - }) -} - -pub fn get_all_deltas_metadata( - store: &Store, - shard_uid: ShardUId, -) -> FlatStorageResult> { - store - .iter_prefix_ser(DBCol::FlatStateDeltaMetadata, &shard_uid.to_bytes()) - .map(|res| { - res.map(|(_, value)| value).map_err(|err| { - FlatStorageError::StorageInternalError(format!( - "failed to read delta metadata: {err}" - )) - }) - }) - .collect() -} - -/// Retrieves a row of `FlatStateDeltaMetadata` for the given key. -fn get_delta_metadata( - store: &Store, - shard_uid: ShardUId, - block_hash: CryptoHash, -) -> FlatStorageResult> { - let key = KeyForFlatStateDelta { shard_uid, block_hash }.to_bytes(); - store.get_ser(DBCol::FlatStateDeltaMetadata, &key).map_err(|err| { - FlatStorageError::StorageInternalError(format!( - "failed to read delta metadata for {key:?}: {err}" - )) - }) -} - -pub fn get_prev_block_with_changes( - store: &Store, - shard_uid: ShardUId, - block_hash: CryptoHash, - prev_hash: CryptoHash, -) -> FlatStorageResult> { - let prev_delta_metadata = get_delta_metadata(store, shard_uid, prev_hash)?; - let prev_block_with_changes = match prev_delta_metadata { - None => { - // DeltaMetadata not found, which means the prev block is the flat head. - let flat_storage_status = get_flat_storage_status(store, shard_uid)?; - match flat_storage_status { - FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head }) => { - if flat_head.hash == prev_hash { - Some(BlockWithChangesInfo { hash: prev_hash, height: flat_head.height }) - } else { - tracing::error!(target: "store", ?block_hash, ?prev_hash, "Missing delta metadata"); - None - } - } - // Don't do any performance optimizations while flat storage is not ready. - _ => None, - } - } - Some(metadata) => { - // If the prev block contains `prev_block_with_changes`, then use that value. - // Otherwise reference the prev block. - Some(metadata.prev_block_with_changes.unwrap_or(BlockWithChangesInfo { - hash: metadata.block.hash, - height: metadata.block.height, - })) - } - }; - Ok(prev_block_with_changes) -} - -pub fn set_delta(store_update: &mut StoreUpdate, shard_uid: ShardUId, delta: &FlatStateDelta) { - let key = KeyForFlatStateDelta { shard_uid, block_hash: delta.metadata.block.hash }.to_bytes(); - store_update - .set_ser(DBCol::FlatStateChanges, &key, &delta.changes) - .expect("Borsh should not have failed here"); - store_update - .set_ser(DBCol::FlatStateDeltaMetadata, &key, &delta.metadata) - .expect("Borsh should not have failed here"); -} - -pub fn remove_delta(store_update: &mut StoreUpdate, shard_uid: ShardUId, block_hash: CryptoHash) { - let key = KeyForFlatStateDelta { shard_uid, block_hash }.to_bytes(); - store_update.delete(DBCol::FlatStateChanges, &key); - store_update.delete(DBCol::FlatStateDeltaMetadata, &key); -} - -fn remove_range_by_shard_uid(store_update: &mut StoreUpdate, shard_uid: ShardUId, col: DBCol) { - let key_from = shard_uid.to_bytes(); - let key_to = ShardUId::next_shard_prefix(&key_from); - store_update.delete_range(col, &key_from, &key_to); -} - -pub fn remove_all_deltas(store_update: &mut StoreUpdate, shard_uid: ShardUId) { - remove_range_by_shard_uid(store_update, shard_uid, DBCol::FlatStateChanges); - remove_range_by_shard_uid(store_update, shard_uid, DBCol::FlatStateDeltaMetadata); -} - -pub fn remove_all_flat_state_values(store_update: &mut StoreUpdate, shard_uid: ShardUId) { - remove_range_by_shard_uid(store_update, shard_uid, DBCol::FlatState); -} - -pub fn remove_all_state_values(store_update: &mut StoreUpdate, shard_uid: ShardUId) { - remove_range_by_shard_uid(store_update, shard_uid, DBCol::State); -} - -pub fn encode_flat_state_db_key(shard_uid: ShardUId, key: &[u8]) -> Vec { - let mut buffer = vec![]; - buffer.extend_from_slice(&shard_uid.to_bytes()); - buffer.extend_from_slice(key); - buffer -} - -pub fn decode_flat_state_db_key(key: &[u8]) -> io::Result<(ShardUId, Vec)> { - let (shard_uid_bytes, trie_key) = key.split_at_checked(8).ok_or_else(|| { - io::Error::other(format!("expected FlatState key length to be at least 8: {key:?}")) - })?; - let shard_uid = shard_uid_bytes.try_into().map_err(|err| { - io::Error::other(format!("failed to decode shard_uid as part of FlatState key: {err}")) - })?; - Ok((shard_uid, trie_key.to_vec())) -} - -pub fn get_flat_state_value( - store: &Store, - shard_uid: ShardUId, - key: &[u8], -) -> FlatStorageResult> { - let db_key = encode_flat_state_db_key(shard_uid, key); - store.get_ser(DBCol::FlatState, &db_key).map_err(|err| { - FlatStorageError::StorageInternalError(format!("failed to read FlatState value: {err}")) - }) -} - -// TODO(#8577): make pub(crate) once flat storage creator is moved inside `flat` module. -pub fn set_flat_state_value( - store_update: &mut StoreUpdate, - shard_uid: ShardUId, - key: Vec, - value: Option, -) { - let db_key = encode_flat_state_db_key(shard_uid, &key); - match value { - Some(value) => store_update - .set_ser(DBCol::FlatState, &db_key, &value) - .expect("Borsh should not have failed here"), - None => store_update.delete(DBCol::FlatState, &db_key), - } -} - -pub fn get_flat_storage_status( - store: &Store, - shard_uid: ShardUId, -) -> FlatStorageResult { - store - .get_ser(DBCol::FlatStorageStatus, &shard_uid.to_bytes()) - .map(|status| status.unwrap_or(FlatStorageStatus::Empty)) - .map_err(|err| { - FlatStorageError::StorageInternalError(format!( - "failed to read flat storage status: {err}" - )) - }) -} - -pub fn set_flat_storage_status( - store_update: &mut StoreUpdate, - shard_uid: ShardUId, - status: FlatStorageStatus, -) { - store_update - .set_ser(DBCol::FlatStorageStatus, &shard_uid.to_bytes(), &status) - .expect("Borsh should not have failed here") -} - -/// Returns iterator over flat storage entries for a given shard and range of -/// state keys. `None` means that there is no bound in respective direction. -/// It reads data only from `FlatState` column which represents the state at -/// flat storage head. Reads only committed changes. -pub fn iter_flat_state_entries<'a>( - shard_uid: ShardUId, - store: &'a Store, - from: Option<&[u8]>, - to: Option<&[u8]>, -) -> FlatStateIterator<'a> { - // If left direction is unbounded, encoded `shard_uid` serves as the - // smallest possible key in DB for the shard. - let db_key_from = match from { - Some(from) => encode_flat_state_db_key(shard_uid, from), - None => shard_uid.to_bytes().to_vec(), - }; - // If right direction is unbounded, `ShardUId::next_shard_prefix` serves as - // the key which is strictly bigger than all keys in DB for this shard and - // still doesn't include keys from other shards. - let db_key_to = match to { - Some(to) => encode_flat_state_db_key(shard_uid, to), - None => ShardUId::next_shard_prefix(&shard_uid.to_bytes()).to_vec(), - }; - let iter = - store.iter_range(DBCol::FlatState, Some(&db_key_from), Some(&db_key_to)).map(|result| { - match result { - Ok((key, value)) => Ok(( - decode_flat_state_db_key(&key) - .map_err(|err| { - FlatStorageError::StorageInternalError(format!( - "invalid FlatState key format: {err}" - )) - })? - .1, - FlatStateValue::try_from_slice(&value).map_err(|err| { - FlatStorageError::StorageInternalError(format!( - "invalid FlatState value format: {err}" - )) - })?, - )), - Err(err) => Err(FlatStorageError::StorageInternalError(format!( - "FlatState iterator error: {err}" - ))), - } - }); - Box::new(iter) -} - -#[cfg(test)] -mod tests { - use crate::flat::store_helper::set_flat_state_value; - use crate::test_utils::create_test_store; - use near_primitives::shard_layout::ShardUId; - use near_primitives::state::FlatStateValue; - - #[test] - fn iter_flat_state_entries() { - // Setup shards and store - let store = create_test_store(); - let shard_uids = [0, 1, 2].map(|id| ShardUId { version: 0, shard_id: id }); - - for (i, shard_uid) in shard_uids.iter().enumerate() { - let mut store_update = store.store_update(); - let key: Vec = vec![0, 1, i as u8]; - let val: Vec = vec![0, 1, 2, i as u8]; - - // Add value to FlatState - set_flat_state_value( - &mut store_update, - *shard_uid, - key.clone(), - Some(FlatStateValue::inlined(&val)), - ); - - store_update.commit().unwrap(); - } - - for (i, shard_uid) in shard_uids.iter().enumerate() { - let entries: Vec<_> = - super::iter_flat_state_entries(*shard_uid, &store, None, None).collect(); - assert_eq!(entries.len(), 1); - let key: Vec = vec![0, 1, i as u8]; - let val: Vec = vec![0, 1, 2, i as u8]; - - assert_eq!(entries, vec![Ok((key, FlatStateValue::inlined(&val)))]); - } - } -} diff --git a/core/store/src/genesis/initialization.rs b/core/store/src/genesis/initialization.rs index ed35943ce95..fcf77179a74 100644 --- a/core/store/src/genesis/initialization.rs +++ b/core/store/src/genesis/initialization.rs @@ -16,9 +16,9 @@ use near_primitives::{ use tracing::{error, info, warn}; use crate::{ - flat::FlatStorageManager, genesis::GenesisStateApplier, get_genesis_hash, - get_genesis_state_roots, set_genesis_hash, set_genesis_state_roots, ShardTries, - StateSnapshotConfig, Store, TrieConfig, + adapter::StoreAdapter, flat::FlatStorageManager, genesis::GenesisStateApplier, + get_genesis_hash, get_genesis_state_roots, set_genesis_hash, set_genesis_state_roots, + ShardTries, StateSnapshotConfig, Store, TrieConfig, }; const STATE_DUMP_FILE: &str = "state_dump"; @@ -132,7 +132,7 @@ fn genesis_state_from_genesis( store.clone(), TrieConfig::default(), &shard_uids, - FlatStorageManager::new(store), + FlatStorageManager::new(store.flat_store()), StateSnapshotConfig::default(), ); diff --git a/core/store/src/genesis/state_applier.rs b/core/store/src/genesis/state_applier.rs index e0c4a187883..349c19b432c 100644 --- a/core/store/src/genesis/state_applier.rs +++ b/core/store/src/genesis/state_applier.rs @@ -1,3 +1,4 @@ +use crate::adapter::StoreUpdateAdapter; use crate::flat::FlatStateChanges; use crate::{ get_account, has_received_data, set, set_access_key, set_account, set_code, @@ -143,7 +144,7 @@ impl<'a> AutoFlushingTrieUpdate<'a> { let mut store_update = self.tries.store_update(); *state_root = self.tries.apply_all(&trie_changes, self.shard_uid, &mut store_update); FlatStateChanges::from_state_changes(&state_changes) - .apply_to_flat_state(&mut store_update, self.shard_uid); + .apply_to_flat_state(&mut store_update.flat_store_update(), self.shard_uid); store_update.commit().expect("Store update failed on genesis initialization"); *state_update = Some(self.tries.new_trie_update(self.shard_uid, *state_root)); *changes = 0; diff --git a/core/store/src/lib.rs b/core/store/src/lib.rs index d03e710d8c6..cec9740cfdd 100644 --- a/core/store/src/lib.rs +++ b/core/store/src/lib.rs @@ -12,6 +12,7 @@ pub use crate::trie::{ TrieChanges, TrieConfig, TrieDBStorage, TrieStorage, WrappedTrieChanges, STATE_SNAPSHOT_COLUMNS, }; +use adapter::{StoreAdapter, StoreUpdateAdapter}; use borsh::{BorshDeserialize, BorshSerialize}; pub use columns::DBCol; use db::{SplitDB, GENESIS_CONGESTION_INFO_KEY}; @@ -43,6 +44,7 @@ use std::sync::LazyLock; use std::{fmt, io}; use strum; +pub mod adapter; pub mod cold_storage; mod columns; pub mod config; @@ -112,6 +114,12 @@ pub struct Store { storage: Arc, } +impl StoreAdapter for Store { + fn store(&self) -> Store { + self.clone() + } +} + impl NodeStorage { /// Initialises a new opener with given home directory and hot and cold /// store config. @@ -450,6 +458,12 @@ pub struct StoreUpdate { storage: Arc, } +impl StoreUpdateAdapter for StoreUpdate { + fn store_update(&mut self) -> &mut StoreUpdate { + self + } +} + impl StoreUpdate { const ONE: std::num::NonZeroU32 = match std::num::NonZeroU32::new(1) { Some(num) => num, diff --git a/core/store/src/test_utils.rs b/core/store/src/test_utils.rs index 1c66a7b29a2..577294c66c5 100644 --- a/core/store/src/test_utils.rs +++ b/core/store/src/test_utils.rs @@ -1,7 +1,6 @@ +use crate::adapter::{StoreAdapter, StoreUpdateAdapter}; use crate::db::TestDB; -use crate::flat::{ - store_helper, BlockInfo, FlatStorageManager, FlatStorageReadyStatus, FlatStorageStatus, -}; +use crate::flat::{BlockInfo, FlatStorageManager, FlatStorageReadyStatus, FlatStorageStatus}; use crate::metadata::{DbKind, DbVersion, DB_VERSION}; use crate::{ get, get_delayed_receipt_indices, get_promise_yield_indices, DBCol, NodeStorage, ShardTries, @@ -123,7 +122,7 @@ impl TestTriesBuilder { let shard_uids = (0..self.num_shards) .map(|shard_id| ShardUId { shard_id: shard_id as u32, version: self.shard_version }) .collect::>(); - let flat_storage_manager = FlatStorageManager::new(store.clone()); + let flat_storage_manager = FlatStorageManager::new(store.flat_store()); let tries = ShardTries::new( store.clone(), TrieConfig { @@ -141,8 +140,7 @@ impl TestTriesBuilder { version: self.shard_version, shard_id: shard_id.try_into().unwrap(), }; - store_helper::set_flat_storage_status( - &mut store_update, + store_update.flat_store_update().set_flat_storage_status( shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: BlockInfo::genesis(CryptoHash::default(), 0), @@ -220,17 +218,15 @@ pub fn test_populate_flat_storage( prev_block_hash: &CryptoHash, changes: &Vec<(Vec, Option>)>, ) { - let mut store_update = tries.store_update(); - store_helper::set_flat_storage_status( - &mut store_update, + let mut store_update = tries.get_store().flat_store().store_update(); + store_update.set_flat_storage_status( shard_uid, crate::flat::FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: BlockInfo { hash: *block_hash, prev_hash: *prev_block_hash, height: 1 }, }), ); for (key, value) in changes { - store_helper::set_flat_state_value( - &mut store_update, + store_update.set( shard_uid, key.clone(), value.as_ref().map(|value| FlatStateValue::on_disk(value)), diff --git a/core/store/src/trie/from_flat.rs b/core/store/src/trie/from_flat.rs index 71cf52684ea..b476ee25492 100644 --- a/core/store/src/trie/from_flat.rs +++ b/core/store/src/trie/from_flat.rs @@ -1,4 +1,5 @@ -use crate::flat::{store_helper, FlatStorageError, FlatStorageManager}; +use crate::adapter::StoreAdapter; +use crate::flat::{FlatStorageError, FlatStorageManager}; use crate::{ShardTries, StateSnapshotConfig, Store, Trie, TrieConfig, TrieDBStorage, TrieStorage}; use near_primitives::{shard_layout::ShardUId, state::FlatStateValue}; use std::time::Instant; @@ -24,15 +25,15 @@ pub fn construct_trie_from_flat(store: Store, write_store: Store, shard_uid: Sha (key, value) }; - let mut iter = store_helper::iter_flat_state_entries(shard_uid, &store, None, None) - .map(flat_state_to_trie_kv); + let store = store.flat_store(); + let mut iter = store.iter(shard_uid).map(flat_state_to_trie_kv); // new ShardTries for write storage location let tries = ShardTries::new( write_store.clone(), TrieConfig::default(), &[shard_uid], - FlatStorageManager::new(write_store), + FlatStorageManager::new(write_store.flat_store()), StateSnapshotConfig::default(), ); let mut trie_root = Trie::EMPTY_ROOT; diff --git a/core/store/src/trie/mem/loading.rs b/core/store/src/trie/mem/loading.rs index b98e3b15d5c..727fef5a6de 100644 --- a/core/store/src/trie/mem/loading.rs +++ b/core/store/src/trie/mem/loading.rs @@ -1,9 +1,8 @@ use super::arena::single_thread::STArena; use super::mem_tries::MemTries; use super::node::MemTrieNodeId; -use crate::flat::store_helper::{ - decode_flat_state_db_key, get_all_deltas_metadata, get_delta_changes, get_flat_storage_status, -}; +use crate::adapter::flat_store::decode_flat_state_db_key; +use crate::adapter::StoreAdapter; use crate::flat::{FlatStorageError, FlatStorageStatus}; use crate::trie::mem::arena::Arena; use crate::trie::mem::construction::TrieConstructor; @@ -129,7 +128,8 @@ pub fn load_trie_from_flat_state_and_delta( parallelize: bool, ) -> Result { debug!(target: "memtrie", %shard_uid, "Loading base trie from flat state..."); - let flat_head = match get_flat_storage_status(&store, shard_uid)? { + let flat_store = store.flat_store(); + let flat_head = match flat_store.get_flat_storage_status(shard_uid)? { FlatStorageStatus::Ready(status) => status.flat_head, other => { return Err(StorageError::MemTrieLoadingError(format!( @@ -152,13 +152,13 @@ pub fn load_trie_from_flat_state_and_delta( // We load the deltas in order of height, so that we always have the previous state root // already loaded. let mut sorted_deltas: BTreeSet<(BlockHeight, CryptoHash, CryptoHash)> = Default::default(); - for delta in get_all_deltas_metadata(&store, shard_uid).unwrap() { + for delta in flat_store.get_all_deltas_metadata(shard_uid).unwrap() { sorted_deltas.insert((delta.block.height, delta.block.hash, delta.block.prev_hash)); } debug!(target: "memtrie", %shard_uid, "{} deltas to apply", sorted_deltas.len()); for (height, hash, prev_hash) in sorted_deltas.into_iter() { - let delta = get_delta_changes(&store, shard_uid, hash).unwrap(); + let delta = flat_store.get_delta(shard_uid, hash).unwrap(); if let Some(changes) = delta { let old_state_root = get_state_root(store, prev_hash, shard_uid)?; let new_state_root = get_state_root(store, hash, shard_uid)?; @@ -187,8 +187,9 @@ pub fn load_trie_from_flat_state_and_delta( #[cfg(test)] mod tests { use super::load_trie_from_flat_state_and_delta; + use crate::adapter::StoreAdapter; use crate::flat::test_utils::MockChain; - use crate::flat::{store_helper, BlockInfo, FlatStorageReadyStatus, FlatStorageStatus}; + use crate::flat::{BlockInfo, FlatStorageReadyStatus, FlatStorageStatus}; use crate::test_utils::{ create_test_store, simplify_changes, test_populate_flat_storage, test_populate_trie, TestTriesBuilder, @@ -392,18 +393,12 @@ mod tests { let shard_uid = ShardUId { version: 1, shard_id: 1 }; // Populate the initial flat storage state at block 0. - let mut store_update = shard_tries.store_update(); - store_helper::set_flat_storage_status( - &mut store_update, + let mut store_update = shard_tries.get_store().flat_store().store_update(); + store_update.set_flat_storage_status( shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: chain.get_block(0) }), ); - store_helper::set_flat_state_value( - &mut store_update, - shard_uid, - test_key.to_vec(), - Some(FlatStateValue::inlined(&test_val0)), - ); + store_update.set(shard_uid, test_key.to_vec(), Some(FlatStateValue::inlined(&test_val0))); store_update.commit().unwrap(); // Populate the initial trie at block 0 too. @@ -511,7 +506,8 @@ mod tests { shard_uid, &state_changes, ) - .unwrap(), + .unwrap() + .into(), ); store_update.commit().unwrap(); diff --git a/core/store/src/trie/resharding_v2.rs b/core/store/src/trie/resharding_v2.rs index e0455893768..d02055001cb 100644 --- a/core/store/src/trie/resharding_v2.rs +++ b/core/store/src/trie/resharding_v2.rs @@ -1,3 +1,4 @@ +use crate::adapter::StoreUpdateAdapter; use crate::flat::FlatStateChanges; use crate::{ get, get_delayed_receipt_indices, get_promise_yield_indices, set, ShardTries, StoreUpdate, @@ -72,7 +73,7 @@ impl ShardTries { let mut store_update = self.store_update(); for (shard_uid, changes) in changes_by_shard { FlatStateChanges::from_raw_key_value(&changes) - .apply_to_flat_state(&mut store_update, shard_uid); + .apply_to_flat_state(&mut store_update.flat_store_update(), shard_uid); // Here we assume that state_roots contains shard_uid, the caller of this method will guarantee that. let trie_changes = self.get_trie_for_shard(shard_uid, state_roots[&shard_uid]).update(changes)?; @@ -136,7 +137,7 @@ impl ShardTries { let (_, trie_changes, state_changes) = update.finalize()?; let state_root = self.apply_all(&trie_changes, shard_uid, &mut store_update); FlatStateChanges::from_state_changes(&state_changes) - .apply_to_flat_state(&mut store_update, shard_uid); + .apply_to_flat_state(&mut store_update.flat_store_update(), shard_uid); new_state_roots.insert(shard_uid, state_root); } Ok((store_update, new_state_roots)) diff --git a/core/store/src/trie/shard_tries.rs b/core/store/src/trie/shard_tries.rs index 8b83b6d02ff..f6990b191ea 100644 --- a/core/store/src/trie/shard_tries.rs +++ b/core/store/src/trie/shard_tries.rs @@ -1,7 +1,6 @@ use super::mem::mem_tries::MemTries; use super::state_snapshot::{StateSnapshot, StateSnapshotConfig}; use super::TrieRefcountSubtraction; -use crate::flat::store_helper::remove_all_state_values; use crate::flat::{FlatStorageManager, FlatStorageStatus}; use crate::trie::config::TrieConfig; use crate::trie::mem::loading::load_trie_from_flat_state_and_delta; @@ -404,7 +403,13 @@ impl ShardTries { // Clear both caches and remove state values from store let _cache = self.0.caches.lock().expect(POISONED_LOCK_ERR).remove(&shard_uid); let _view_cache = self.0.view_caches.lock().expect(POISONED_LOCK_ERR).remove(&shard_uid); - remove_all_state_values(store_update, shard_uid); + Self::remove_all_state_values(store_update, shard_uid); + } + + fn remove_all_state_values(store_update: &mut StoreUpdate, shard_uid: ShardUId) { + let key_from = shard_uid.to_bytes(); + let key_to = ShardUId::next_shard_prefix(&key_from); + store_update.delete_range(DBCol::State, &key_from, &key_to); } /// Retains in-memory tries for given shards, i.e. unload tries from memory for shards that are NOT @@ -743,6 +748,7 @@ impl KeyForStateChanges { #[cfg(test)] mod test { + use crate::adapter::StoreAdapter; use crate::{ config::TrieCacheConfig, test_utils::create_test_store, trie::DEFAULT_SHARD_CACHE_TOTAL_SIZE_LIMIT, TrieConfig, @@ -768,7 +774,7 @@ mod test { store.clone(), trie_config, &shard_uids, - FlatStorageManager::new(store), + FlatStorageManager::new(store.flat_store()), StateSnapshotConfig::default(), ) } @@ -886,7 +892,7 @@ mod test { store.clone(), trie_config, &shard_uids, - FlatStorageManager::new(store), + FlatStorageManager::new(store.flat_store()), StateSnapshotConfig::default(), ); diff --git a/core/store/src/trie/state_parts.rs b/core/store/src/trie/state_parts.rs index 7a0ee49ad20..23c89b19b6f 100644 --- a/core/store/src/trie/state_parts.rs +++ b/core/store/src/trie/state_parts.rs @@ -119,8 +119,7 @@ impl Trie { Some(NibbleSlice::nibbles_to_bytes(&nibbles_end)) }; - Ok(flat_storage_chunk_view - .iter_flat_state_entries(key_begin.as_deref(), key_end.as_deref())) + Ok(flat_storage_chunk_view.iter_range(key_begin.as_deref(), key_end.as_deref())) } /// Determines the boundaries of a state part by accessing the Trie (i.e. State column). @@ -518,6 +517,7 @@ mod tests { use near_primitives::hash::{hash, CryptoHash}; + use crate::adapter::StoreUpdateAdapter; use crate::test_utils::{gen_changes, test_populate_trie, TestTriesBuilder}; use crate::trie::iterator::CrumbStatus; use crate::trie::{ @@ -1205,7 +1205,7 @@ mod tests { state_items.into_iter().map(|(k, v)| (k, Some(FlatStateValue::inlined(&v)))); let delta = FlatStateChanges::from(changes_for_delta); let mut store_update = tries.store_update(); - delta.apply_to_flat_state(&mut store_update, shard_uid); + delta.apply_to_flat_state(&mut store_update.flat_store_update(), shard_uid); store_update.commit().unwrap(); let (partial_state, nibbles_begin, nibbles_end) = @@ -1253,7 +1253,7 @@ mod tests { // is invalid. let mut store_update = tries.store_update(); let delta = FlatStateChanges::from(vec![(b"ba".to_vec(), None)]); - delta.apply_to_flat_state(&mut store_update, shard_uid); + delta.apply_to_flat_state(&mut store_update.flat_store_update(), shard_uid); store_update.commit().unwrap(); assert_matches!( diff --git a/core/store/src/trie/state_snapshot.rs b/core/store/src/trie/state_snapshot.rs index 43e2c1f75d9..d1874af6724 100644 --- a/core/store/src/trie/state_snapshot.rs +++ b/core/store/src/trie/state_snapshot.rs @@ -1,3 +1,4 @@ +use crate::adapter::StoreAdapter; use crate::config::StateSnapshotType; use crate::db::STATE_SNAPSHOT_KEY; use crate::flat::{FlatStorageManager, FlatStorageStatus}; @@ -218,7 +219,7 @@ impl ShardTries { // It is fine to create a separate FlatStorageManager, because // it is used only for reading flat storage in the snapshot a // doesn't introduce memory overhead. - let flat_storage_manager = FlatStorageManager::new(store.clone()); + let flat_storage_manager = FlatStorageManager::new(store.flat_store()); *state_snapshot_lock = Some(StateSnapshot::new( store, prev_block_hash, @@ -361,7 +362,7 @@ impl ShardTries { let opener = NodeStorage::opener(&snapshot_path, false, &store_config, None); let storage = opener.open_in_mode(Mode::ReadOnly)?; let store = storage.get_hot_store(); - let flat_storage_manager = FlatStorageManager::new(store.clone()); + let flat_storage_manager = FlatStorageManager::new(store.flat_store()); let shard_uids = get_shard_uids_fn(snapshot_hash)?; let mut guard = self.state_snapshot().write().unwrap(); diff --git a/genesis-tools/genesis-populate/src/lib.rs b/genesis-tools/genesis-populate/src/lib.rs index 72b1bf3e3aa..905dc8fa469 100644 --- a/genesis-tools/genesis-populate/src/lib.rs +++ b/genesis-tools/genesis-populate/src/lib.rs @@ -21,6 +21,7 @@ use near_primitives::types::chunk_extra::ChunkExtra; use near_primitives::types::{AccountId, Balance, EpochId, ShardId, StateChangeCause, StateRoot}; use near_primitives::utils::to_timestamp; use near_primitives::version::ProtocolFeature; +use near_store::adapter::StoreUpdateAdapter; use near_store::genesis::{compute_storage_usage, initialize_genesis_state}; use near_store::{ get_account, get_genesis_state_roots, set_access_key, set_account, set_code, Store, TrieUpdate, @@ -203,7 +204,7 @@ impl GenesisBuilder { let mut store_update = tries.store_update(); let root = tries.apply_all(&trie_changes, shard_uid, &mut store_update); near_store::flat::FlatStateChanges::from_state_changes(&state_changes) - .apply_to_flat_state(&mut store_update, shard_uid); + .apply_to_flat_state(&mut store_update.flat_store_update(), shard_uid); store_update.commit()?; self.roots.insert(shard_idx, root); diff --git a/integration-tests/src/tests/client/flat_storage.rs b/integration-tests/src/tests/client/flat_storage.rs index fd3d30e114a..1e63f0e357c 100644 --- a/integration-tests/src/tests/client/flat_storage.rs +++ b/integration-tests/src/tests/client/flat_storage.rs @@ -13,9 +13,10 @@ use near_primitives::transaction::SignedTransaction; use near_primitives::trie_key::TrieKey; use near_primitives::types::AccountId; use near_primitives_core::types::BlockHeight; +use near_store::adapter::StoreAdapter; use near_store::flat::{ - store_helper, FetchingStateStatus, FlatStorageCreationStatus, FlatStorageManager, - FlatStorageReadyStatus, FlatStorageStatus, NUM_PARTS_IN_ONE_STEP, + FetchingStateStatus, FlatStorageCreationStatus, FlatStorageManager, FlatStorageReadyStatus, + FlatStorageStatus, NUM_PARTS_IN_ONE_STEP, }; use near_store::test_utils::create_test_store; use near_store::trie::TrieNodesCount; @@ -46,16 +47,16 @@ fn wait_for_flat_storage_creation( shard_uid: ShardUId, produce_blocks: bool, ) -> BlockHeight { - let store = env.clients[0].runtime_adapter.store().clone(); + let store = env.clients[0].runtime_adapter.store().flat_store(); let mut next_height = start_height; - let mut prev_status = store_helper::get_flat_storage_status(&store, shard_uid).unwrap(); + let mut prev_status = store.get_flat_storage_status(shard_uid).unwrap(); while next_height < start_height + CREATION_TIMEOUT { if produce_blocks { env.produce_block(0, next_height); } env.clients[0].run_flat_storage_creation_step().unwrap(); - let status = store_helper::get_flat_storage_status(&store, shard_uid).unwrap(); + let status = store.get_flat_storage_status(shard_uid).unwrap(); // Check validity of state transition for flat storage creation. match &prev_status { FlatStorageStatus::Empty => assert_matches!( @@ -109,8 +110,7 @@ fn wait_for_flat_storage_creation( // We don't expect any forks in the chain after flat storage head, so the number of // deltas stored on DB should be exactly 2, as there are only 2 blocks after // the final block. - let deltas_in_metadata = - store_helper::get_all_deltas_metadata(&store, shard_uid).unwrap().len() as u64; + let deltas_in_metadata = store.get_all_deltas_metadata(shard_uid).unwrap().len() as u64; assert_eq!(deltas_in_metadata, 2); next_height @@ -122,11 +122,11 @@ fn test_flat_storage_creation_sanity() { init_test_logger(); let genesis = Genesis::test(vec!["test0".parse().unwrap()], 1); let shard_uid = genesis.config.shard_layout.shard_uids().next().unwrap(); - let store = create_test_store(); + let store = create_test_store().flat_store(); // Process some blocks with flat storage. Then remove flat storage data from disk. { - let mut env = setup_env(&genesis, store.clone()); + let mut env = setup_env(&genesis, store.store()); let signer = InMemorySigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0").into(); let genesis_hash = *env.clients[0].chain.genesis().hash(); @@ -149,7 +149,7 @@ fn test_flat_storage_creation_sanity() { let flat_head_height = START_HEIGHT - 4; let expected_flat_storage_head = env.clients[0].chain.get_block_hash_by_height(flat_head_height).unwrap(); - let status = store_helper::get_flat_storage_status(&store, shard_uid); + let status = store.get_flat_storage_status(shard_uid); if let Ok(FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head })) = status { assert_eq!(flat_head.hash, expected_flat_storage_head); assert_eq!(flat_head.height, flat_head_height); @@ -160,14 +160,14 @@ fn test_flat_storage_creation_sanity() { // Deltas for blocks until `flat_head_height` should not exist. for height in 0..=flat_head_height { let block_hash = env.clients[0].chain.get_block_hash_by_height(height).unwrap(); - assert_eq!(store_helper::get_delta_changes(&store, shard_uid, block_hash), Ok(None)); + assert_eq!(store.get_delta(shard_uid, block_hash), Ok(None)); } // Deltas for blocks until `START_HEIGHT` should still exist, // because they come after flat storage head. for height in flat_head_height + 1..START_HEIGHT { let block_hash = env.clients[0].chain.get_block_hash_by_height(height).unwrap(); assert_matches!( - store_helper::get_delta_changes(&store, shard_uid, block_hash), + store.get_delta(shard_uid, block_hash), Ok(Some(_)), "height: {height}" ); @@ -182,21 +182,18 @@ fn test_flat_storage_creation_sanity() { // Create new chain and runtime using the same store. It should produce next blocks normally, but now it should // think that flat storage does not exist and background creation should be initiated. - let mut env = setup_env(&genesis, store.clone()); + let mut env = setup_env(&genesis, store.store()); for height in START_HEIGHT..START_HEIGHT + 2 { env.produce_block(0, height); } assert!(get_flat_storage_manager(&env).get_flat_storage_for_shard(shard_uid).is_none()); - assert_eq!( - store_helper::get_flat_storage_status(&store, shard_uid), - Ok(FlatStorageStatus::Empty) - ); + assert_eq!(store.get_flat_storage_status(shard_uid), Ok(FlatStorageStatus::Empty)); assert!(!env.clients[0].run_flat_storage_creation_step().unwrap()); // At first, flat storage state should start saving deltas. Deltas for all newly processed blocks should be saved to // disk. assert_eq!( - store_helper::get_flat_storage_status(&store, shard_uid), + store.get_flat_storage_status(shard_uid), Ok(FlatStorageStatus::Creation(FlatStorageCreationStatus::SavingDeltas)) ); // Introduce fork block to check that deltas for it will be GC-d later. @@ -207,14 +204,8 @@ fn test_flat_storage_creation_sanity() { env.process_block(0, fork_block, Provenance::PRODUCED); env.process_block(0, next_block, Provenance::PRODUCED); - assert_matches!( - store_helper::get_delta_changes(&store, shard_uid, fork_block_hash), - Ok(Some(_)) - ); - assert_matches!( - store_helper::get_delta_changes(&store, shard_uid, next_block_hash), - Ok(Some(_)) - ); + assert_matches!(store.get_delta(shard_uid, fork_block_hash), Ok(Some(_))); + assert_matches!(store.get_delta(shard_uid, next_block_hash), Ok(Some(_))); // Produce new block and run flat storage creation step. // We started the node from height `START_HEIGHT - 1`, and now final head should move to height `START_HEIGHT`. @@ -224,7 +215,7 @@ fn test_flat_storage_creation_sanity() { assert!(!env.clients[0].run_flat_storage_creation_step().unwrap()); let final_block_hash = env.clients[0].chain.get_block_hash_by_height(START_HEIGHT).unwrap(); assert_eq!( - store_helper::get_flat_storage_status(&store, shard_uid), + store.get_flat_storage_status(shard_uid), Ok(FlatStorageStatus::Creation(FlatStorageCreationStatus::FetchingState( FetchingStateStatus { block_hash: final_block_hash, @@ -246,11 +237,11 @@ fn test_flat_storage_creation_two_shards() { let genesis = Genesis::test_sharded_new_version(vec!["test0".parse().unwrap()], 1, vec![1; num_shards]); let shard_uids: Vec<_> = genesis.config.shard_layout.shard_uids().collect(); - let store = create_test_store(); + let store = create_test_store().flat_store(); // Process some blocks with flat storages for two shards. Then remove flat storage data from disk for shard 0. { - let mut env = setup_env(&genesis, store.clone()); + let mut env = setup_env(&genesis, store.store()); let signer = InMemorySigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0").into(); let genesis_hash = *env.clients[0].chain.genesis().hash(); @@ -270,7 +261,7 @@ fn test_flat_storage_creation_two_shards() { for &shard_uid in shard_uids.iter() { assert_matches!( - store_helper::get_flat_storage_status(&store, shard_uid), + store.get_flat_storage_status(shard_uid), Ok(FlatStorageStatus::Ready(_)) ); } @@ -283,17 +274,11 @@ fn test_flat_storage_creation_two_shards() { } // Check that flat storage is not ready for shard 0 but ready for shard 1. - let mut env = setup_env(&genesis, store.clone()); + let mut env = setup_env(&genesis, store.store()); assert!(get_flat_storage_manager(&env).get_flat_storage_for_shard(shard_uids[0]).is_none()); - assert_matches!( - store_helper::get_flat_storage_status(&store, shard_uids[0]), - Ok(FlatStorageStatus::Empty) - ); + assert_matches!(store.get_flat_storage_status(shard_uids[0]), Ok(FlatStorageStatus::Empty)); assert!(get_flat_storage_manager(&env).get_flat_storage_for_shard(shard_uids[1]).is_some()); - assert_matches!( - store_helper::get_flat_storage_status(&store, shard_uids[1]), - Ok(FlatStorageStatus::Ready(_)) - ); + assert_matches!(store.get_flat_storage_status(shard_uids[1]), Ok(FlatStorageStatus::Ready(_))); wait_for_flat_storage_creation(&mut env, START_HEIGHT, shard_uids[0], true); } @@ -308,21 +293,18 @@ fn test_flat_storage_creation_start_from_state_part() { (0..4).map(|i| AccountId::from_str(&format!("test{}", i)).unwrap()).collect::>(); let genesis = Genesis::test(accounts, 1); let shard_uid = genesis.config.shard_layout.shard_uids().next().unwrap(); - let store = create_test_store(); + let store = create_test_store().flat_store(); // Process some blocks with flat storage. // Reshard into two parts and return trie keys corresponding to each part. const NUM_PARTS: u64 = 2; let trie_keys: Vec<_> = { - let mut env = setup_env(&genesis, store.clone()); + let mut env = setup_env(&genesis, store.store()); for height in 1..START_HEIGHT { env.produce_block(0, height); } - assert_matches!( - store_helper::get_flat_storage_status(&store, shard_uid), - Ok(FlatStorageStatus::Ready(_)) - ); + assert_matches!(store.get_flat_storage_status(shard_uid), Ok(FlatStorageStatus::Ready(_))); let block_hash = env.clients[0].chain.get_block_hash_by_height(START_HEIGHT - 1).unwrap(); let state_root = @@ -353,7 +335,7 @@ fn test_flat_storage_creation_start_from_state_part() { { // Remove keys of part 1 from the flat state. // Manually set flat storage creation status to the step when it should start from fetching part 1. - let status = store_helper::get_flat_storage_status(&store, shard_uid); + let status = store.get_flat_storage_status(shard_uid); let flat_head = if let Ok(FlatStorageStatus::Ready(ready_status)) = status { ready_status.flat_head.hash } else { @@ -361,10 +343,9 @@ fn test_flat_storage_creation_start_from_state_part() { }; let mut store_update = store.store_update(); for key in trie_keys[1].iter() { - store_helper::set_flat_state_value(&mut store_update, shard_uid, key.clone(), None); + store_update.set(shard_uid, key.clone(), None); } - store_helper::set_flat_storage_status( - &mut store_update, + store_update.set_flat_storage_status( shard_uid, FlatStorageStatus::Creation(FlatStorageCreationStatus::FetchingState( FetchingStateStatus { @@ -378,7 +359,7 @@ fn test_flat_storage_creation_start_from_state_part() { store_update.commit().unwrap(); // Re-create runtime, check that flat storage is not created yet. - let mut env = setup_env(&genesis, store); + let mut env = setup_env(&genesis, store.store()); assert!(get_flat_storage_manager(&env).get_flat_storage_for_shard(shard_uid).is_none()); // Run chain for a couple of blocks and check that flat storage for shard 0 is eventually created. @@ -411,12 +392,12 @@ fn test_flat_storage_creation_start_from_state_part() { fn test_catchup_succeeds_even_if_no_new_blocks() { init_test_logger(); let genesis = Genesis::test(vec!["test0".parse().unwrap()], 1); - let store = create_test_store(); + let store = create_test_store().flat_store(); let shard_uid = ShardLayout::v0_single_shard().shard_uids().next().unwrap(); // Process some blocks with flat storage. Then remove flat storage data from disk. { - let mut env = setup_env(&genesis, store.clone()); + let mut env = setup_env(&genesis, store.store()); for height in 1..START_HEIGHT { env.produce_block(0, height); } @@ -427,12 +408,9 @@ fn test_catchup_succeeds_even_if_no_new_blocks() { .unwrap(); store_update.commit().unwrap(); } - let mut env = setup_env(&genesis, store.clone()); + let mut env = setup_env(&genesis, store.store()); assert!(get_flat_storage_manager(&env).get_flat_storage_for_shard(shard_uid).is_none()); - assert_eq!( - store_helper::get_flat_storage_status(&store, shard_uid), - Ok(FlatStorageStatus::Empty) - ); + assert_eq!(store.get_flat_storage_status(shard_uid), Ok(FlatStorageStatus::Empty)); // Create 3 more blocks (so that the deltas are generated) - and assume that no new blocks are received. // In the future, we should also support the scenario where no new blocks are created. @@ -460,17 +438,16 @@ fn test_flat_storage_iter() { shard_layout.clone(), ); - let store = create_test_store(); + let store = create_test_store().flat_store(); - let mut env = setup_env(&genesis, store.clone()); + let mut env = setup_env(&genesis, store.store()); for height in 1..START_HEIGHT { env.produce_block(0, height); } for shard_id in 0..3 { let shard_uid = ShardUId::from_shard_id_and_layout(shard_id, &shard_layout); - let items: Vec<_> = - store_helper::iter_flat_state_entries(shard_uid, &store, None, None).collect(); + let items: Vec<_> = store.iter(shard_uid).collect(); match shard_id { 0 => { diff --git a/integration-tests/src/tests/client/process_blocks.rs b/integration-tests/src/tests/client/process_blocks.rs index a05c2419c44..c4c991c0110 100644 --- a/integration-tests/src/tests/client/process_blocks.rs +++ b/integration-tests/src/tests/client/process_blocks.rs @@ -65,6 +65,7 @@ use near_primitives::views::{ }; use near_primitives_core::num_rational::{Ratio, Rational32}; use near_primitives_core::types::ShardId; +use near_store::adapter::StoreUpdateAdapter; use near_store::cold_storage::{update_cold_db, update_cold_head}; use near_store::metadata::DbKind; use near_store::metadata::DB_VERSION; @@ -2420,7 +2421,7 @@ fn test_catchup_gas_price_change() { let mut store_update = store.store_update(); assert!(rt .get_flat_storage_manager() - .remove_flat_storage_for_shard(msg.shard_uid, &mut store_update) + .remove_flat_storage_for_shard(msg.shard_uid, &mut store_update.flat_store_update()) .unwrap()); store_update.commit().unwrap(); for part_id in 0..msg.num_parts { diff --git a/integration-tests/src/tests/client/state_dump.rs b/integration-tests/src/tests/client/state_dump.rs index d64e692de6a..266f426458c 100644 --- a/integration-tests/src/tests/client/state_dump.rs +++ b/integration-tests/src/tests/client/state_dump.rs @@ -20,7 +20,7 @@ use near_primitives::transaction::SignedTransaction; use near_primitives::types::BlockHeight; use near_primitives::validator_signer::{EmptyValidatorSigner, InMemoryValidatorSigner}; use near_primitives::views::{QueryRequest, QueryResponseKind}; -use near_store::flat::store_helper; +use near_store::adapter::{StoreAdapter, StoreUpdateAdapter}; use near_store::DBCol; use near_store::Store; use nearcore::state_sync::StateSyncDumper; @@ -305,7 +305,10 @@ fn run_state_sync_with_dumped_parts( let mut store_update = runtime_client_1.store().store_update(); assert!(runtime_client_1 .get_flat_storage_manager() - .remove_flat_storage_for_shard(ShardUId::single_shard(), &mut store_update) + .remove_flat_storage_for_shard( + ShardUId::single_shard(), + &mut store_update.flat_store_update() + ) .unwrap()); store_update.commit().unwrap(); @@ -395,7 +398,7 @@ fn test_state_sync_w_dumped_parts() { fn count_flat_state_value_kinds(store: &Store) -> (u64, u64) { let mut num_inlined_values = 0; let mut num_ref_values = 0; - for item in store_helper::iter_flat_state_entries(ShardUId::single_shard(), store, None, None) { + for item in store.flat_store().iter(ShardUId::single_shard()) { match item { Ok((_, FlatStateValue::Ref(_))) => { num_ref_values += 1; diff --git a/integration-tests/src/tests/client/state_snapshot.rs b/integration-tests/src/tests/client/state_snapshot.rs index 6d1e6b7f85b..07fbfcef016 100644 --- a/integration-tests/src/tests/client/state_snapshot.rs +++ b/integration-tests/src/tests/client/state_snapshot.rs @@ -8,6 +8,7 @@ use near_primitives::block::Block; use near_primitives::hash::CryptoHash; use near_primitives::shard_layout::ShardUId; use near_primitives::transaction::SignedTransaction; +use near_store::adapter::StoreAdapter; use near_store::config::StateSnapshotType; use near_store::flat::FlatStorageManager; use near_store::{ @@ -42,7 +43,7 @@ impl StateSnaptshotTestEnv { view_shard_cache_config: trie_cache_config, ..TrieConfig::default() }; - let flat_storage_manager = FlatStorageManager::new(store.clone()); + let flat_storage_manager = FlatStorageManager::new(store.flat_store()); let shard_uids = [ShardUId::single_shard()]; let state_snapshot_config = StateSnapshotConfig { state_snapshot_type: StateSnapshotType::EveryEpoch, diff --git a/integration-tests/src/tests/client/sync_state_nodes.rs b/integration-tests/src/tests/client/sync_state_nodes.rs index d7473ce51fe..71129748dbe 100644 --- a/integration-tests/src/tests/client/sync_state_nodes.rs +++ b/integration-tests/src/tests/client/sync_state_nodes.rs @@ -23,6 +23,7 @@ use near_primitives::transaction::SignedTransaction; use near_primitives::types::{BlockId, BlockReference, EpochId, EpochReference}; use near_primitives::utils::MaybeValidated; use near_primitives_core::types::ShardId; +use near_store::adapter::StoreUpdateAdapter; use near_store::DBCol; use nearcore::test_utils::TestEnvNightshadeSetupExt; use nearcore::{load_test_config, start_with_config}; @@ -678,7 +679,10 @@ fn test_dump_epoch_missing_chunk_in_last_block() { let mut store_update = store.store_update(); assert!(rt .get_flat_storage_manager() - .remove_flat_storage_for_shard(msg.shard_uid, &mut store_update) + .remove_flat_storage_for_shard( + msg.shard_uid, + &mut store_update.flat_store_update() + ) .unwrap()); store_update.commit().unwrap(); diff --git a/integration-tests/src/user/runtime_user.rs b/integration-tests/src/user/runtime_user.rs index 7be65456a57..ef9df7afa0d 100644 --- a/integration-tests/src/user/runtime_user.rs +++ b/integration-tests/src/user/runtime_user.rs @@ -20,6 +20,7 @@ use near_primitives::views::{ ExecutionOutcomeView, ExecutionOutcomeWithIdView, ExecutionStatusView, FinalExecutionOutcomeView, FinalExecutionStatus, ViewStateResult, }; +use near_store::adapter::StoreUpdateAdapter; use near_store::{ShardTries, TrieUpdate}; use node_runtime::state_viewer::TrieViewer; use node_runtime::{state_viewer::ViewApplyState, ApplyState, Runtime}; @@ -136,7 +137,7 @@ impl RuntimeUser { ); if use_flat_storage { near_store::flat::FlatStateChanges::from_state_changes(&apply_result.state_changes) - .apply_to_flat_state(&mut update, ShardUId::single_shard()); + .apply_to_flat_state(&mut update.flat_store_update(), ShardUId::single_shard()); } update.commit().unwrap(); client.state_root = apply_result.state_root; diff --git a/nearcore/src/entity_debug.rs b/nearcore/src/entity_debug.rs index 70bca547610..e074e572ebe 100644 --- a/nearcore/src/entity_debug.rs +++ b/nearcore/src/entity_debug.rs @@ -30,9 +30,9 @@ use near_primitives::utils::{get_block_shard_id, get_outcome_id_block_hash}; use near_primitives::views::{ BlockHeaderView, BlockView, ChunkView, ExecutionOutcomeView, ReceiptView, SignedTransactionView, }; +use near_store::adapter::flat_store::encode_flat_state_db_key; use near_store::db::GENESIS_CONGESTION_INFO_KEY; use near_store::flat::delta::KeyForFlatStateDelta; -use near_store::flat::store_helper::encode_flat_state_db_key; use near_store::flat::{FlatStateChanges, FlatStateDeltaMetadata, FlatStorageStatus}; use near_store::{ DBCol, NibbleSlice, RawTrieNode, RawTrieNodeWithSize, ShardUId, Store, TrieCachingStorage, diff --git a/runtime/runtime-params-estimator/src/estimator_context.rs b/runtime/runtime-params-estimator/src/estimator_context.rs index 071527c26eb..5b72701e05e 100644 --- a/runtime/runtime-params-estimator/src/estimator_context.rs +++ b/runtime/runtime-params-estimator/src/estimator_context.rs @@ -15,8 +15,9 @@ use near_primitives::test_utils::MockEpochInfoProvider; use near_primitives::transaction::{ExecutionStatus, SignedTransaction}; use near_primitives::types::{Gas, MerkleHash}; use near_primitives::version::{ProtocolFeature, PROTOCOL_VERSION}; +use near_store::adapter::{StoreAdapter, StoreUpdateAdapter}; use near_store::flat::{ - store_helper, BlockInfo, FlatStateChanges, FlatStateDelta, FlatStateDeltaMetadata, FlatStorage, + BlockInfo, FlatStateChanges, FlatStateDelta, FlatStateDeltaMetadata, FlatStorage, FlatStorageManager, FlatStorageReadyStatus, FlatStorageStatus, }; use near_store::{ShardTries, ShardUId, StateSnapshotConfig, TrieUpdate}; @@ -77,10 +78,10 @@ impl<'c> EstimatorContext<'c> { let root = roots[0]; let shard_uid = ShardUId::single_shard(); - let flat_storage_manager = FlatStorageManager::new(store.clone()); - let mut store_update = store.store_update(); - store_helper::set_flat_storage_status( - &mut store_update, + let flat_store = store.flat_store(); + let flat_storage_manager = FlatStorageManager::new(flat_store.clone()); + let mut store_update = flat_store.store_update(); + store_update.set_flat_storage_status( shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: BlockInfo::genesis(CryptoHash::hash_borsh(0usize), 0), @@ -358,7 +359,8 @@ impl Testbed<'_> { ) .unwrap(); - let mut store_update = self.tries.store_update(); + let store = self.tries.get_store(); + let mut store_update = store.store_update(); let shard_uid = ShardUId::single_shard(); self.root = self.tries.apply_all(&apply_result.trie_changes, shard_uid, &mut store_update); if self.config.memtrie { @@ -375,7 +377,7 @@ impl Testbed<'_> { assert_eq!(self.root, memtrie_root); } near_store::flat::FlatStateChanges::from_state_changes(&apply_result.state_changes) - .apply_to_flat_state(&mut store_update, shard_uid); + .apply_to_flat_state(&mut store_update.flat_store_update(), shard_uid); store_update.commit().unwrap(); self.apply_state.block_height += 1; if let Some(congestion_info) = apply_result.congestion_info { diff --git a/runtime/runtime/src/prefetch.rs b/runtime/runtime/src/prefetch.rs index e75e51f83ce..033d48f7e50 100644 --- a/runtime/runtime/src/prefetch.rs +++ b/runtime/runtime/src/prefetch.rs @@ -386,6 +386,7 @@ impl TriePrefetcher { mod tests { use super::TriePrefetcher; use near_primitives::{trie_key::TrieKey, types::AccountId}; + use near_store::adapter::StoreAdapter; use near_store::test_utils::{create_test_store, test_populate_trie}; use near_store::{ShardTries, ShardUId, StateSnapshotConfig, Trie, TrieConfig}; use std::str::FromStr; @@ -474,7 +475,7 @@ mod tests { let shard_uids = vec![ShardUId::single_shard()]; let trie_config = TrieConfig { enable_receipt_prefetching: true, ..TrieConfig::default() }; let store = create_test_store(); - let flat_storage_manager = near_store::flat::FlatStorageManager::new(store.clone()); + let flat_storage_manager = near_store::flat::FlatStorageManager::new(store.flat_store()); let tries = ShardTries::new( store, trie_config, diff --git a/tools/database/src/analyze_delayed_receipt.rs b/tools/database/src/analyze_delayed_receipt.rs index 13942a8d0b1..b589649f82e 100644 --- a/tools/database/src/analyze_delayed_receipt.rs +++ b/tools/database/src/analyze_delayed_receipt.rs @@ -1,4 +1,5 @@ use clap::Parser; +use near_store::adapter::StoreAdapter; use near_store::flat::FlatStorageManager; use near_store::{get_delayed_receipt_indices, ShardTries, StateSnapshotConfig, TrieConfig}; use std::collections::HashMap; @@ -61,7 +62,7 @@ impl AnalyzeDelayedReceiptCommand { store.clone(), TrieConfig::default(), &shard_uids, - FlatStorageManager::new(store), + FlatStorageManager::new(store.flat_store()), StateSnapshotConfig::default(), ); // Create an iterator over the blocks that should be analysed diff --git a/tools/database/src/corrupt.rs b/tools/database/src/corrupt.rs index af299efa908..5fe5ae360d9 100644 --- a/tools/database/src/corrupt.rs +++ b/tools/database/src/corrupt.rs @@ -2,7 +2,9 @@ use crate::utils::open_state_snapshot; use anyhow::anyhow; use clap::Parser; use near_primitives::shard_layout::{ShardLayout, ShardVersion}; -use near_store::{flat::FlatStorageManager, ShardUId, StoreUpdate}; +use near_store::adapter::flat_store::FlatStoreUpdateAdapter; +use near_store::adapter::StoreAdapter; +use near_store::{flat::FlatStorageManager, ShardUId}; use std::path::PathBuf; #[derive(Parser)] @@ -13,7 +15,7 @@ pub(crate) struct CorruptStateSnapshotCommand { impl CorruptStateSnapshotCommand { pub(crate) fn run(&self, home: &PathBuf) -> anyhow::Result<()> { - let store = open_state_snapshot(home, near_store::Mode::ReadWrite)?; + let store = open_state_snapshot(home, near_store::Mode::ReadWrite)?.flat_store(); let flat_storage_manager = FlatStorageManager::new(store.clone()); let mut store_update = store.store_update(); @@ -41,7 +43,7 @@ impl CorruptStateSnapshotCommand { } fn corrupt( - store_update: &mut StoreUpdate, + store_update: &mut FlatStoreUpdateAdapter, flat_storage_manager: &FlatStorageManager, shard_uid: ShardUId, ) -> Result<(), anyhow::Error> { diff --git a/tools/database/src/state_perf.rs b/tools/database/src/state_perf.rs index 9e6af54ccef..2b35612fb88 100644 --- a/tools/database/src/state_perf.rs +++ b/tools/database/src/state_perf.rs @@ -1,5 +1,7 @@ use clap::Parser; use indicatif::{ProgressBar, ProgressIterator}; +use near_store::adapter::flat_store::FlatStoreAdapter; +use near_store::adapter::StoreAdapter; use std::collections::BTreeMap; use std::fmt::{Display, Write}; use std::path::Path; @@ -12,8 +14,7 @@ use rand::rngs::StdRng; use rand::seq::SliceRandom; use rand::SeedableRng; -use near_store::flat::store_helper::iter_flat_state_entries; -use near_store::{Store, TrieStorage}; +use near_store::TrieStorage; use crate::utils::open_rocksdb; @@ -38,7 +39,10 @@ impl StatePerfCommand { let mut perf_context = PerfContext::new(); let total_samples = self.warmup_samples + self.samples; for (sample_i, (shard_uid, value_ref)) in - generate_state_requests(store.clone(), total_samples).into_iter().enumerate().progress() + generate_state_requests(store.flat_store(), total_samples) + .into_iter() + .enumerate() + .progress() { let trie_storage = near_store::TrieDBStorage::new(store.clone(), shard_uid); let include_sample = sample_i >= self.warmup_samples; @@ -159,7 +163,7 @@ impl PerfContext { } } -fn generate_state_requests(store: Store, samples: usize) -> Vec<(ShardUId, ValueRef)> { +fn generate_state_requests(store: FlatStoreAdapter, samples: usize) -> Vec<(ShardUId, ValueRef)> { eprintln!("Generate {samples} requests to State"); let shard_uids = ShardLayout::get_simple_nightshade_layout().shard_uids().collect::>(); let num_shards = shard_uids.len(); @@ -168,8 +172,8 @@ fn generate_state_requests(store: Store, samples: usize) -> Vec<(ShardUId, Value for shard_uid in shard_uids { let shard_samples = samples / num_shards; let mut keys_read = std::collections::HashSet::new(); - for value_ref in iter_flat_state_entries(shard_uid, &store, None, None) - .flat_map(|res| res.map(|(_, value)| value.to_value_ref())) + for value_ref in + store.iter(shard_uid).flat_map(|res| res.map(|(_, value)| value.to_value_ref())) { if value_ref.length > 4096 || !keys_read.insert(value_ref.hash) { continue; diff --git a/tools/flat-storage/src/commands.rs b/tools/flat-storage/src/commands.rs index ef6527411c4..9c2a2b9f12b 100644 --- a/tools/flat-storage/src/commands.rs +++ b/tools/flat-storage/src/commands.rs @@ -9,8 +9,10 @@ use near_epoch_manager::{EpochManager, EpochManagerAdapter, EpochManagerHandle}; use near_primitives::shard_layout::{account_id_to_shard_id, ShardVersion}; use near_primitives::state::FlatStateValue; use near_primitives::types::{BlockHeight, ShardId}; +use near_store::adapter::flat_store::FlatStoreAdapter; +use near_store::adapter::StoreAdapter; use near_store::flat::{ - store_helper, FlatStateChanges, FlatStateDelta, FlatStateDeltaMetadata, FlatStorageStatus, + FlatStateChanges, FlatStateDelta, FlatStateDeltaMetadata, FlatStorageStatus, }; use near_store::{DBCol, Mode, NodeStorage, ShardUId, Store, StoreOpener}; use nearcore::{load_config, NearConfig, NightshadeRuntime, NightshadeRuntimeExt}; @@ -125,14 +127,13 @@ pub struct MoveFlatHeadCmd { mode: MoveFlatHeadMode, } -fn print_delta(store: &Store, shard_uid: ShardUId, metadata: FlatStateDeltaMetadata) { - let changes = - store_helper::get_delta_changes(store, shard_uid, metadata.block.hash).unwrap().unwrap(); +fn print_delta(store: &FlatStoreAdapter, shard_uid: ShardUId, metadata: FlatStateDeltaMetadata) { + let changes = store.get_delta(shard_uid, metadata.block.hash).unwrap().unwrap(); println!("{:?}", FlatStateDelta { metadata, changes }); } -fn print_deltas(store: &Store, shard_uid: ShardUId) { - let deltas_metadata = store_helper::get_all_deltas_metadata(store, shard_uid).unwrap(); +fn print_deltas(store: &FlatStoreAdapter, shard_uid: ShardUId) { + let deltas_metadata = store.get_all_deltas_metadata(shard_uid).unwrap(); let num_deltas = deltas_metadata.len(); println!("Deltas: {}", num_deltas); @@ -202,7 +203,7 @@ impl FlatStorageCommand { "Shard: {shard_uid:?} - flat storage @{:?} ({})", ready_status.flat_head.height, ready_status.flat_head.hash, ); - print_deltas(&hot_store, shard_uid); + print_deltas(&hot_store.flat_store(), shard_uid); } status => { println!("Shard: {shard_uid:?} - no flat storage: {status:?}"); @@ -239,7 +240,7 @@ impl FlatStorageCommand { let shard_uid = epoch_manager.shard_id_to_uid(cmd.shard_id, &tip.epoch_id)?; let flat_storage_manager = rw_hot_runtime.get_flat_storage_manager(); flat_storage_manager.create_flat_storage_for_shard(shard_uid)?; - let mut store_update = store.store_update(); + let mut store_update = store.flat_store().store_update(); flat_storage_manager.remove_flat_storage_for_shard(shard_uid, &mut store_update)?; store_update.commit()?; Ok(()) @@ -266,7 +267,7 @@ impl FlatStorageCommand { if status { break; } - let current_status = store_helper::get_flat_storage_status(&rw_hot_store, shard_uid); + let current_status = rw_hot_store.flat_store().get_flat_storage_status(shard_uid); println!("Status: {:?}", current_status); std::thread::sleep(Duration::from_secs(1)); @@ -287,8 +288,10 @@ impl FlatStorageCommand { Self::get_db(&opener, home_dir, &near_config, near_store::Mode::ReadOnly); let tip = chain_store.final_head()?; let shard_uid = epoch_manager.shard_id_to_uid(cmd.shard_id, &tip.epoch_id)?; + let hot_store = hot_store.flat_store(); - let head_hash = match store_helper::get_flat_storage_status(&hot_store, shard_uid) + let head_hash = match hot_store + .get_flat_storage_status(shard_uid) .expect("falied to read flat storage status") { FlatStorageStatus::Ready(ready_status) => ready_status.flat_head.hash, @@ -321,8 +324,7 @@ impl FlatStorageCommand { let trie = hot_runtime.get_view_trie_for_shard(cmd.shard_id, &head_hash, *state_root)?; - let flat_state_entries_iter = - store_helper::iter_flat_state_entries(shard_uid, &hot_store, None, None); + let flat_state_entries_iter = hot_store.iter(shard_uid); let trie_iter = trie.disk_iter()?; let mut verified = 0; @@ -428,9 +430,9 @@ impl FlatStorageCommand { }), ); - let iter = store_helper::iter_flat_state_entries( + let flat_store = store.flat_store(); + let iter = flat_store.iter_range( shard_uid, - &store, Some(missing_keys_left_boundary), Some(missing_keys_right_boundary), ); @@ -462,7 +464,8 @@ impl FlatStorageCommand { blocks: usize, ) -> anyhow::Result<()> { let store = chain_store.store(); - let flat_head = match store_helper::get_flat_storage_status(&store, shard_uid) { + let flat_store = store.flat_store(); + let flat_head = match flat_store.get_flat_storage_status(shard_uid) { Ok(FlatStorageStatus::Ready(ready_status)) => ready_status.flat_head, status => { panic!("invalid flat storage status for shard {shard_uid:?}: {status:?}") @@ -529,7 +532,8 @@ impl FlatStorageCommand { .map(|value_ref| { near_primitives::state::FlatStateValue::Ref(value_ref.into_value_ref()) }); - let value = store_helper::get_flat_state_value(&store, shard_uid, trie_key)? + let value = flat_store + .get(shard_uid, trie_key)? .map(|val| near_primitives::state::FlatStateValue::Ref(val.to_value_ref())); if prev_value != value { prev_delta.insert(trie_key.to_vec(), prev_value); @@ -547,10 +551,9 @@ impl FlatStorageCommand { // Note that we don't write delta to DB, because this command is // used to simulate applying chunks from past blocks, and in that // simulations future deltas should not exist. - let mut store_update = store.store_update(); + let mut store_update = flat_store.store_update(); prev_delta.apply_to_flat_state(&mut store_update, shard_uid); - store_helper::set_flat_storage_status( - &mut store_update, + store_update.set_flat_storage_status( shard_uid, FlatStorageStatus::Ready(near_store::flat::FlatStorageReadyStatus { flat_head: near_store::flat::BlockInfo { diff --git a/tools/fork-network/src/cli.rs b/tools/fork-network/src/cli.rs index 8c17e41f9e3..b5bbccebeea 100644 --- a/tools/fork-network/src/cli.rs +++ b/tools/fork-network/src/cli.rs @@ -25,8 +25,9 @@ use near_primitives::types::{ AccountId, AccountInfo, Balance, BlockHeight, EpochId, NumBlocks, ShardId, StateRoot, }; use near_primitives::version::{ProtocolVersion, PROTOCOL_VERSION}; +use near_store::adapter::StoreAdapter; use near_store::db::RocksDB; -use near_store::flat::{store_helper, BlockInfo, FlatStorageManager, FlatStorageStatus}; +use near_store::flat::{BlockInfo, FlatStorageManager, FlatStorageStatus}; use near_store::{ checkpoint_hot_storage_and_cleanup_columns, DBCol, Store, TrieDBStorage, TrieStorage, FINAL_HEAD_KEY, @@ -268,7 +269,7 @@ impl ForkNetworkCommand { let desired_block_header = chain.get_block_header(&desired_block_hash)?; let epoch_id = desired_block_header.epoch_id(); - let flat_storage_manager = FlatStorageManager::new(store.clone()); + let flat_storage_manager = FlatStorageManager::new(store.flat_store()); // Advance flat heads to the same (max) block height to ensure // consistency of state across the shards. @@ -519,7 +520,7 @@ impl ForkNetworkCommand { let mut postponed_receipts_updated = 0; let mut received_data_updated = 0; let mut fake_block_height = block_height + 1; - for item in store_helper::iter_flat_state_entries(shard_uid, &store, None, None) { + for item in store.flat_store().iter(shard_uid) { let (key, value) = match item { Ok((key, FlatStateValue::Ref(ref_value))) => { ref_keys_retrieved += 1; @@ -660,7 +661,7 @@ impl ForkNetworkCommand { // Iterating over the whole flat state is very fast compared to writing all the updates. let mut num_added = 0; let mut num_accounts = 0; - for item in store_helper::iter_flat_state_entries(shard_uid, &store, None, None) { + for item in store.flat_store().iter(shard_uid) { if let Ok((key, _)) = item { if key[0] == col::ACCOUNT { num_accounts += 1; diff --git a/tools/fork-network/src/single_shard_storage_mutator.rs b/tools/fork-network/src/single_shard_storage_mutator.rs index 70bf695c689..76e74fee5fd 100644 --- a/tools/fork-network/src/single_shard_storage_mutator.rs +++ b/tools/fork-network/src/single_shard_storage_mutator.rs @@ -8,6 +8,7 @@ use near_primitives::shard_layout::ShardUId; use near_primitives::trie_key::TrieKey; use near_primitives::types::{AccountId, StateRoot}; use near_primitives::types::{StoreKey, StoreValue}; +use near_store::adapter::StoreUpdateAdapter; use near_store::{flat::FlatStateChanges, DBCol, ShardTries}; use nearcore::NightshadeRuntime; @@ -150,9 +151,9 @@ impl SingleShardStorageMutator { ) -> anyhow::Result { let num_updates = self.updates.len(); tracing::info!(?shard_uid, num_updates, "commit"); - let mut update = self.shard_tries.store_update(); let flat_state_changes = FlatStateChanges::from_raw_key_value(&self.updates); - flat_state_changes.apply_to_flat_state(&mut update, *shard_uid); + let mut update = self.shard_tries.store_update(); + flat_state_changes.apply_to_flat_state(&mut update.flat_store_update(), *shard_uid); let trie_changes = self .shard_tries diff --git a/tools/state-viewer/src/apply_chain_range.rs b/tools/state-viewer/src/apply_chain_range.rs index 77925fbfc8d..2a07740b234 100644 --- a/tools/state-viewer/src/apply_chain_range.rs +++ b/tools/state-viewer/src/apply_chain_range.rs @@ -15,6 +15,7 @@ use near_primitives::transaction::{Action, ExecutionOutcomeWithId, ExecutionOutc use near_primitives::trie_key::TrieKey; use near_primitives::types::chunk_extra::ChunkExtra; use near_primitives::types::{BlockHeight, ShardId}; +use near_store::adapter::StoreAdapter; use near_store::flat::{BlockInfo, FlatStateChanges, FlatStorageStatus}; use near_store::{DBCol, Store}; use nearcore::NightshadeRuntime; @@ -360,10 +361,7 @@ pub fn apply_chain_range( shard_id, &shard_layout, ); - let flat_head = match near_store::flat::store_helper::get_flat_storage_status( - &read_store, - shard_uid, - ) { + let flat_head = match read_store.flat_store().get_flat_storage_status(shard_uid) { Ok(FlatStorageStatus::Ready(ready_status)) => ready_status.flat_head, status => { panic!("cannot create flat storage for shard {shard_id} with status {status:?}") diff --git a/tools/state-viewer/src/commands.rs b/tools/state-viewer/src/commands.rs index 6c61cbdc109..2d9bc200101 100644 --- a/tools/state-viewer/src/commands.rs +++ b/tools/state-viewer/src/commands.rs @@ -1311,9 +1311,7 @@ fn get_state_stats_group_by<'a>( // the account id. let type_iters = COLUMNS_WITH_ACCOUNT_ID_IN_KEY .iter() - .map(|(type_byte, _)| { - chunk_view.iter_flat_state_entries(Some(&[*type_byte]), Some(&[*type_byte + 1])) - }) + .map(|(type_byte, _)| chunk_view.iter_range(Some(&[*type_byte]), Some(&[*type_byte + 1]))) .into_iter(); // Filter out any errors. diff --git a/tools/state-viewer/src/scan_db.rs b/tools/state-viewer/src/scan_db.rs index 5a6276fd71c..db8395e4de5 100644 --- a/tools/state-viewer/src/scan_db.rs +++ b/tools/state-viewer/src/scan_db.rs @@ -18,6 +18,7 @@ use near_primitives::types::{EpochId, StateRoot}; use near_primitives::utils::{get_block_shard_id_rev, get_outcome_id_block_hash_rev}; use near_primitives_core::hash::CryptoHash; use near_primitives_core::types::BlockHeight; +use near_store::adapter::flat_store::decode_flat_state_db_key; use near_store::flat::delta::KeyForFlatStateDelta; use near_store::flat::{FlatStateChanges, FlatStateDeltaMetadata}; use near_store::{DBCol, RawTrieNodeWithSize, Store, TrieChanges}; @@ -133,8 +134,7 @@ fn format_key_and_value<'a>( Box::new(BlockHeight::try_from_slice(value).unwrap()), ), DBCol::FlatState => { - let (shard_uid, key) = - near_store::flat::store_helper::decode_flat_state_db_key(key).unwrap(); + let (shard_uid, key) = decode_flat_state_db_key(key).unwrap(); (Box::new((shard_uid, key)), Box::new(FlatStateValue::try_from_slice(value).unwrap())) } DBCol::FlatStateChanges => ( From 0e58ed76eaca4e867b45fcc85d74cfc9570d682e Mon Sep 17 00:00:00 2001 From: Shreyan Gupta Date: Thu, 26 Sep 2024 12:03:08 -0700 Subject: [PATCH 30/49] [store] Introduce Chunk store adapter (#12158) Follow up on https://github.com/near/nearcore/pull/12123 This PR replaces `ReadOnlyChunksStore` with `ChunkStoreAdapter`. --- chain/chain-primitives/src/error.rs | 10 +++- chain/chain/src/chunks_store.rs | 60 ------------------- chain/chain/src/lib.rs | 1 - chain/chain/src/store/mod.rs | 5 -- chain/chunks/src/shards_manager_actor.rs | 48 +++++++-------- chain/chunks/src/test_utils.rs | 7 ++- chain/client/src/test_utils/setup.rs | 4 +- core/primitives/src/errors.rs | 14 +++++ core/store/src/adapter/chunk_store.rs | 42 +++++++++++++ core/store/src/adapter/mod.rs | 5 ++ integration-tests/src/test_loop/builder.rs | 4 +- .../tests/simple_test_loop_example.rs | 4 +- 12 files changed, 105 insertions(+), 99 deletions(-) delete mode 100644 chain/chain/src/chunks_store.rs create mode 100644 core/store/src/adapter/chunk_store.rs diff --git a/chain/chain-primitives/src/error.rs b/chain/chain-primitives/src/error.rs index 2161c01f30f..f394a31addf 100644 --- a/chain/chain-primitives/src/error.rs +++ b/chain/chain-primitives/src/error.rs @@ -1,6 +1,6 @@ use near_primitives::block::BlockValidityError; use near_primitives::challenge::{ChunkProofs, ChunkState}; -use near_primitives::errors::{EpochError, StorageError}; +use near_primitives::errors::{ChunkAccessError, EpochError, StorageError}; use near_primitives::shard_layout::ShardLayoutError; use near_primitives::sharding::{ChunkHash, ShardChunkHeader}; use near_primitives::types::{BlockHeight, EpochId, ShardId}; @@ -440,6 +440,14 @@ impl From for Error { } } +impl From for Error { + fn from(error: ChunkAccessError) -> Self { + match error { + ChunkAccessError::ChunkMissing(chunk_hash) => Error::ChunkMissing(chunk_hash), + } + } +} + #[derive(Clone, Eq, PartialEq, Debug, thiserror::Error)] pub enum BlockKnownError { #[error("already known in header")] diff --git a/chain/chain/src/chunks_store.rs b/chain/chain/src/chunks_store.rs deleted file mode 100644 index 56219d4db92..00000000000 --- a/chain/chain/src/chunks_store.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::sync::Arc; - -use borsh::BorshDeserialize; -use near_cache::CellLruCache; -use near_chain_primitives::Error; -use near_primitives::sharding::{ChunkHash, PartialEncodedChunk, ShardChunk}; -use near_store::{DBCol, Store}; - -#[cfg(not(feature = "no_cache"))] -const CHUNK_CACHE_SIZE: usize = 1024; -#[cfg(feature = "no_cache")] -const CHUNK_CACHE_SIZE: usize = 1; - -pub struct ReadOnlyChunksStore { - store: Store, - partial_chunks: CellLruCache, Arc>, - chunks: CellLruCache, Arc>, -} - -impl ReadOnlyChunksStore { - pub fn new(store: Store) -> Self { - Self { - store, - partial_chunks: CellLruCache::new(CHUNK_CACHE_SIZE), - chunks: CellLruCache::new(CHUNK_CACHE_SIZE), - } - } - - fn read_with_cache<'a, T: BorshDeserialize + Clone + 'a>( - &self, - col: DBCol, - cache: &'a CellLruCache, T>, - key: &[u8], - ) -> std::io::Result> { - if let Some(value) = cache.get(key) { - return Ok(Some(value)); - } - if let Some(result) = self.store.get_ser::(col, key)? { - cache.put(key.to_vec(), result.clone()); - return Ok(Some(result)); - } - Ok(None) - } - pub fn get_partial_chunk( - &self, - chunk_hash: &ChunkHash, - ) -> Result, Error> { - match self.read_with_cache(DBCol::PartialChunks, &self.partial_chunks, chunk_hash.as_ref()) - { - Ok(Some(shard_chunk)) => Ok(shard_chunk), - _ => Err(Error::ChunkMissing(chunk_hash.clone())), - } - } - pub fn get_chunk(&self, chunk_hash: &ChunkHash) -> Result, Error> { - match self.read_with_cache(DBCol::Chunks, &self.chunks, chunk_hash.as_ref()) { - Ok(Some(shard_chunk)) => Ok(shard_chunk), - _ => Err(Error::ChunkMissing(chunk_hash.clone())), - } - } -} diff --git a/chain/chain/src/lib.rs b/chain/chain/src/lib.rs index d296f721a5d..ed95f14c37f 100644 --- a/chain/chain/src/lib.rs +++ b/chain/chain/src/lib.rs @@ -15,7 +15,6 @@ mod block_processing_utils; pub mod blocks_delay_tracker; pub mod chain; mod chain_update; -pub mod chunks_store; pub mod crypto_hash_timer; mod doomslug; pub mod flat_storage_creator; diff --git a/chain/chain/src/store/mod.rs b/chain/chain/src/store/mod.rs index 3e65b53690c..619a800bdfb 100644 --- a/chain/chain/src/store/mod.rs +++ b/chain/chain/src/store/mod.rs @@ -47,7 +47,6 @@ use near_store::{ }; use crate::byzantine_assert; -use crate::chunks_store::ReadOnlyChunksStore; use crate::types::{Block, BlockHeader, LatestKnown}; use near_store::db::{StoreStatistics, STATE_SYNC_DUMP_KEY}; use std::sync::Arc; @@ -494,10 +493,6 @@ impl ChainStore { } } - pub fn new_read_only_chunks_store(&self) -> ReadOnlyChunksStore { - ReadOnlyChunksStore::new(self.store.clone()) - } - pub fn store_update(&mut self) -> ChainStoreUpdate<'_> { ChainStoreUpdate::new(self) } diff --git a/chain/chunks/src/shards_manager_actor.rs b/chain/chunks/src/shards_manager_actor.rs index 6ba7a2a3c8b..6829b9f3ee2 100644 --- a/chain/chunks/src/shards_manager_actor.rs +++ b/chain/chunks/src/shards_manager_actor.rs @@ -95,7 +95,6 @@ use near_async::messaging::{self, Handler, Sender}; use near_async::time::Duration; use near_async::time::{self, Clock}; use near_chain::byzantine_assert; -use near_chain::chunks_store::ReadOnlyChunksStore; use near_chain::near_chain_primitives::error::Error::DBNotFoundErr; use near_chain::types::EpochManagerAdapter; use near_chain_configs::MutableValidatorSigner; @@ -129,6 +128,8 @@ use near_primitives::unwrap_or_return; use near_primitives::utils::MaybeValidated; use near_primitives::validator_signer::ValidatorSigner; use near_primitives::version::{ProtocolFeature, ProtocolVersion}; +use near_store::adapter::chunk_store::ChunkStoreAdapter; +use near_store::adapter::StoreAdapter; use near_store::{DBCol, Store, HEADER_HEAD_KEY, HEAD_KEY}; use rand::seq::IteratorRandom; use rand::Rng; @@ -248,7 +249,7 @@ pub struct ShardsManagerActor { /// Lock the value of mutable validator signer for the duration of a request to ensure consistency. /// Please note that the locked value should not be stored anywhere or passed through the thread boundary. validator_signer: MutableValidatorSigner, - store: ReadOnlyChunksStore, + store: ChunkStoreAdapter, /// Epoch manager used to access information about recent epochs (from Hot storage). /// For building PartialChunks from Chunks of the garbage collected epochs, use `view_epoch_manager` instead. @@ -318,7 +319,6 @@ pub fn start_shards_manager( .get_ser::(DBCol::BlockMisc, HEADER_HEAD_KEY) .unwrap() .expect("ShardsManager must be initialized after the chain is initialized"); - let chunks_store = ReadOnlyChunksStore::new(store); let shards_manager = ShardsManagerActor::new( Clock::real(), validator_signer, @@ -327,7 +327,7 @@ pub fn start_shards_manager( shard_tracker, network_adapter, client_adapter_for_shards_manager, - chunks_store, + store.chunk_store(), chain_head, chain_header_head, chunk_request_retry_period, @@ -349,7 +349,7 @@ impl ShardsManagerActor { shard_tracker: ShardTracker, network_adapter: Sender, client_adapter: Sender, - store: ReadOnlyChunksStore, + store: ChunkStoreAdapter, initial_chain_head: Tip, initial_chain_header_head: Tip, chunk_request_retry_period: Duration, @@ -2310,7 +2310,7 @@ mod test { shard_tracker, network_adapter.as_sender(), client_adapter.as_sender(), - ReadOnlyChunksStore::new(store), + store.chunk_store(), mock_tip.clone(), mock_tip, Duration::hours(1), @@ -2355,7 +2355,7 @@ mod test { fixture.shard_tracker.clone(), fixture.mock_network.as_sender(), fixture.mock_client_adapter.as_sender(), - fixture.chain_store.new_read_only_chunks_store(), + fixture.store.clone(), fixture.mock_chain_head.clone(), fixture.mock_chain_head.clone(), Duration::hours(1), @@ -2436,7 +2436,7 @@ mod test { fixture.shard_tracker.clone(), fixture.mock_network.as_sender(), fixture.mock_client_adapter.as_sender(), - fixture.chain_store.new_read_only_chunks_store(), + fixture.store.clone(), fixture.mock_chain_head.clone(), fixture.mock_chain_head.clone(), Duration::hours(1), @@ -2468,7 +2468,7 @@ mod test { fixture.shard_tracker.clone(), fixture.mock_network.as_sender(), fixture.mock_client_adapter.as_sender(), - fixture.chain_store.new_read_only_chunks_store(), + fixture.store.clone(), fixture.mock_chain_head.clone(), fixture.mock_chain_head.clone(), Duration::hours(1), @@ -2551,7 +2551,7 @@ mod test { fixture.shard_tracker.clone(), fixture.mock_network.as_sender(), fixture.mock_client_adapter.as_sender(), - fixture.chain_store.new_read_only_chunks_store(), + fixture.store.clone(), fixture.mock_chain_head.clone(), fixture.mock_chain_head.clone(), Duration::hours(1), @@ -2642,7 +2642,7 @@ mod test { fixture.shard_tracker.clone(), fixture.mock_network.as_sender(), fixture.mock_client_adapter.as_sender(), - fixture.chain_store.new_read_only_chunks_store(), + fixture.store.clone(), fixture.mock_chain_head.clone(), fixture.mock_chain_head.clone(), Duration::hours(1), @@ -2719,7 +2719,7 @@ mod test { fixture.shard_tracker.clone(), fixture.mock_network.as_sender(), fixture.mock_client_adapter.as_sender(), - fixture.chain_store.new_read_only_chunks_store(), + fixture.store.clone(), fixture.mock_chain_head.clone(), fixture.mock_chain_head.clone(), Duration::hours(1), @@ -2788,7 +2788,7 @@ mod test { fixture.shard_tracker.clone(), fixture.mock_network.as_sender(), fixture.mock_client_adapter.as_sender(), - fixture.chain_store.new_read_only_chunks_store(), + fixture.store.clone(), fixture.mock_chain_head.clone(), fixture.mock_chain_head.clone(), Duration::hours(1), @@ -2825,7 +2825,7 @@ mod test { fixture.shard_tracker.clone(), fixture.mock_network.as_sender(), fixture.mock_client_adapter.as_sender(), - fixture.chain_store.new_read_only_chunks_store(), + fixture.store.clone(), fixture.mock_chain_head.clone(), fixture.mock_chain_head.clone(), Duration::hours(1), @@ -2859,7 +2859,7 @@ mod test { fixture.shard_tracker.clone(), fixture.mock_network.as_sender(), fixture.mock_client_adapter.as_sender(), - fixture.chain_store.new_read_only_chunks_store(), + fixture.store.clone(), fixture.mock_chain_head.clone(), fixture.mock_chain_head.clone(), Duration::hours(1), @@ -2893,7 +2893,7 @@ mod test { fixture.shard_tracker.clone(), fixture.mock_network.as_sender(), fixture.mock_client_adapter.as_sender(), - fixture.chain_store.new_read_only_chunks_store(), + fixture.store.clone(), fixture.mock_chain_head.clone(), fixture.mock_chain_head.clone(), Duration::hours(1), @@ -2928,7 +2928,7 @@ mod test { fixture.shard_tracker.clone(), fixture.mock_network.as_sender(), fixture.mock_client_adapter.as_sender(), - fixture.chain_store.new_read_only_chunks_store(), + fixture.store.clone(), fixture.mock_chain_head.clone(), fixture.mock_chain_head.clone(), Duration::hours(1), @@ -2972,7 +2972,7 @@ mod test { fixture.shard_tracker.clone(), fixture.mock_network.as_sender(), fixture.mock_client_adapter.as_sender(), - fixture.chain_store.new_read_only_chunks_store(), + fixture.store.clone(), fixture.mock_chain_head.clone(), fixture.mock_chain_head.clone(), Duration::hours(1), @@ -3020,7 +3020,7 @@ mod test { fixture.shard_tracker.clone(), fixture.mock_network.as_sender(), fixture.mock_client_adapter.as_sender(), - fixture.chain_store.new_read_only_chunks_store(), + fixture.store.clone(), fixture.mock_chain_head.clone(), fixture.mock_chain_head.clone(), Duration::hours(1), @@ -3066,7 +3066,7 @@ mod test { fixture.shard_tracker.clone(), fixture.mock_network.as_sender(), fixture.mock_client_adapter.as_sender(), - fixture.chain_store.new_read_only_chunks_store(), + fixture.store.clone(), fixture.mock_chain_head.clone(), fixture.mock_chain_head.clone(), Duration::hours(1), @@ -3092,7 +3092,7 @@ mod test { fixture.shard_tracker.clone(), fixture.mock_network.as_sender(), fixture.mock_client_adapter.as_sender(), - fixture.chain_store.new_read_only_chunks_store(), + fixture.store.clone(), fixture.mock_chain_head.clone(), fixture.mock_chain_head.clone(), Duration::hours(1), @@ -3118,7 +3118,7 @@ mod test { fixture.shard_tracker.clone(), fixture.mock_network.as_sender(), fixture.mock_client_adapter.as_sender(), - fixture.chain_store.new_read_only_chunks_store(), + fixture.store.clone(), fixture.mock_chain_head.clone(), fixture.mock_chain_head.clone(), Duration::hours(1), @@ -3154,7 +3154,7 @@ mod test { fixture.shard_tracker.clone(), fixture.mock_network.as_sender(), fixture.mock_client_adapter.as_sender(), - fixture.chain_store.new_read_only_chunks_store(), + fixture.store.clone(), fixture.mock_chain_head.clone(), fixture.mock_chain_head.clone(), Duration::hours(1), @@ -3188,7 +3188,7 @@ mod test { fixture.shard_tracker.clone(), fixture.mock_network.as_sender(), fixture.mock_client_adapter.as_sender(), - fixture.chain_store.new_read_only_chunks_store(), + fixture.store.clone(), fixture.mock_chain_head.clone(), fixture.mock_chain_head.clone(), Duration::hours(1), diff --git a/chain/chunks/src/test_utils.rs b/chain/chunks/src/test_utils.rs index 4a2e18d2140..4415b467a6d 100644 --- a/chain/chunks/src/test_utils.rs +++ b/chain/chunks/src/test_utils.rs @@ -18,8 +18,9 @@ use near_primitives::test_utils::create_test_signer; use near_primitives::types::MerkleHash; use near_primitives::types::{AccountId, EpochId, ShardId}; use near_primitives::version::{ProtocolFeature, PROTOCOL_VERSION}; +use near_store::adapter::chunk_store::ChunkStoreAdapter; +use near_store::adapter::StoreAdapter; use near_store::test_utils::create_test_store; -use near_store::Store; use reed_solomon_erasure::galois_8::ReedSolomon; use std::collections::VecDeque; use std::sync::{Arc, Mutex, RwLock}; @@ -29,7 +30,7 @@ use crate::client::ShardsManagerResponse; use crate::shards_manager_actor::ShardsManagerActor; pub struct ChunkTestFixture { - pub store: Store, + pub store: ChunkStoreAdapter, pub epoch_manager: EpochManagerHandle, pub shard_tracker: ShardTracker, pub mock_network: Arc, @@ -175,7 +176,7 @@ impl ChunkTestFixture { let chain_store = ChainStore::new(store.clone(), 0, true); ChunkTestFixture { - store, + store: store.chunk_store(), epoch_manager, shard_tracker, mock_network, diff --git a/chain/client/src/test_utils/setup.rs b/chain/client/src/test_utils/setup.rs index 201f176e7f6..c6c669dc8c5 100644 --- a/chain/client/src/test_utils/setup.rs +++ b/chain/client/src/test_utils/setup.rs @@ -59,6 +59,7 @@ use near_primitives::test_utils::create_test_signer; use near_primitives::types::{AccountId, BlockHeightDelta, EpochId, NumBlocks, NumSeats}; use near_primitives::validator_signer::{EmptyValidatorSigner, ValidatorSigner}; use near_primitives::version::PROTOCOL_VERSION; +use near_store::adapter::StoreAdapter; use near_store::test_utils::create_test_store; use near_telemetry::TelemetryActor; use num_rational::Ratio; @@ -1049,6 +1050,7 @@ pub fn setup_synchronous_shards_manager( // ShardsManager. This way we don't have to wait to construct the Client first. // TODO(#8324): This should just be refactored so that we can construct Chain first // before anything else. + let chunk_store = runtime.store().chunk_store(); let chain = Chain::new( clock.clone(), epoch_manager.clone(), @@ -1080,7 +1082,7 @@ pub fn setup_synchronous_shards_manager( shard_tracker, network_adapter.request_sender, client_adapter, - chain.chain_store().new_read_only_chunks_store(), + chunk_store, chain_head, chain_header_head, Duration::hours(1), diff --git a/core/primitives/src/errors.rs b/core/primitives/src/errors.rs index 33d9699de7b..1f437d1f3ad 100644 --- a/core/primitives/src/errors.rs +++ b/core/primitives/src/errors.rs @@ -1,5 +1,6 @@ use crate::hash::CryptoHash; use crate::serialize::dec_format; +use crate::sharding::ChunkHash; use crate::types::{AccountId, Balance, EpochId, Gas, Nonce}; use borsh::{BorshDeserialize, BorshSerialize}; use near_crypto::PublicKey; @@ -1255,3 +1256,16 @@ pub enum FunctionCallError { _EVMError, ExecutionError(String), } + +#[derive(Debug)] +pub enum ChunkAccessError { + ChunkMissing(ChunkHash), +} + +impl std::fmt::Display for ChunkAccessError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.write_str(&format!("{:?}", self)) + } +} + +impl std::error::Error for ChunkAccessError {} diff --git a/core/store/src/adapter/chunk_store.rs b/core/store/src/adapter/chunk_store.rs new file mode 100644 index 00000000000..e3b8a3cf294 --- /dev/null +++ b/core/store/src/adapter/chunk_store.rs @@ -0,0 +1,42 @@ +use std::sync::Arc; + +use near_primitives::errors::ChunkAccessError; +use near_primitives::sharding::{ChunkHash, PartialEncodedChunk, ShardChunk}; + +use crate::{DBCol, Store}; + +use super::StoreAdapter; + +#[derive(Clone)] +pub struct ChunkStoreAdapter { + store: Store, +} + +impl StoreAdapter for ChunkStoreAdapter { + fn store(&self) -> Store { + self.store.clone() + } +} + +impl ChunkStoreAdapter { + pub fn new(store: Store) -> Self { + Self { store } + } + + pub fn get_partial_chunk( + &self, + chunk_hash: &ChunkHash, + ) -> Result, ChunkAccessError> { + self.store + .get_ser(DBCol::PartialChunks, chunk_hash.as_ref()) + .expect("Borsh should not have failed here") + .ok_or_else(|| ChunkAccessError::ChunkMissing(chunk_hash.clone())) + } + + pub fn get_chunk(&self, chunk_hash: &ChunkHash) -> Result, ChunkAccessError> { + self.store + .get_ser(DBCol::Chunks, chunk_hash.as_ref()) + .expect("Borsh should not have failed here") + .ok_or_else(|| ChunkAccessError::ChunkMissing(chunk_hash.clone())) + } +} diff --git a/core/store/src/adapter/mod.rs b/core/store/src/adapter/mod.rs index 4475d866489..a984a2237f1 100644 --- a/core/store/src/adapter/mod.rs +++ b/core/store/src/adapter/mod.rs @@ -1,3 +1,4 @@ +pub mod chunk_store; pub mod flat_store; use std::ops::{Deref, DerefMut}; @@ -88,6 +89,10 @@ pub trait StoreAdapter { fn flat_store(&self) -> flat_store::FlatStoreAdapter { flat_store::FlatStoreAdapter::new(self.store()) } + + fn chunk_store(&self) -> chunk_store::ChunkStoreAdapter { + chunk_store::ChunkStoreAdapter::new(self.store()) + } } /// Simple adapter wrapper on top of StoreUpdate to provide a more ergonomic interface for diff --git a/integration-tests/src/test_loop/builder.rs b/integration-tests/src/test_loop/builder.rs index 6559afd669d..087866a89ab 100644 --- a/integration-tests/src/test_loop/builder.rs +++ b/integration-tests/src/test_loop/builder.rs @@ -6,7 +6,6 @@ use near_async::messaging::{noop, IntoMultiSender, IntoSender, LateBoundSender}; use near_async::test_loop::sender::TestLoopSender; use near_async::test_loop::TestLoopV2; use near_async::time::{Clock, Duration}; -use near_chain::chunks_store::ReadOnlyChunksStore; use near_chain::runtime::NightshadeRuntime; use near_chain::state_snapshot_actor::{ get_delete_snapshot_callback, get_make_snapshot_callback, SnapshotCallbacks, StateSnapshotActor, @@ -30,6 +29,7 @@ use near_parameters::RuntimeConfigStore; use near_primitives::network::PeerId; use near_primitives::test_utils::create_test_signer; use near_primitives::types::AccountId; +use near_store::adapter::StoreAdapter; use near_store::config::StateSnapshotType; use near_store::genesis::initialize_genesis_state; use near_store::test_utils::{create_test_split_store, create_test_store}; @@ -417,7 +417,7 @@ impl TestLoopBuilder { shard_tracker.clone(), network_adapter.as_sender(), client_adapter.as_sender(), - ReadOnlyChunksStore::new(split_store.as_ref().unwrap_or(&store).clone()), + store.chunk_store(), client.chain.head().unwrap(), client.chain.header_head().unwrap(), Duration::milliseconds(100), diff --git a/integration-tests/src/test_loop/tests/simple_test_loop_example.rs b/integration-tests/src/test_loop/tests/simple_test_loop_example.rs index fac35dfb843..17da7fca629 100644 --- a/integration-tests/src/test_loop/tests/simple_test_loop_example.rs +++ b/integration-tests/src/test_loop/tests/simple_test_loop_example.rs @@ -1,7 +1,6 @@ use near_async::messaging::{noop, IntoMultiSender, IntoSender, LateBoundSender}; use near_async::test_loop::TestLoopV2; use near_async::time::Duration; -use near_chain::chunks_store::ReadOnlyChunksStore; use near_chain::ChainGenesis; use near_chain_configs::test_genesis::TestGenesisBuilder; use near_chain_configs::{ClientConfig, MutableConfigValue}; @@ -17,6 +16,7 @@ use near_primitives::network::PeerId; use near_primitives::test_utils::create_test_signer; use near_primitives::types::AccountId; +use near_store::adapter::StoreAdapter; use crate::test_loop::utils::ONE_NEAR; use near_store::genesis::initialize_genesis_state; @@ -116,7 +116,7 @@ fn test_client_with_simple_test_loop() { shard_tracker, noop().into_sender(), client_adapter.as_sender(), - ReadOnlyChunksStore::new(store), + store.chunk_store(), client.chain.head().unwrap(), client.chain.header_head().unwrap(), Duration::milliseconds(100), From ab133e789066cebbbd6605f07780b22b64d8fad8 Mon Sep 17 00:00:00 2001 From: Shreyan Gupta Date: Thu, 26 Sep 2024 12:37:10 -0700 Subject: [PATCH 31/49] [fix] Fix Lychee lint (#12157) -_- --- docs/architecture/gas/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/gas/README.md b/docs/architecture/gas/README.md index e03a625d2cb..fc80e20d4c6 100644 --- a/docs/architecture/gas/README.md +++ b/docs/architecture/gas/README.md @@ -37,7 +37,7 @@ good places to dig deeper. For timing to make sense at all, we must first define hardware constraints. The official hardware requirements for a validator are published on -[near-nodes.io/validator/hardware](https://near-nodes.io/validator/hardware). They +[near-nodes.io/validator/hardware-validator](https://near-nodes.io/validator/hardware-validator). They may change over time but the main principle is that a moderately configured, cloud-hosted virtual machine suffices. From e5aa208ad53812422f9e89d8adeb926dd373118b Mon Sep 17 00:00:00 2001 From: Shreyan Gupta Date: Thu, 26 Sep 2024 13:36:32 -0700 Subject: [PATCH 32/49] [store] Introduce Trie store adapter (#12146) PR in the series to move to store adapter. Follow up on https://github.com/near/nearcore/pull/12123 --- chain/chain/src/chain.rs | 4 +- chain/chain/src/flat_storage_creator.rs | 4 +- chain/chain/src/garbage_collection.rs | 11 +- chain/chain/src/runtime/mod.rs | 2 +- chain/chain/src/runtime/tests.rs | 4 +- chain/chain/src/store/mod.rs | 11 +- core/store/src/adapter/mod.rs | 9 ++ core/store/src/adapter/trie_store.rs | 153 ++++++++++++++++++ core/store/src/genesis/initialization.rs | 2 +- core/store/src/lib.rs | 11 -- core/store/src/test_utils.rs | 4 +- core/store/src/trie/from_flat.rs | 4 +- core/store/src/trie/mem/loading.rs | 25 +-- core/store/src/trie/mem/parallel_loader.rs | 49 ++---- core/store/src/trie/mod.rs | 6 +- .../src/trie/prefetching_trie_storage.rs | 34 ++-- core/store/src/trie/resharding_v2.rs | 15 +- core/store/src/trie/shard_tries.rs | 118 ++++---------- core/store/src/trie/state_parts.rs | 5 +- core/store/src/trie/state_snapshot.rs | 58 +++---- core/store/src/trie/trie_recording.rs | 20 ++- core/store/src/trie/trie_storage.rs | 35 +--- core/store/src/trie/trie_tests.rs | 10 +- .../src/tests/client/state_snapshot.rs | 17 +- nearcore/src/entity_debug.rs | 29 ++-- nearcore/src/metrics.rs | 3 +- .../src/estimator_context.rs | 8 +- runtime/runtime/src/prefetch.rs | 2 +- tools/database/src/analyze_contract_sizes.rs | 3 +- tools/database/src/analyze_delayed_receipt.rs | 2 +- tools/database/src/state_perf.rs | 2 +- tools/fork-network/src/cli.rs | 2 +- .../src/single_shard_storage_mutator.rs | 2 +- tools/state-viewer/src/apply_chain_range.rs | 2 +- tools/state-viewer/src/cli.rs | 5 +- tools/state-viewer/src/commands.rs | 21 ++- .../src/trie_iteration_benchmark.rs | 3 +- 37 files changed, 358 insertions(+), 337 deletions(-) create mode 100644 core/store/src/adapter/trie_store.rs diff --git a/chain/chain/src/chain.rs b/chain/chain/src/chain.rs index f2e8be752f5..12f31d34d7d 100644 --- a/chain/chain/src/chain.rs +++ b/chain/chain/src/chain.rs @@ -90,7 +90,7 @@ use near_primitives::views::{ FinalExecutionOutcomeView, FinalExecutionOutcomeWithReceiptView, FinalExecutionStatus, LightClientBlockView, SignedTransactionView, }; -use near_store::adapter::StoreUpdateAdapter; +use near_store::adapter::{StoreAdapter, StoreUpdateAdapter}; use near_store::config::StateSnapshotType; use near_store::flat::{FlatStorageReadyStatus, FlatStorageStatus}; use near_store::trie::mem::resharding::RetainMode; @@ -1924,7 +1924,7 @@ impl Chain { ); chain_store_update.commit()?; - let mut store_update = self.chain_store.store().store_update(); + let mut store_update = self.chain_store.store().trie_store().store_update(); tries.apply_insertions(&trie_changes, new_shard_uid, &mut store_update); store_update.commit()?; } diff --git a/chain/chain/src/flat_storage_creator.rs b/chain/chain/src/flat_storage_creator.rs index edda5a5e6b9..e26e721d90b 100644 --- a/chain/chain/src/flat_storage_creator.rs +++ b/chain/chain/src/flat_storage_creator.rs @@ -96,7 +96,7 @@ impl FlatStorageShardCreator { progress: Arc, result_sender: Sender, ) { - let trie_storage = TrieDBStorage::new(store.store(), shard_uid); + let trie_storage = TrieDBStorage::new(store.trie_store(), shard_uid); let trie = Trie::new(Arc::new(trie_storage), state_root, None); let path_begin = trie.find_state_part_boundary(part_id.idx, part_id.total).unwrap(); let path_end = trie.find_state_part_boundary(part_id.idx + 1, part_id.total).unwrap(); @@ -186,7 +186,7 @@ impl FlatStorageShardCreator { let block_hash = final_head.last_block_hash; let epoch_id = self.epoch_manager.get_epoch_id(&block_hash)?; let shard_uid = self.epoch_manager.shard_id_to_uid(shard_id, &epoch_id)?; - let trie_storage = TrieDBStorage::new(store.store(), shard_uid); + let trie_storage = TrieDBStorage::new(store.trie_store(), shard_uid); let state_root = *chain_store.get_chunk_extra(&block_hash, &shard_uid)?.state_root(); let trie = Trie::new(Arc::new(trie_storage), state_root, None); diff --git a/chain/chain/src/garbage_collection.rs b/chain/chain/src/garbage_collection.rs index 1b08236231e..876617fb357 100644 --- a/chain/chain/src/garbage_collection.rs +++ b/chain/chain/src/garbage_collection.rs @@ -11,7 +11,7 @@ use near_primitives::shard_layout::get_block_shard_uid; use near_primitives::state_sync::{StateHeaderKey, StatePartKey}; use near_primitives::types::{BlockHeight, BlockHeightDelta, EpochId, NumBlocks, ShardId}; use near_primitives::utils::{get_block_shard_id, get_outcome_id_block_hash, index_to_bytes}; -use near_store::adapter::StoreUpdateAdapter; +use near_store::adapter::{StoreAdapter, StoreUpdateAdapter}; use near_store::{DBCol, KeyForStateChanges, ShardTries, ShardUId}; use crate::types::RuntimeAdapter; @@ -382,12 +382,11 @@ impl ChainStore { chain_store_update.commit()?; // clear all trie data - let tries = runtime_adapter.get_tries(); let mut chain_store_update = self.store_update(); let mut store_update = tries.store_update(); - store_update.delete_all(DBCol::State); - chain_store_update.merge(store_update); + store_update.delete_all_state(); + chain_store_update.merge(store_update.into()); // The reason to reset tail here is not to allow Tail be greater than Head chain_store_update.reset_tail(); @@ -529,7 +528,7 @@ impl<'a> ChainStoreUpdate<'a> { mut block_hash: CryptoHash, gc_mode: GCMode, ) -> Result<(), Error> { - let mut store_update = self.store().store_update(); + let mut store_update = self.store().trie_store().store_update(); tracing::debug!(target: "garbage_collection", ?gc_mode, ?block_hash, "GC block_hash"); @@ -661,7 +660,7 @@ impl<'a> ChainStoreUpdate<'a> { // Chunks deleted separately } }; - self.merge(store_update); + self.merge(store_update.into()); Ok(()) } diff --git a/chain/chain/src/runtime/mod.rs b/chain/chain/src/runtime/mod.rs index 1fd28b915fe..9bbfe219368 100644 --- a/chain/chain/src/runtime/mod.rs +++ b/chain/chain/src/runtime/mod.rs @@ -103,7 +103,7 @@ impl NightshadeRuntime { let flat_storage_manager = FlatStorageManager::new(store.flat_store()); let shard_uids: Vec<_> = genesis_config.shard_layout.shard_uids().collect(); let tries = ShardTries::new( - store.clone(), + store.trie_store(), trie_config, &shard_uids, flat_storage_manager, diff --git a/chain/chain/src/runtime/tests.rs b/chain/chain/src/runtime/tests.rs index 47d25c449cd..494778e2b7d 100644 --- a/chain/chain/src/runtime/tests.rs +++ b/chain/chain/src/runtime/tests.rs @@ -279,8 +279,8 @@ impl TestEnv { let mut store_update = self.runtime.store().store_update(); let flat_state_changes = FlatStateChanges::from_state_changes(&apply_result.trie_changes.state_changes()); - apply_result.trie_changes.insertions_into(&mut store_update); - apply_result.trie_changes.state_changes_into(&mut store_update); + apply_result.trie_changes.insertions_into(&mut store_update.trie_store_update()); + apply_result.trie_changes.state_changes_into(&mut store_update.trie_store_update()); let prev_block_hash = self.head.last_block_hash; let epoch_id = diff --git a/chain/chain/src/store/mod.rs b/chain/chain/src/store/mod.rs index 619a800bdfb..877b0c1f375 100644 --- a/chain/chain/src/store/mod.rs +++ b/chain/chain/src/store/mod.rs @@ -40,6 +40,7 @@ use near_primitives::utils::{ }; use near_primitives::version::ProtocolVersion; use near_primitives::views::LightClientBlockView; +use near_store::adapter::{StoreAdapter, StoreUpdateAdapter}; use near_store::{ DBCol, KeyForStateChanges, PartialStorage, Store, StoreUpdate, WrappedTrieChanges, CHUNK_TAIL_KEY, FINAL_HEAD_KEY, FORK_TAIL_KEY, HEADER_HEAD_KEY, HEAD_KEY, @@ -2462,17 +2463,15 @@ impl<'a> ChainStoreUpdate<'a> { // from the store. { let _span = tracing::trace_span!(target: "store", "write_trie_changes").entered(); - let mut deletions_store_update = self.store().store_update(); + let mut deletions_store_update = self.store().trie_store().store_update(); for mut wrapped_trie_changes in self.trie_changes.drain(..) { wrapped_trie_changes.apply_mem_changes(); - wrapped_trie_changes.insertions_into(&mut store_update); + wrapped_trie_changes.insertions_into(&mut store_update.trie_store_update()); wrapped_trie_changes.deletions_into(&mut deletions_store_update); - wrapped_trie_changes.state_changes_into(&mut store_update); + wrapped_trie_changes.state_changes_into(&mut store_update.trie_store_update()); if self.chain_store.save_trie_changes { - wrapped_trie_changes - .trie_changes_into(&mut store_update) - .map_err(|err| Error::Other(err.to_string()))?; + wrapped_trie_changes.trie_changes_into(&mut store_update.trie_store_update()); } } diff --git a/core/store/src/adapter/mod.rs b/core/store/src/adapter/mod.rs index a984a2237f1..ba31d691775 100644 --- a/core/store/src/adapter/mod.rs +++ b/core/store/src/adapter/mod.rs @@ -1,5 +1,6 @@ pub mod chunk_store; pub mod flat_store; +pub mod trie_store; use std::ops::{Deref, DerefMut}; @@ -86,6 +87,10 @@ impl Into for StoreUpdateHolder<'static> { pub trait StoreAdapter { fn store(&self) -> Store; + fn trie_store(&self) -> trie_store::TrieStoreAdapter { + trie_store::TrieStoreAdapter::new(self.store()) + } + fn flat_store(&self) -> flat_store::FlatStoreAdapter { flat_store::FlatStoreAdapter::new(self.store()) } @@ -103,6 +108,10 @@ pub trait StoreAdapter { pub trait StoreUpdateAdapter: Sized { fn store_update(&mut self) -> &mut StoreUpdate; + fn trie_store_update(&mut self) -> trie_store::TrieStoreUpdateAdapter { + trie_store::TrieStoreUpdateAdapter::new(self.store_update()) + } + fn flat_store_update(&mut self) -> flat_store::FlatStoreUpdateAdapter { flat_store::FlatStoreUpdateAdapter::new(self.store_update()) } diff --git a/core/store/src/adapter/trie_store.rs b/core/store/src/adapter/trie_store.rs new file mode 100644 index 00000000000..e188cddc4f1 --- /dev/null +++ b/core/store/src/adapter/trie_store.rs @@ -0,0 +1,153 @@ +use std::io; +use std::num::NonZero; +use std::sync::Arc; + +use near_primitives::errors::{MissingTrieValueContext, StorageError}; +use near_primitives::hash::CryptoHash; +use near_primitives::shard_layout::{get_block_shard_uid, ShardUId}; +use near_primitives::types::RawStateChangesWithTrieKey; + +use crate::{DBCol, KeyForStateChanges, Store, StoreUpdate, TrieChanges, STATE_SNAPSHOT_KEY}; + +use super::{StoreAdapter, StoreUpdateAdapter, StoreUpdateHolder}; + +#[derive(Clone)] +pub struct TrieStoreAdapter { + store: Store, +} + +impl StoreAdapter for TrieStoreAdapter { + fn store(&self) -> Store { + self.store.clone() + } +} + +impl TrieStoreAdapter { + pub fn new(store: Store) -> Self { + Self { store } + } + + pub fn store_update(&self) -> TrieStoreUpdateAdapter<'static> { + TrieStoreUpdateAdapter { store_update: StoreUpdateHolder::Owned(self.store.store_update()) } + } + + pub fn get(&self, shard_uid: ShardUId, hash: &CryptoHash) -> Result, StorageError> { + let key = get_key_from_shard_uid_and_hash(shard_uid, hash); + let val = self + .store + .get(DBCol::State, key.as_ref()) + .map_err(|_| StorageError::StorageInternalError)? + .ok_or(StorageError::MissingTrieValue(MissingTrieValueContext::TrieStorage, *hash))?; + Ok(val.into()) + } + + pub fn get_state_snapshot_hash(&self) -> Result { + let val = self + .store + .get_ser(DBCol::BlockMisc, STATE_SNAPSHOT_KEY) + .map_err(|_| StorageError::StorageInternalError)? + .ok_or(StorageError::StorageInternalError)?; + Ok(val) + } + + #[cfg(test)] + pub fn iter_raw_bytes(&self) -> crate::db::DBIterator { + self.store.iter_raw_bytes(DBCol::State) + } +} + +pub struct TrieStoreUpdateAdapter<'a> { + store_update: StoreUpdateHolder<'a>, +} + +impl Into for TrieStoreUpdateAdapter<'static> { + fn into(self) -> StoreUpdate { + self.store_update.into() + } +} + +impl TrieStoreUpdateAdapter<'static> { + pub fn commit(self) -> io::Result<()> { + let store_update: StoreUpdate = self.into(); + store_update.commit() + } +} + +impl<'a> StoreUpdateAdapter for TrieStoreUpdateAdapter<'a> { + fn store_update(&mut self) -> &mut StoreUpdate { + &mut self.store_update + } +} + +impl<'a> TrieStoreUpdateAdapter<'a> { + pub fn new(store_update: &'a mut StoreUpdate) -> Self { + Self { store_update: StoreUpdateHolder::Reference(store_update) } + } + + pub fn decrement_refcount_by( + &mut self, + shard_uid: ShardUId, + hash: &CryptoHash, + decrement: NonZero, + ) { + let key = get_key_from_shard_uid_and_hash(shard_uid, hash); + self.store_update.decrement_refcount_by(DBCol::State, key.as_ref(), decrement); + } + + pub fn decrement_refcount(&mut self, shard_uid: ShardUId, hash: &CryptoHash) { + let key = get_key_from_shard_uid_and_hash(shard_uid, hash); + self.store_update.decrement_refcount(DBCol::State, key.as_ref()); + } + + pub fn increment_refcount_by( + &mut self, + shard_uid: ShardUId, + hash: &CryptoHash, + data: &[u8], + decrement: NonZero, + ) { + let key = get_key_from_shard_uid_and_hash(shard_uid, hash); + self.store_update.increment_refcount_by(DBCol::State, key.as_ref(), data, decrement); + } + + pub fn set_state_snapshot_hash(&mut self, hash: Option) { + let key = STATE_SNAPSHOT_KEY; + match hash { + Some(hash) => self.store_update.set_ser(DBCol::BlockMisc, key, &hash).unwrap(), + None => self.store_update.delete(DBCol::BlockMisc, key), + } + } + + pub fn set_trie_changes( + &mut self, + shard_uid: ShardUId, + block_hash: &CryptoHash, + trie_changes: &TrieChanges, + ) { + let key = get_block_shard_uid(block_hash, &shard_uid); + self.store_update.set_ser(DBCol::TrieChanges, &key, trie_changes).unwrap(); + } + + pub fn set_state_changes( + &mut self, + key: KeyForStateChanges, + value: &RawStateChangesWithTrieKey, + ) { + self.store_update.set( + DBCol::StateChanges, + key.as_ref(), + &borsh::to_vec(&value).expect("Borsh serialize cannot fail"), + ) + } + + pub fn delete_all_state(&mut self) { + self.store_update.delete_all(DBCol::State) + } +} + +pub fn get_key_from_shard_uid_and_hash(shard_uid: ShardUId, hash: &CryptoHash) -> [u8; 40] { + let mut key = [0; 40]; + key[0..8].copy_from_slice(&shard_uid.to_bytes()); + key[8..].copy_from_slice(hash.as_ref()); + key +} diff --git a/core/store/src/genesis/initialization.rs b/core/store/src/genesis/initialization.rs index fcf77179a74..bc9b1c9a65c 100644 --- a/core/store/src/genesis/initialization.rs +++ b/core/store/src/genesis/initialization.rs @@ -129,7 +129,7 @@ fn genesis_state_from_genesis( }); assert!(has_protocol_account, "Genesis spec doesn't have protocol treasury account"); let tries = ShardTries::new( - store.clone(), + store.trie_store(), TrieConfig::default(), &shard_uids, FlatStorageManager::new(store.flat_store()), diff --git a/core/store/src/lib.rs b/core/store/src/lib.rs index cec9740cfdd..3804b60a47b 100644 --- a/core/store/src/lib.rs +++ b/core/store/src/lib.rs @@ -1084,17 +1084,6 @@ pub fn set_genesis_congestion_infos( .expect("Borsh cannot fail"); } -fn option_to_not_found(res: io::Result>, field_name: F) -> io::Result -where - F: std::string::ToString, -{ - match res { - Ok(Some(o)) => Ok(o), - Ok(None) => Err(io::Error::new(io::ErrorKind::NotFound, field_name.to_string())), - Err(e) => Err(e), - } -} - #[derive(Clone)] pub struct StoreContractRuntimeCache { db: Arc, diff --git a/core/store/src/test_utils.rs b/core/store/src/test_utils.rs index 577294c66c5..70d0703a34d 100644 --- a/core/store/src/test_utils.rs +++ b/core/store/src/test_utils.rs @@ -124,7 +124,7 @@ impl TestTriesBuilder { .collect::>(); let flat_storage_manager = FlatStorageManager::new(store.flat_store()); let tries = ShardTries::new( - store.clone(), + store.trie_store(), TrieConfig { load_mem_tries_for_tracked_shards: self.enable_in_memory_tries, ..Default::default() @@ -218,7 +218,7 @@ pub fn test_populate_flat_storage( prev_block_hash: &CryptoHash, changes: &Vec<(Vec, Option>)>, ) { - let mut store_update = tries.get_store().flat_store().store_update(); + let mut store_update = tries.store().flat_store().store_update(); store_update.set_flat_storage_status( shard_uid, crate::flat::FlatStorageStatus::Ready(FlatStorageReadyStatus { diff --git a/core/store/src/trie/from_flat.rs b/core/store/src/trie/from_flat.rs index b476ee25492..97ccb717666 100644 --- a/core/store/src/trie/from_flat.rs +++ b/core/store/src/trie/from_flat.rs @@ -12,7 +12,7 @@ use std::time::Instant; // Please note that the trie is created for the block state with height equal to flat_head // flat state can comtain deltas after flat_head and can be different from tip of the blockchain. pub fn construct_trie_from_flat(store: Store, write_store: Store, shard_uid: ShardUId) { - let trie_storage = TrieDBStorage::new(store.clone(), shard_uid); + let trie_storage = TrieDBStorage::new(store.trie_store(), shard_uid); let flat_state_to_trie_kv = |entry: Result<(Vec, FlatStateValue), FlatStorageError>| -> (Vec, Vec) { let (key, value) = entry.unwrap(); @@ -30,7 +30,7 @@ pub fn construct_trie_from_flat(store: Store, write_store: Store, shard_uid: Sha // new ShardTries for write storage location let tries = ShardTries::new( - write_store.clone(), + write_store.trie_store(), TrieConfig::default(), &[shard_uid], FlatStorageManager::new(write_store.flat_store()), diff --git a/core/store/src/trie/mem/loading.rs b/core/store/src/trie/mem/loading.rs index 727fef5a6de..32994789d67 100644 --- a/core/store/src/trie/mem/loading.rs +++ b/core/store/src/trie/mem/loading.rs @@ -1,9 +1,8 @@ use super::arena::single_thread::STArena; use super::mem_tries::MemTries; use super::node::MemTrieNodeId; -use crate::adapter::flat_store::decode_flat_state_db_key; use crate::adapter::StoreAdapter; -use crate::flat::{FlatStorageError, FlatStorageStatus}; +use crate::flat::FlatStorageStatus; use crate::trie::mem::arena::Arena; use crate::trie::mem::construction::TrieConstructor; use crate::trie::mem::parallel_loader::load_memtrie_in_parallel; @@ -11,7 +10,6 @@ use crate::{DBCol, NibbleSlice, Store}; use near_primitives::errors::StorageError; use near_primitives::hash::CryptoHash; use near_primitives::shard_layout::{get_block_shard_uid, ShardUId}; -use near_primitives::state::FlatStateValue; use near_primitives::types::chunk_extra::ChunkExtra; use near_primitives::types::{BlockHeight, StateRoot}; use std::collections::BTreeSet; @@ -39,7 +37,7 @@ fn load_trie_from_flat_state( let (arena, root_id) = if parallelize { const NUM_PARALLEL_SUBTREES_DESIRED: usize = 256; load_memtrie_in_parallel( - store.clone(), + store.trie_store(), shard_uid, state_root, NUM_PARALLEL_SUBTREES_DESIRED, @@ -68,15 +66,8 @@ fn load_memtrie_single_thread( let mut arena = STArena::new(shard_uid.to_string()); let mut recon = TrieConstructor::new(&mut arena); let mut num_keys_loaded = 0; - for item in store - .iter_prefix_ser::(DBCol::FlatState, &borsh::to_vec(&shard_uid).unwrap()) - { - let (key, value) = item.map_err(|err| { - FlatStorageError::StorageInternalError(format!("Error iterating over FlatState: {err}")) - })?; - let (_, key) = decode_flat_state_db_key(&key).map_err(|err| { - FlatStorageError::StorageInternalError(format!("invalid FlatState key format: {err}")) - })?; + for item in store.flat_store().iter(shard_uid) { + let (key, value) = item?; recon.add_leaf(NibbleSlice::new(&key), value); num_keys_loaded += 1; if num_keys_loaded % 1000000 == 0 { @@ -187,7 +178,7 @@ pub fn load_trie_from_flat_state_and_delta( #[cfg(test)] mod tests { use super::load_trie_from_flat_state_and_delta; - use crate::adapter::StoreAdapter; + use crate::adapter::{StoreAdapter, StoreUpdateAdapter}; use crate::flat::test_utils::MockChain; use crate::flat::{BlockInfo, FlatStorageReadyStatus, FlatStorageStatus}; use crate::test_utils::{ @@ -225,7 +216,7 @@ mod tests { eprintln!("Trie and flat storage populated"); let in_memory_trie = load_trie_from_flat_state( - &shard_tries.get_store(), + &shard_tries.store().store(), shard_uid, state_root, 123, @@ -393,7 +384,7 @@ mod tests { let shard_uid = ShardUId { version: 1, shard_id: 1 }; // Populate the initial flat storage state at block 0. - let mut store_update = shard_tries.get_store().flat_store().store_update(); + let mut store_update = shard_tries.store().flat_store().store_update(); store_update.set_flat_storage_status( shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: chain.get_block(0) }), @@ -496,7 +487,7 @@ mod tests { let (_, trie_changes, state_changes) = trie_update.finalize().unwrap(); let mut store_update = tries.store_update(); tries.apply_insertions(&trie_changes, shard_uid, &mut store_update); - store_update.merge( + store_update.store_update().merge( tries .get_flat_storage_manager() .save_flat_state_changes( diff --git a/core/store/src/trie/mem/parallel_loader.rs b/core/store/src/trie/mem/parallel_loader.rs index 03945976434..e38254a3031 100644 --- a/core/store/src/trie/mem/parallel_loader.rs +++ b/core/store/src/trie/mem/parallel_loader.rs @@ -3,11 +3,13 @@ use super::arena::single_thread::STArena; use super::arena::ArenaMut; use super::construction::TrieConstructor; use super::node::{InputMemTrieNode, MemTrieNodeId}; +use crate::adapter::trie_store::TrieStoreAdapter; +use crate::adapter::StoreAdapter; use crate::flat::FlatStorageError; use crate::trie::Children; -use crate::{DBCol, NibbleSlice, RawTrieNode, RawTrieNodeWithSize, Store}; +use crate::{DBCol, NibbleSlice, RawTrieNode, RawTrieNodeWithSize}; use borsh::BorshDeserialize; -use near_primitives::errors::{MissingTrieValueContext, StorageError}; +use near_primitives::errors::StorageError; use near_primitives::hash::CryptoHash; use near_primitives::shard_layout::ShardUId; use near_primitives::state::FlatStateValue; @@ -18,7 +20,7 @@ use std::sync::Mutex; /// Top-level entry function to load a memtrie in parallel. pub fn load_memtrie_in_parallel( - store: Store, + store: TrieStoreAdapter, shard_uid: ShardUId, root: StateRoot, num_subtrees_desired: usize, @@ -45,7 +47,7 @@ pub fn load_memtrie_in_parallel( /// This loader is only suitable for loading a single trie. It does not load multiple state roots, /// or multiple shards. pub struct ParallelMemTrieLoader { - store: Store, + store: TrieStoreAdapter, shard_uid: ShardUId, root: StateRoot, num_subtrees_desired: usize, @@ -53,7 +55,7 @@ pub struct ParallelMemTrieLoader { impl ParallelMemTrieLoader { pub fn new( - store: Store, + store: TrieStoreAdapter, shard_uid: ShardUId, root: StateRoot, num_subtrees_desired: usize, @@ -87,18 +89,9 @@ impl ParallelMemTrieLoader { max_subtree_size: Option, ) -> Result { // Read the node from the State column. - let mut key = [0u8; 40]; - key[0..8].copy_from_slice(&self.shard_uid.to_bytes()); - key[8..40].copy_from_slice(&hash.0); - let node = RawTrieNodeWithSize::try_from_slice( - &self - .store - .get(DBCol::State, &key) - .map_err(|e| StorageError::StorageInconsistentState(e.to_string()))? - .ok_or(StorageError::MissingTrieValue(MissingTrieValueContext::TrieStorage, hash))? - .as_slice(), - ) - .map_err(|e| StorageError::StorageInconsistentState(e.to_string()))?; + let value = self.store.get(self.shard_uid, &hash)?; + let node = RawTrieNodeWithSize::try_from_slice(&value) + .map_err(|e| StorageError::StorageInconsistentState(e.to_string()))?; let max_subtree_size = max_subtree_size .unwrap_or_else(|| node.memory_usage / self.num_subtrees_desired as u64); @@ -117,15 +110,7 @@ impl ParallelMemTrieLoader { // almost like a corner case because we're not really interested in values here // (that's the job of the parallel loading part), but if we do get here, we have to // deal with it. - key[8..40].copy_from_slice(&value_ref.hash.0); - let value = self - .store - .get(DBCol::State, &key) - .map_err(|e| StorageError::StorageInconsistentState(e.to_string()))? - .ok_or(StorageError::MissingTrieValue( - MissingTrieValueContext::TrieStorage, - hash, - ))?; + let value = self.store.get(self.shard_uid, &value_ref.hash)?; let flat_value = FlatStateValue::on_disk(&value); Ok(TrieLoadingPlanNode::Leaf { extension: extension.into_boxed_slice(), @@ -145,15 +130,7 @@ impl ParallelMemTrieLoader { } RawTrieNode::BranchWithValue(value_ref, children_hashes) => { // Similar here, except we have to also look up the value. - key[8..40].copy_from_slice(&value_ref.hash.0); - let value = self - .store - .get(DBCol::State, &key) - .map_err(|e| StorageError::StorageInconsistentState(e.to_string()))? - .ok_or(StorageError::MissingTrieValue( - MissingTrieValueContext::TrieStorage, - hash, - ))?; + let value = self.store.get(self.shard_uid, &value_ref.hash)?; let flat_value = FlatStateValue::on_disk(&value); let children = self.make_children_plans_in_parallel( @@ -218,7 +195,7 @@ impl ParallelMemTrieLoader { // Load all the keys in this range from the FlatState column. let mut recon = TrieConstructor::new(arena); - for item in self.store.iter_range(DBCol::FlatState, Some(&start), Some(&end)) { + for item in self.store.store().iter_range(DBCol::FlatState, Some(&start), Some(&end)) { let (key, value) = item.map_err(|err| { FlatStorageError::StorageInternalError(format!( "Error iterating over FlatState: {err}" diff --git a/core/store/src/trie/mod.rs b/core/store/src/trie/mod.rs index 97d3ca76b34..ee4a34d2028 100644 --- a/core/store/src/trie/mod.rs +++ b/core/store/src/trie/mod.rs @@ -1824,7 +1824,7 @@ mod tests { create_test_store, gen_changes, simplify_changes, test_populate_flat_storage, test_populate_trie, TestTriesBuilder, }; - use crate::{DBCol, MissingTrieValueContext}; + use crate::MissingTrieValueContext; use super::*; @@ -2123,7 +2123,7 @@ mod tests { for _test_run in 0..10 { let num_iterations = rng.gen_range(1..20); let tries = TestTriesBuilder::new().build(); - let store = tries.get_store(); + let store = tries.store(); let mut state_root = Trie::EMPTY_ROOT; for _ in 0..num_iterations { let trie_changes = gen_changes(&mut rng, 20); @@ -2149,7 +2149,7 @@ mod tests { state_root = test_populate_trie(&tries, &state_root, ShardUId::single_shard(), trie_changes); assert_eq!(state_root, Trie::EMPTY_ROOT, "Trie must be empty"); - assert!(store.iter(DBCol::State).peekable().peek().is_none(), "Storage must be empty"); + assert!(store.iter_raw_bytes().peekable().peek().is_none(), "Storage must be empty"); } } diff --git a/core/store/src/trie/prefetching_trie_storage.rs b/core/store/src/trie/prefetching_trie_storage.rs index e6a60924550..31e831e331c 100644 --- a/core/store/src/trie/prefetching_trie_storage.rs +++ b/core/store/src/trie/prefetching_trie_storage.rs @@ -1,9 +1,7 @@ +use crate::adapter::trie_store::TrieStoreAdapter; use crate::config::PrefetchConfig; use crate::sync_utils::Monitor; -use crate::{ - metrics, DBCol, MissingTrieValueContext, StorageError, Store, Trie, TrieCache, - TrieCachingStorage, TrieConfig, TrieStorage, -}; +use crate::{metrics, StorageError, Trie, TrieCache, TrieConfig, TrieStorage}; use crossbeam::select; use near_o11y::metrics::prometheus; use near_o11y::metrics::prometheus::core::GenericGauge; @@ -47,7 +45,7 @@ const NUM_IO_THREADS: usize = 8; #[derive(Clone)] struct TriePrefetchingStorage { /// Store is shared with parent `TrieCachingStorage`. - store: Store, + store: TrieStoreAdapter, shard_uid: ShardUId, /// Shard cache is shared with parent `TrieCachingStorage`. But the /// pre-fetcher uses this in read-only mode to avoid premature evictions. @@ -77,7 +75,7 @@ pub struct PrefetchApi { /// multiple times. pub(crate) prefetching: PrefetchStagingArea, - store: Store, + store: TrieStoreAdapter, shard_cache: TrieCache, pub enable_receipt_prefetching: bool, @@ -243,29 +241,17 @@ impl TrieStorage for TriePrefetchingStorage { match prefetch_state { // Slot reserved for us, this thread should fetch it from DB. PrefetcherResult::SlotReserved => { - let key = TrieCachingStorage::get_key_from_shard_uid_and_hash(self.shard_uid, hash); - match self.store.get(DBCol::State, key.as_ref()) { - Ok(Some(value)) => { - let value: Arc<[u8]> = value.into(); + match self.store.get(self.shard_uid, hash) { + Ok(value) => { self.prefetching.insert_fetched(*hash, value.clone()); Ok(value) } - Ok(None) => { - // This is an unrecoverable error, a hash found in the trie had no node. - // Releasing the lock here to unstuck main thread if it - // was blocking on this value, but it will also fail on its read. - self.prefetching.release(hash); - Err(StorageError::MissingTrieValue( - MissingTrieValueContext::TriePrefetchingStorage, - *hash, - )) - } Err(e) => { - // This is an unrecoverable IO error. + // This is an unrecoverable error. // Releasing the lock here to unstuck main thread if it // was blocking on this value, but it will also fail on its read. self.prefetching.release(hash); - Err(StorageError::StorageInconsistentState(e.to_string())) + Err(e) } } } @@ -303,7 +289,7 @@ impl TrieStorage for TriePrefetchingStorage { impl TriePrefetchingStorage { pub(crate) fn new( - store: Store, + store: TrieStoreAdapter, shard_uid: ShardUId, shard_cache: TrieCache, prefetching: PrefetchStagingArea, @@ -402,7 +388,7 @@ impl PrefetchStagingArea { impl PrefetchApi { pub(crate) fn new( - store: Store, + store: TrieStoreAdapter, shard_cache: TrieCache, shard_uid: ShardUId, trie_config: &TrieConfig, diff --git a/core/store/src/trie/resharding_v2.rs b/core/store/src/trie/resharding_v2.rs index d02055001cb..6233a3169df 100644 --- a/core/store/src/trie/resharding_v2.rs +++ b/core/store/src/trie/resharding_v2.rs @@ -1,8 +1,9 @@ +use crate::adapter::trie_store::TrieStoreUpdateAdapter; use crate::adapter::StoreUpdateAdapter; use crate::flat::FlatStateChanges; use crate::{ - get, get_delayed_receipt_indices, get_promise_yield_indices, set, ShardTries, StoreUpdate, - Trie, TrieAccess as _, TrieUpdate, + get, get_delayed_receipt_indices, get_promise_yield_indices, set, ShardTries, Trie, + TrieAccess as _, TrieUpdate, }; use borsh::BorshDeserialize; use bytesize::ByteSize; @@ -39,7 +40,7 @@ impl ShardTries { state_roots: &HashMap, values: Vec<(Vec, Option>)>, account_id_to_shard_id: &dyn Fn(&AccountId) -> ShardUId, - ) -> Result<(StoreUpdate, HashMap), StorageError> { + ) -> Result<(TrieStoreUpdateAdapter<'static>, HashMap), StorageError> { self.add_values_to_children_states_impl(state_roots, values, &|raw_key| { // Here changes on DelayedReceipt, DelayedReceiptIndices, PromiseYieldTimeout, and // PromiseYieldIndices will be excluded. Both the delayed receipts and the yield @@ -62,7 +63,7 @@ impl ShardTries { state_roots: &HashMap, values: Vec<(Vec, Option>)>, key_to_shard_id: &dyn Fn(&[u8]) -> Result, StorageError>, - ) -> Result<(StoreUpdate, HashMap), StorageError> { + ) -> Result<(TrieStoreUpdateAdapter<'static>, HashMap), StorageError> { let mut changes_by_shard: HashMap<_, Vec<_>> = HashMap::new(); for (raw_key, value) in values.into_iter() { if let Some(new_shard_uid) = key_to_shard_id(&raw_key)? { @@ -100,7 +101,7 @@ impl ShardTries { state_roots: &HashMap, receipts: &[Receipt], account_id_to_shard_uid: &dyn Fn(&AccountId) -> ShardUId, - ) -> Result<(StoreUpdate, HashMap), StorageError> { + ) -> Result<(TrieStoreUpdateAdapter<'static>, HashMap), StorageError> { let mut trie_updates: HashMap<_, _> = self.get_trie_updates(state_roots); apply_delayed_receipts_to_children_states_impl( &mut trie_updates, @@ -116,7 +117,7 @@ impl ShardTries { state_roots: &HashMap, timeouts: &[PromiseYieldTimeout], account_id_to_shard_uid: &dyn Fn(&AccountId) -> ShardUId, - ) -> Result<(StoreUpdate, HashMap), StorageError> { + ) -> Result<(TrieStoreUpdateAdapter<'static>, HashMap), StorageError> { let mut trie_updates: HashMap<_, _> = self.get_trie_updates(state_roots); apply_promise_yield_timeouts_to_children_states_impl( &mut trie_updates, @@ -130,7 +131,7 @@ impl ShardTries { fn finalize_and_apply_trie_updates( &self, updates: HashMap, - ) -> Result<(StoreUpdate, HashMap), StorageError> { + ) -> Result<(TrieStoreUpdateAdapter<'static>, HashMap), StorageError> { let mut new_state_roots = HashMap::new(); let mut store_update = self.store_update(); for (shard_uid, update) in updates { diff --git a/core/store/src/trie/shard_tries.rs b/core/store/src/trie/shard_tries.rs index f6990b191ea..92977af9469 100644 --- a/core/store/src/trie/shard_tries.rs +++ b/core/store/src/trie/shard_tries.rs @@ -1,17 +1,19 @@ use super::mem::mem_tries::MemTries; use super::state_snapshot::{StateSnapshot, StateSnapshotConfig}; use super::TrieRefcountSubtraction; +use crate::adapter::trie_store::{TrieStoreAdapter, TrieStoreUpdateAdapter}; +use crate::adapter::StoreAdapter; use crate::flat::{FlatStorageManager, FlatStorageStatus}; use crate::trie::config::TrieConfig; use crate::trie::mem::loading::load_trie_from_flat_state_and_delta; use crate::trie::prefetching_trie_storage::PrefetchingThreadsHandle; use crate::trie::trie_storage::{TrieCache, TrieCachingStorage}; use crate::trie::{TrieRefcountAddition, POISONED_LOCK_ERR}; -use crate::{metrics, DBCol, PrefetchApi, TrieDBStorage, TrieStorage}; -use crate::{Store, StoreUpdate, Trie, TrieChanges, TrieUpdate}; +use crate::{metrics, DBCol, PrefetchApi, Store, TrieDBStorage, TrieStorage}; +use crate::{Trie, TrieChanges, TrieUpdate}; use near_primitives::errors::StorageError; use near_primitives::hash::CryptoHash; -use near_primitives::shard_layout::{self, ShardUId}; +use near_primitives::shard_layout::ShardUId; use near_primitives::trie_key::TrieKey; use near_primitives::types::{ BlockHeight, RawStateChange, RawStateChangesWithTrieKey, StateChangeCause, StateRoot, @@ -22,7 +24,7 @@ use std::sync::{Arc, Mutex, RwLock}; use tracing::info; struct ShardTriesInner { - store: Store, + store: TrieStoreAdapter, trie_config: TrieConfig, mem_tries: RwLock>>>, /// Cache reserved for client actor to use @@ -46,7 +48,7 @@ pub struct ShardTries(Arc); impl ShardTries { pub fn new( - store: Store, + store: TrieStoreAdapter, trie_config: TrieConfig, shard_uids: &[ShardUId], flat_storage_manager: FlatStorageManager, @@ -165,18 +167,14 @@ impl ShardTries { self.get_trie_for_shard_internal(shard_uid, state_root, true, None) } - pub fn store_update(&self) -> StoreUpdate { - StoreUpdate::new(self.get_db().clone()) + pub fn store_update(&self) -> TrieStoreUpdateAdapter<'static> { + self.0.store.store_update() } - pub fn get_store(&self) -> Store { + pub fn store(&self) -> TrieStoreAdapter { self.0.store.clone() } - pub(crate) fn get_db(&self) -> &Arc { - &self.0.store.storage - } - pub fn get_flat_storage_manager(&self) -> FlatStorageManager { self.0.flat_storage_manager.clone() } @@ -238,15 +236,11 @@ impl ShardTries { &self, deletions: &[TrieRefcountSubtraction], shard_uid: ShardUId, - store_update: &mut StoreUpdate, + store_update: &mut TrieStoreUpdateAdapter, ) { let mut ops = Vec::with_capacity(deletions.len()); for TrieRefcountSubtraction { trie_node_or_value_hash, rc, .. } in deletions.iter() { - let key = TrieCachingStorage::get_key_from_shard_uid_and_hash( - shard_uid, - trie_node_or_value_hash, - ); - store_update.decrement_refcount_by(DBCol::State, key.as_ref(), *rc); + store_update.decrement_refcount_by(shard_uid, trie_node_or_value_hash, *rc); ops.push((trie_node_or_value_hash, None)); } @@ -257,17 +251,18 @@ impl ShardTries { &self, insertions: &[TrieRefcountAddition], shard_uid: ShardUId, - store_update: &mut StoreUpdate, + store_update: &mut TrieStoreUpdateAdapter, ) { let mut ops = Vec::with_capacity(insertions.len()); for TrieRefcountAddition { trie_node_or_value_hash, trie_node_or_value, rc } in insertions.iter() { - let key = TrieCachingStorage::get_key_from_shard_uid_and_hash( + store_update.increment_refcount_by( shard_uid, trie_node_or_value_hash, + trie_node_or_value, + *rc, ); - store_update.increment_refcount_by(DBCol::State, key.as_ref(), trie_node_or_value, *rc); ops.push((trie_node_or_value_hash, Some(trie_node_or_value.as_slice()))); } self.update_cache(ops, shard_uid); @@ -278,7 +273,7 @@ impl ShardTries { trie_changes: &TrieChanges, shard_uid: ShardUId, apply_deletions: bool, - store_update: &mut StoreUpdate, + store_update: &mut TrieStoreUpdateAdapter, ) -> StateRoot { self.apply_insertions_inner(&trie_changes.insertions, shard_uid, store_update); if apply_deletions { @@ -298,7 +293,7 @@ impl ShardTries { &self, trie_changes: &TrieChanges, shard_uid: ShardUId, - store_update: &mut StoreUpdate, + store_update: &mut TrieStoreUpdateAdapter, ) { // `itoa` is much faster for printing shard_id to a string than trivial alternatives. let mut buffer = itoa::Buffer::new(); @@ -321,7 +316,7 @@ impl ShardTries { &self, trie_changes: &TrieChanges, shard_uid: ShardUId, - store_update: &mut StoreUpdate, + store_update: &mut TrieStoreUpdateAdapter, ) { // `itoa` is much faster for printing shard_id to a string than trivial alternatives. let mut buffer = itoa::Buffer::new(); @@ -337,7 +332,7 @@ impl ShardTries { &self, trie_changes: &TrieChanges, shard_uid: ShardUId, - store_update: &mut StoreUpdate, + store_update: &mut TrieStoreUpdateAdapter, ) { // `itoa` is much faster for printing shard_id to a string than trivial alternatives. let mut buffer = itoa::Buffer::new(); @@ -359,7 +354,7 @@ impl ShardTries { &self, trie_changes: &TrieChanges, shard_uid: ShardUId, - store_update: &mut StoreUpdate, + store_update: &mut TrieStoreUpdateAdapter, ) -> StateRoot { self.apply_all_inner(trie_changes, shard_uid, true, store_update) } @@ -396,22 +391,6 @@ impl ShardTries { Ok(manager.get_flat_storage_status(shard_uid)) } - /// Removes all trie state values from store for a given shard_uid - /// Useful when we are trying to delete state of parent shard after resharding - /// Note that flat storage needs to be handled separately - pub fn delete_trie_for_shard(&self, shard_uid: ShardUId, store_update: &mut StoreUpdate) { - // Clear both caches and remove state values from store - let _cache = self.0.caches.lock().expect(POISONED_LOCK_ERR).remove(&shard_uid); - let _view_cache = self.0.view_caches.lock().expect(POISONED_LOCK_ERR).remove(&shard_uid); - Self::remove_all_state_values(store_update, shard_uid); - } - - fn remove_all_state_values(store_update: &mut StoreUpdate, shard_uid: ShardUId) { - let key_from = shard_uid.to_bytes(); - let key_to = ShardUId::next_shard_prefix(&key_from); - store_update.delete_range(DBCol::State, &key_from, &key_to); - } - /// Retains in-memory tries for given shards, i.e. unload tries from memory for shards that are NOT /// in the given list. Should be called to unload obsolete tries from memory. pub fn retain_mem_tries(&self, shard_uids: &[ShardUId]) { @@ -437,7 +416,7 @@ impl ShardTries { ) -> Result<(), StorageError> { info!(target: "memtrie", "Loading trie to memory for shard {:?}...", shard_uid); let mem_tries = load_trie_from_flat_state_and_delta( - &self.0.store, + &self.0.store.store(), *shard_uid, state_root, parallelize, @@ -558,12 +537,12 @@ impl WrappedTrieChanges { } /// Save insertions of trie nodes into Store. - pub fn insertions_into(&self, store_update: &mut StoreUpdate) { + pub fn insertions_into(&self, store_update: &mut TrieStoreUpdateAdapter) { self.tries.apply_insertions(&self.trie_changes, self.shard_uid, store_update) } /// Save deletions of trie nodes into Store. - pub fn deletions_into(&self, store_update: &mut StoreUpdate) { + pub fn deletions_into(&self, store_update: &mut TrieStoreUpdateAdapter) { self.tries.apply_deletions(&self.trie_changes, self.shard_uid, store_update) } @@ -577,7 +556,7 @@ impl WrappedTrieChanges { fields(num_state_changes = self.state_changes.len(), shard_id = self.shard_uid.shard_id()), skip_all, )] - pub fn state_changes_into(&mut self, store_update: &mut StoreUpdate) { + pub fn state_changes_into(&mut self, store_update: &mut TrieStoreUpdateAdapter) { for mut change_with_trie_key in self.state_changes.drain(..) { assert!( !change_with_trie_key.changes.iter().any(|RawStateChange { cause, .. }| matches!( @@ -610,11 +589,7 @@ impl WrappedTrieChanges { ), }; - store_update.set( - DBCol::StateChanges, - storage_key.as_ref(), - &borsh::to_vec(&change_with_trie_key).expect("Borsh serialize cannot fail"), - ); + store_update.set_state_changes(storage_key, &change_with_trie_key); } } @@ -624,12 +599,8 @@ impl WrappedTrieChanges { "ShardTries::trie_changes_into", skip_all )] - pub fn trie_changes_into(&mut self, store_update: &mut StoreUpdate) -> std::io::Result<()> { - store_update.set_ser( - DBCol::TrieChanges, - &shard_layout::get_block_shard_uid(&self.block_hash, &self.shard_uid), - &self.trie_changes, - ) + pub fn trie_changes_into(&mut self, store_update: &mut TrieStoreUpdateAdapter) { + store_update.set_trie_changes(self.shard_uid, &self.block_hash, &self.trie_changes) } } @@ -771,7 +742,7 @@ mod test { }; let shard_uids = Vec::from([ShardUId::single_shard()]); ShardTries::new( - store.clone(), + store.trie_store(), trie_config, &shard_uids, FlatStorageManager::new(store.flat_store()), @@ -889,7 +860,7 @@ mod test { let shard_uid = *shard_uids.first().unwrap(); let trie = ShardTries::new( - store.clone(), + store.trie_store(), trie_config, &shard_uids, FlatStorageManager::new(store.flat_store()), @@ -915,33 +886,4 @@ mod test { trie.update_cache(insert_ops, shard_uid); assert!(trie_caches.lock().unwrap().get(&shard_uid).unwrap().get(&key).is_none()); } - - #[test] - fn test_delete_trie_for_shard() { - let shard_uid = ShardUId::single_shard(); - let tries = create_trie(); - - let key = CryptoHash::hash_borsh("alice").as_bytes().to_vec(); - let val: Vec = Vec::from([0, 1, 2, 3, 4]); - - // insert some data - let trie = tries.get_trie_for_shard(shard_uid, CryptoHash::default()); - let trie_changes = trie.update(vec![(key, Some(val))]).unwrap(); - let mut store_update = tries.store_update(); - tries.apply_insertions(&trie_changes, shard_uid, &mut store_update); - store_update.commit().unwrap(); - - // delete trie for shard_uid - let mut store_update = tries.store_update(); - tries.delete_trie_for_shard(shard_uid, &mut store_update); - store_update.commit().unwrap(); - - // verify if data and caches are deleted - assert!(tries.0.caches.lock().unwrap().get(&shard_uid).is_none()); - assert!(tries.0.view_caches.lock().unwrap().get(&shard_uid).is_none()); - let store = tries.get_store(); - let key_prefix = shard_uid.to_bytes(); - let mut iter = store.iter_prefix(DBCol::State, &key_prefix); - assert!(iter.next().is_none()); - } } diff --git a/core/store/src/trie/state_parts.rs b/core/store/src/trie/state_parts.rs index 23c89b19b6f..47b54740534 100644 --- a/core/store/src/trie/state_parts.rs +++ b/core/store/src/trie/state_parts.rs @@ -525,7 +525,7 @@ mod tests { }; use super::*; - use crate::{DBCol, MissingTrieValueContext, TrieCachingStorage}; + use crate::MissingTrieValueContext; use near_primitives::shard_layout::ShardUId; /// Checks that sampling state boundaries always gives valid state keys @@ -1228,8 +1228,7 @@ mod tests { let mut store_update = tries.store_update(); let store_value = vec![5; value_len]; let value_hash = hash(&store_value); - let store_key = TrieCachingStorage::get_key_from_shard_uid_and_hash(shard_uid, &value_hash); - store_update.decrement_refcount(DBCol::State, &store_key); + store_update.decrement_refcount(shard_uid, &value_hash); store_update.commit().unwrap(); assert_eq!( diff --git a/core/store/src/trie/state_snapshot.rs b/core/store/src/trie/state_snapshot.rs index d1874af6724..0a723464e9d 100644 --- a/core/store/src/trie/state_snapshot.rs +++ b/core/store/src/trie/state_snapshot.rs @@ -1,11 +1,11 @@ +use crate::adapter::trie_store::TrieStoreAdapter; use crate::adapter::StoreAdapter; use crate::config::StateSnapshotType; -use crate::db::STATE_SNAPSHOT_KEY; use crate::flat::{FlatStorageManager, FlatStorageStatus}; use crate::Mode; +use crate::ShardTries; +use crate::StoreConfig; use crate::{checkpoint_hot_storage_and_cleanup_columns, metrics, DBCol, NodeStorage}; -use crate::{option_to_not_found, ShardTries}; -use crate::{Store, StoreConfig}; use near_primitives::block::Block; use near_primitives::errors::EpochError; use near_primitives::errors::StorageError; @@ -70,7 +70,7 @@ pub struct StateSnapshot { /// The state snapshot represents the state including changes of the next block of this block. prev_block_hash: CryptoHash, /// Read-only store. - store: Store, + store: TrieStoreAdapter, /// Access to flat storage in that store. flat_storage_manager: FlatStorageManager, /// Shards which were successfully included in the snapshot. @@ -80,7 +80,7 @@ pub struct StateSnapshot { impl StateSnapshot { /// Creates an object and also creates flat storage for the given shards. pub fn new( - store: Store, + store: TrieStoreAdapter, prev_block_hash: CryptoHash, flat_storage_manager: FlatStorageManager, requested_shard_uids: &[ShardUId], @@ -159,7 +159,7 @@ impl ShardTries { pub fn get_state_snapshot( &self, block_hash: &CryptoHash, - ) -> Result<(Store, FlatStorageManager), SnapshotError> { + ) -> Result<(TrieStoreAdapter, FlatStorageManager), SnapshotError> { // Taking this lock can last up to 10 seconds, if the snapshot happens to be re-created. let guard = self.state_snapshot().try_read()?; let data = guard.as_ref().ok_or(SnapshotError::SnapshotNotFound(*block_hash))?; @@ -189,7 +189,7 @@ impl ShardTries { // `write()` lock is held for the whole duration of this function. let mut state_snapshot_lock = self.state_snapshot().write().unwrap(); - let db_snapshot_hash = self.get_state_snapshot_hash(); + let db_snapshot_hash = self.store().get_state_snapshot_hash(); if let Some(state_snapshot) = &*state_snapshot_lock { // only return Ok() when the hash stored in STATE_SNAPSHOT_KEY and in state_snapshot_lock and prev_block_hash are the same if db_snapshot_hash.is_ok_and(|hash| hash == prev_block_hash) @@ -204,7 +204,7 @@ impl ShardTries { let StateSnapshotConfig { home_dir, hot_store_path, state_snapshot_subdir, .. } = self.state_snapshot_config(); let storage = checkpoint_hot_storage_and_cleanup_columns( - &self.get_store(), + &self.store().store(), &Self::get_state_snapshot_base_dir( &prev_block_hash, home_dir, @@ -215,7 +215,7 @@ impl ShardTries { // Can't be cleaned up now because these columns are needed to `update_flat_head()`. Some(STATE_SNAPSHOT_COLUMNS), )?; - let store = storage.get_hot_store(); + let store = storage.get_hot_store().trie_store(); // It is fine to create a separate FlatStorageManager, because // it is used only for reading flat storage in the snapshot a // doesn't introduce memory overhead. @@ -229,14 +229,13 @@ impl ShardTries { )); // this will set the new hash for state snapshot in rocksdb. will retry until success. - let mut set_state_snapshot_in_db = false; - while !set_state_snapshot_in_db { - set_state_snapshot_in_db = match self.set_state_snapshot_hash(Some(prev_block_hash)) { - Ok(_) => true, + for _ in 0..3 { + let mut store_update = self.store_update(); + store_update.set_state_snapshot_hash(Some(prev_block_hash)); + match store_update.commit() { + Ok(_) => {} Err(err) => { - // This will be retried. - tracing::debug!(target: "state_snapshot", ?err, "Failed to set the new state snapshot for BlockMisc::STATE_SNAPSHOT_KEY in rocksdb"); - false + tracing::error!(target: "state_snapshot", ?err, "Failed to set the new state snapshot for BlockMisc::STATE_SNAPSHOT_KEY in rocksdb"); } } } @@ -273,7 +272,9 @@ impl ShardTries { // this will delete the STATE_SNAPSHOT_KEY-value pair from db. Will retry 3 times for _ in 0..3 { - match self.set_state_snapshot_hash(None) { + let mut store_update = self.store_update(); + store_update.set_state_snapshot_hash(None); + match store_update.commit() { Ok(_) => break, Err(err) => { tracing::error!(target: "state_snapshot", ?err, "Failed to delete the old state snapshot for BlockMisc::STATE_SNAPSHOT_KEY in rocksdb") @@ -312,25 +313,6 @@ impl ShardTries { home_dir.join(hot_store_path).join(state_snapshot_subdir).join(format!("{prev_block_hash}")) } - /// Retrieves STATE_SNAPSHOT_KEY - pub fn get_state_snapshot_hash(&self) -> Result { - option_to_not_found( - self.get_store().get_ser(DBCol::BlockMisc, STATE_SNAPSHOT_KEY), - "STATE_SNAPSHOT_KEY", - ) - } - - /// Updates STATE_SNAPSHOT_KEY. - pub fn set_state_snapshot_hash(&self, value: Option) -> Result<(), io::Error> { - let mut store_update = self.store_update(); - let key = STATE_SNAPSHOT_KEY; - match value { - None => store_update.delete(DBCol::BlockMisc, key), - Some(value) => store_update.set_ser(DBCol::BlockMisc, key, &value)?, - } - store_update.commit().into() - } - /// Read RocksDB for the latest available snapshot hash, if available, open base_path+snapshot_hash for the state snapshot /// we don't deal with multiple snapshots here because we will deal with it whenever a new snapshot is created and saved to file system pub fn maybe_open_state_snapshot( @@ -344,7 +326,7 @@ impl ShardTries { self.state_snapshot_config(); // directly return error if no snapshot is found - let snapshot_hash = self.get_state_snapshot_hash()?; + let snapshot_hash = self.store().get_state_snapshot_hash()?; let snapshot_path = Self::get_state_snapshot_base_dir( &snapshot_hash, @@ -361,7 +343,7 @@ impl ShardTries { let opener = NodeStorage::opener(&snapshot_path, false, &store_config, None); let storage = opener.open_in_mode(Mode::ReadOnly)?; - let store = storage.get_hot_store(); + let store = storage.get_hot_store().trie_store(); let flat_storage_manager = FlatStorageManager::new(store.flat_store()); let shard_uids = get_shard_uids_fn(snapshot_hash)?; diff --git a/core/store/src/trie/trie_recording.rs b/core/store/src/trie/trie_recording.rs index f285a321744..c8e3b701aba 100644 --- a/core/store/src/trie/trie_recording.rs +++ b/core/store/src/trie/trie_recording.rs @@ -274,6 +274,8 @@ impl SubtreeSize { #[cfg(test)] mod trie_recording_tests { + use crate::adapter::trie_store::TrieStoreAdapter; + use crate::adapter::{StoreAdapter, StoreUpdateAdapter}; use crate::db::refcount::decode_value_with_rc; use crate::test_utils::{ gen_larger_changes, simplify_changes, test_populate_flat_storage, test_populate_trie, @@ -362,6 +364,7 @@ mod trie_recording_tests { ); let mut update_for_chunk_extra = tries_for_building.store_update(); update_for_chunk_extra + .store_update() .set_ser( DBCol::ChunkExtra, &get_block_shard_uid(&CryptoHash::default(), &shard_uid), @@ -412,7 +415,7 @@ mod trie_recording_tests { let (keys_to_get, keys_to_get_ref) = keys.into_iter().filter(|_| random()).partition::, _>(|_| random()); PreparedTrie { - store: tries_for_building.get_store(), + store: tries_for_building.store().store(), shard_uid, data_in_trie, keys_to_get, @@ -428,24 +431,25 @@ mod trie_recording_tests { /// The only thing we don't delete are the values, which may not be /// inlined. fn destructively_delete_in_memory_state_from_disk( - store: &Store, + store: &TrieStoreAdapter, data_in_trie: &HashMap, Vec>, ) { let key_hashes_to_keep = data_in_trie.iter().map(|(_, v)| hash(&v)).collect::>(); let mut update = store.store_update(); - for result in store.iter_raw_bytes(DBCol::State) { + for result in store.iter_raw_bytes() { let (key, value) = result.unwrap(); let (_, refcount) = decode_value_with_rc(&value); - let key_hash: CryptoHash = CryptoHash::try_from_slice(&key[8..]).unwrap(); + let shard_uid = ShardUId::try_from_slice(&key[0..8]).unwrap(); + let key_hash = CryptoHash::try_from_slice(&key[8..]).unwrap(); if !key_hashes_to_keep.contains(&key_hash) { update.decrement_refcount_by( - DBCol::State, - &key, + shard_uid, + &key_hash, NonZeroU32::new(refcount as u32).unwrap(), ); } } - update.delete_all(DBCol::FlatState); + update.store_update().delete_all(DBCol::FlatState); update.commit().unwrap(); } @@ -567,7 +571,7 @@ mod trie_recording_tests { tries.load_mem_trie(&shard_uid, None, false).unwrap(); // Delete the on-disk state so that we really know we're using // in-memory tries. - destructively_delete_in_memory_state_from_disk(&store, &data_in_trie); + destructively_delete_in_memory_state_from_disk(&store.trie_store(), &data_in_trie); let trie = get_trie_for_shard(&tries, shard_uid, state_root, use_flat_storage) .recording_reads(); trie.accounting_cache.borrow().enable_switch().set(enable_accounting_cache); diff --git a/core/store/src/trie/trie_storage.rs b/core/store/src/trie/trie_storage.rs index 4293c08a406..61ac07fa9b0 100644 --- a/core/store/src/trie/trie_storage.rs +++ b/core/store/src/trie/trie_storage.rs @@ -1,7 +1,8 @@ +use crate::adapter::trie_store::TrieStoreAdapter; use crate::trie::config::TrieConfig; use crate::trie::prefetching_trie_storage::PrefetcherResult; use crate::trie::POISONED_LOCK_ERR; -use crate::{metrics, DBCol, MissingTrieValueContext, PrefetchApi, StorageError, Store}; +use crate::{metrics, MissingTrieValueContext, PrefetchApi, StorageError}; use lru::LruCache; use near_o11y::log_assert; use near_o11y::metrics::prometheus; @@ -358,7 +359,7 @@ impl TrieMemoryPartialStorage { /// optimization to speed up execution, whereas the latter is a deterministic /// cache used for gas accounting during contract execution. pub struct TrieCachingStorage { - pub(crate) store: Store, + pub(crate) store: TrieStoreAdapter, pub(crate) shard_uid: ShardUId, pub(crate) is_view: bool, @@ -390,7 +391,7 @@ struct TrieCacheInnerMetrics { impl TrieCachingStorage { pub fn new( - store: Store, + store: TrieStoreAdapter, shard_cache: TrieCache, shard_uid: ShardUId, is_view: bool, @@ -421,13 +422,6 @@ impl TrieCachingStorage { TrieCachingStorage { store, shard_uid, is_view, shard_cache, prefetch_api, metrics } } - pub fn get_key_from_shard_uid_and_hash(shard_uid: ShardUId, hash: &CryptoHash) -> [u8; 40] { - let mut key = [0; 40]; - key[0..8].copy_from_slice(&shard_uid.to_bytes()); - key[8..].copy_from_slice(hash.as_ref()); - key - } - /// Reads value if it is not in shard cache. Handles dropping the cache /// lock. Either waits for prefetcher to fetch it or reads it from DB. /// It is responsibility of caller to release the prefetch slot later. @@ -548,22 +542,9 @@ impl TrieStorage for TrieCachingStorage { } } -fn read_node_from_db( - store: &Store, - shard_uid: ShardUId, - hash: &CryptoHash, -) -> Result, StorageError> { - let key = TrieCachingStorage::get_key_from_shard_uid_and_hash(shard_uid, hash); - let val = store - .get(DBCol::State, key.as_ref()) - .map_err(|_| StorageError::StorageInternalError)? - .ok_or(StorageError::MissingTrieValue(MissingTrieValueContext::TrieStorage, *hash))?; - Ok(val.into()) -} - impl TrieCachingStorage { fn read_from_db(&self, hash: &CryptoHash) -> Result, StorageError> { - read_node_from_db(&self.store, self.shard_uid, hash) + self.store.get(self.shard_uid, hash) } pub fn prefetch_api(&self) -> &Option { @@ -576,19 +557,19 @@ impl TrieCachingStorage { /// This `TrieStorage` implementation has no caches, it just goes to DB. /// It is useful for background tasks that should not affect chunk processing and block each other. pub struct TrieDBStorage { - pub(crate) store: Store, + pub(crate) store: TrieStoreAdapter, pub(crate) shard_uid: ShardUId, } impl TrieDBStorage { - pub fn new(store: Store, shard_uid: ShardUId) -> Self { + pub fn new(store: TrieStoreAdapter, shard_uid: ShardUId) -> Self { Self { store, shard_uid } } } impl TrieStorage for TrieDBStorage { fn retrieve_raw_bytes(&self, hash: &CryptoHash) -> Result, StorageError> { - read_node_from_db(&self.store, self.shard_uid, hash) + self.store.get(self.shard_uid, hash) } } diff --git a/core/store/src/trie/trie_tests.rs b/core/store/src/trie/trie_tests.rs index 05626ae2c24..77eb52c08de 100644 --- a/core/store/src/trie/trie_tests.rs +++ b/core/store/src/trie/trie_tests.rs @@ -203,16 +203,18 @@ mod nodes_counter_tests { #[cfg(test)] mod trie_storage_tests { use super::*; + use crate::adapter::trie_store::TrieStoreAdapter; + use crate::adapter::StoreAdapter; use crate::test_utils::create_test_store; use crate::trie::accounting_cache::TrieAccountingCache; use crate::trie::trie_storage::{TrieCache, TrieCachingStorage, TrieDBStorage}; use crate::trie::TrieRefcountAddition; - use crate::{Store, TrieChanges, TrieConfig, TrieIterator}; + use crate::{TrieChanges, TrieConfig, TrieIterator}; use assert_matches::assert_matches; use near_o11y::testonly::init_test_logger; use near_primitives::hash::hash; - fn create_store_with_values(values: &[Vec], shard_uid: ShardUId) -> Store { + fn create_store_with_values(values: &[Vec], shard_uid: ShardUId) -> TrieStoreAdapter { let tries = TestTriesBuilder::new().build(); let mut trie_changes = TrieChanges::empty(Trie::EMPTY_ROOT); trie_changes.insertions = values @@ -226,7 +228,7 @@ mod trie_storage_tests { let mut store_update = tries.store_update(); tries.apply_all(&trie_changes, shard_uid, &mut store_update); store_update.commit().unwrap(); - tries.get_store() + tries.store() } /// Put item into storage. Check that it is retrieved correctly. @@ -276,7 +278,7 @@ mod trie_storage_tests { let shard_uid = ShardUId::single_shard(); let store = create_test_store(); let trie_caching_storage = TrieCachingStorage::new( - store, + store.trie_store(), TrieCache::new(&TrieConfig::default(), shard_uid, false), shard_uid, false, diff --git a/integration-tests/src/tests/client/state_snapshot.rs b/integration-tests/src/tests/client/state_snapshot.rs index 07fbfcef016..3142c4112fa 100644 --- a/integration-tests/src/tests/client/state_snapshot.rs +++ b/integration-tests/src/tests/client/state_snapshot.rs @@ -52,7 +52,7 @@ impl StateSnaptshotTestEnv { state_snapshot_subdir: state_snapshot_subdir.clone(), }; let shard_tries = ShardTries::new( - store.clone(), + store.trie_store(), trie_config, &shard_uids, flat_storage_manager, @@ -88,7 +88,9 @@ fn test_maybe_open_state_snapshot_file_not_exist() { let store = create_test_store(); let test_env = set_up_test_env_for_state_snapshots(&store); let snapshot_hash = CryptoHash::new(); - test_env.shard_tries.set_state_snapshot_hash(Some(snapshot_hash)).unwrap(); + let mut store_update = test_env.shard_tries.store_update(); + store_update.set_state_snapshot_hash(Some(snapshot_hash)); + store_update.commit().unwrap(); let result = test_env.shard_tries.maybe_open_state_snapshot(|_| Ok(vec![ShardUId::single_shard()])); assert!(result.is_err()); @@ -104,7 +106,9 @@ fn test_maybe_open_state_snapshot_garbage_snapshot() { let store = create_test_store(); let test_env = set_up_test_env_for_state_snapshots(&store); let snapshot_hash = CryptoHash::new(); - test_env.shard_tries.set_state_snapshot_hash(Some(snapshot_hash)).unwrap(); + let mut store_update = test_env.shard_tries.store_update(); + store_update.set_state_snapshot_hash(Some(snapshot_hash)); + store_update.commit().unwrap(); let snapshot_path = ShardTries::get_state_snapshot_base_dir( &snapshot_hash, &test_env.home_dir, @@ -148,7 +152,8 @@ fn verify_make_snapshot( .shard_tries .maybe_open_state_snapshot(|_| Ok(vec![ShardUId::single_shard()]))?; // check that the entry of STATE_SNAPSHOT_KEY is the latest block hash - let db_state_snapshot_hash = state_snapshot_test_env.shard_tries.get_state_snapshot_hash()?; + let db_state_snapshot_hash = + state_snapshot_test_env.shard_tries.store().get_state_snapshot_hash()?; if db_state_snapshot_hash != block_hash { return Err(anyhow::Error::msg( "the entry of STATE_SNAPSHOT_KEY does not equal to the prev block hash", @@ -228,7 +233,9 @@ fn test_make_state_snapshot() { } // check that if the entry in DBCol::STATE_SNAPSHOT_KEY was missing while snapshot file exists, an overwrite of snapshot can succeed - state_snapshot_test_env.shard_tries.set_state_snapshot_hash(None).unwrap(); + let mut store_update = state_snapshot_test_env.shard_tries.store_update(); + store_update.set_state_snapshot_hash(None); + store_update.commit().unwrap(); let head = env.clients[0].chain.head().unwrap(); let head_block_hash = head.last_block_hash; let head_block = env.clients[0].chain.get_block(&head_block_hash).unwrap(); diff --git a/nearcore/src/entity_debug.rs b/nearcore/src/entity_debug.rs index e074e572ebe..6f297abced2 100644 --- a/nearcore/src/entity_debug.rs +++ b/nearcore/src/entity_debug.rs @@ -31,14 +31,15 @@ use near_primitives::views::{ BlockHeaderView, BlockView, ChunkView, ExecutionOutcomeView, ReceiptView, SignedTransactionView, }; use near_store::adapter::flat_store::encode_flat_state_db_key; +use near_store::adapter::trie_store::get_key_from_shard_uid_and_hash; use near_store::db::GENESIS_CONGESTION_INFO_KEY; use near_store::flat::delta::KeyForFlatStateDelta; use near_store::flat::{FlatStateChanges, FlatStateDeltaMetadata, FlatStorageStatus}; use near_store::{ - DBCol, NibbleSlice, RawTrieNode, RawTrieNodeWithSize, ShardUId, Store, TrieCachingStorage, - CHUNK_TAIL_KEY, COLD_HEAD_KEY, FINAL_HEAD_KEY, FORK_TAIL_KEY, GENESIS_JSON_HASH_KEY, - GENESIS_STATE_ROOTS_KEY, HEADER_HEAD_KEY, HEAD_KEY, LARGEST_TARGET_HEIGHT_KEY, - LATEST_KNOWN_KEY, STATE_SNAPSHOT_KEY, STATE_SYNC_DUMP_KEY, TAIL_KEY, + DBCol, NibbleSlice, RawTrieNode, RawTrieNodeWithSize, ShardUId, Store, CHUNK_TAIL_KEY, + COLD_HEAD_KEY, FINAL_HEAD_KEY, FORK_TAIL_KEY, GENESIS_JSON_HASH_KEY, GENESIS_STATE_ROOTS_KEY, + HEADER_HEAD_KEY, HEAD_KEY, LARGEST_TARGET_HEIGHT_KEY, LATEST_KNOWN_KEY, STATE_SNAPSHOT_KEY, + STATE_SYNC_DUMP_KEY, TAIL_KEY, }; use serde::Serialize; use std::collections::{HashMap, HashSet}; @@ -248,10 +249,7 @@ impl EntityDebugHandlerImpl { let node = store .get_ser::( DBCol::State, - &TrieCachingStorage::get_key_from_shard_uid_and_hash( - shard_uid, - &trie_node_hash, - ), + &get_key_from_shard_uid_and_hash(shard_uid, &trie_node_hash), )? .ok_or_else(|| anyhow!("Trie node not found"))?; Ok(serialize_raw_trie_node(node)) @@ -270,10 +268,7 @@ impl EntityDebugHandlerImpl { let node = store .get_ser::( DBCol::State, - &TrieCachingStorage::get_key_from_shard_uid_and_hash( - shard_uid, - &chunk.prev_state_root(), - ), + &get_key_from_shard_uid_and_hash(shard_uid, &chunk.prev_state_root()), )? .ok_or_else(|| anyhow!("State root not found"))?; Ok(serialize_raw_trie_node(node)) @@ -282,10 +277,7 @@ impl EntityDebugHandlerImpl { let value = store .get( DBCol::State, - &TrieCachingStorage::get_key_from_shard_uid_and_hash( - shard_uid, - &trie_value_hash, - ), + &get_key_from_shard_uid_and_hash(shard_uid, &trie_value_hash), )? .ok_or_else(|| anyhow!("Trie value not found"))?; Ok(serialize_entity(&hex::encode(value.as_slice()))) @@ -454,10 +446,7 @@ impl EntityDebugHandlerImpl { ) -> anyhow::Result> { Ok(match state { FlatStateValue::Ref(value) => store - .get( - DBCol::State, - &TrieCachingStorage::get_key_from_shard_uid_and_hash(shard_uid, &value.hash), - )? + .get(DBCol::State, &get_key_from_shard_uid_and_hash(shard_uid, &value.hash))? .ok_or_else(|| anyhow!("ValueRef could not be dereferenced"))? .to_vec(), FlatStateValue::Inlined(data) => data, diff --git a/nearcore/src/metrics.rs b/nearcore/src/metrics.rs index 3ace5e8d90b..f8cb0d85362 100644 --- a/nearcore/src/metrics.rs +++ b/nearcore/src/metrics.rs @@ -9,6 +9,7 @@ use near_o11y::metrics::{ IntGaugeVec, }; use near_primitives::{shard_layout::ShardLayout, state_record::StateRecord, trie_key}; +use near_store::adapter::StoreAdapter; use near_store::{ShardUId, Store, Trie, TrieDBStorage}; use std::sync::Arc; use std::sync::LazyLock; @@ -156,7 +157,7 @@ fn get_postponed_receipt_count_for_shard( let shard_uid = ShardUId::from_shard_id_and_layout(shard_id, shard_layout); let chunk_extra = chain_store.get_chunk_extra(block.hash(), &shard_uid)?; let state_root = chunk_extra.state_root(); - let storage = TrieDBStorage::new(store.clone(), shard_uid); + let storage = TrieDBStorage::new(store.trie_store(), shard_uid); let storage = Arc::new(storage); let flat_storage_chunk_view = None; let trie = Trie::new(storage, *state_root, flat_storage_chunk_view); diff --git a/runtime/runtime-params-estimator/src/estimator_context.rs b/runtime/runtime-params-estimator/src/estimator_context.rs index 5b72701e05e..e75e0c1f200 100644 --- a/runtime/runtime-params-estimator/src/estimator_context.rs +++ b/runtime/runtime-params-estimator/src/estimator_context.rs @@ -100,7 +100,7 @@ impl<'c> EstimatorContext<'c> { trie_config.load_mem_tries_for_shards = vec![shard_uid]; } let tries = ShardTries::new( - store, + store.trie_store(), trie_config, &[shard_uid], flat_storage_manager, @@ -310,7 +310,7 @@ impl Testbed<'_> { } pub(crate) fn trie_caching_storage(&mut self) -> TrieCachingStorage { - let store = self.tries.get_store(); + let store = self.tries.store(); let is_view = false; let prefetcher = None; let caching_storage = TrieCachingStorage::new( @@ -325,7 +325,7 @@ impl Testbed<'_> { pub(crate) fn clear_caches(&mut self) { // Flush out writes hanging in memtable - self.tries.get_store().flush().unwrap(); + self.tries.store().store().flush().unwrap(); // OS caches: // - only required in time based measurements, since ICount looks at syscalls directly. @@ -359,7 +359,7 @@ impl Testbed<'_> { ) .unwrap(); - let store = self.tries.get_store(); + let store = self.tries.store(); let mut store_update = store.store_update(); let shard_uid = ShardUId::single_shard(); self.root = self.tries.apply_all(&apply_result.trie_changes, shard_uid, &mut store_update); diff --git a/runtime/runtime/src/prefetch.rs b/runtime/runtime/src/prefetch.rs index 033d48f7e50..ef44616fa8e 100644 --- a/runtime/runtime/src/prefetch.rs +++ b/runtime/runtime/src/prefetch.rs @@ -477,7 +477,7 @@ mod tests { let store = create_test_store(); let flat_storage_manager = near_store::flat::FlatStorageManager::new(store.flat_store()); let tries = ShardTries::new( - store, + store.trie_store(), trie_config, &shard_uids, flat_storage_manager, diff --git a/tools/database/src/analyze_contract_sizes.rs b/tools/database/src/analyze_contract_sizes.rs index 60f8cc85d8d..459e418477c 100644 --- a/tools/database/src/analyze_contract_sizes.rs +++ b/tools/database/src/analyze_contract_sizes.rs @@ -5,6 +5,7 @@ use near_chain_configs::GenesisValidationMode; use near_epoch_manager::EpochManager; use near_primitives::trie_key::col; use near_primitives::types::AccountId; +use near_store::adapter::StoreAdapter; use near_store::{ShardUId, Trie, TrieDBStorage}; use nearcore::{load_config, open_storage}; use std::collections::BTreeMap; @@ -99,7 +100,7 @@ impl AnalyzeContractSizesCommand { chain_store.get_chunk_extra(&head.last_block_hash, &shard_uid).unwrap(); let state_root = chunk_extra.state_root(); - let trie_storage = Arc::new(TrieDBStorage::new(store.clone(), shard_uid)); + let trie_storage = Arc::new(TrieDBStorage::new(store.trie_store(), shard_uid)); let trie = Trie::new(trie_storage, *state_root, None); let mut iterator = trie.disk_iter().unwrap(); diff --git a/tools/database/src/analyze_delayed_receipt.rs b/tools/database/src/analyze_delayed_receipt.rs index b589649f82e..b425dbb812a 100644 --- a/tools/database/src/analyze_delayed_receipt.rs +++ b/tools/database/src/analyze_delayed_receipt.rs @@ -59,7 +59,7 @@ impl AnalyzeDelayedReceiptCommand { let shard_layout = epoch_manager.get_shard_layout(&tip.epoch_id).unwrap(); let shard_uids = shard_layout.shard_uids().collect::>(); let shard_tries = ShardTries::new( - store.clone(), + store.trie_store(), TrieConfig::default(), &shard_uids, FlatStorageManager::new(store.flat_store()), diff --git a/tools/database/src/state_perf.rs b/tools/database/src/state_perf.rs index 2b35612fb88..ec829c0908a 100644 --- a/tools/database/src/state_perf.rs +++ b/tools/database/src/state_perf.rs @@ -44,7 +44,7 @@ impl StatePerfCommand { .enumerate() .progress() { - let trie_storage = near_store::TrieDBStorage::new(store.clone(), shard_uid); + let trie_storage = near_store::TrieDBStorage::new(store.trie_store(), shard_uid); let include_sample = sample_i >= self.warmup_samples; if include_sample { perf_context.reset(); diff --git a/tools/fork-network/src/cli.rs b/tools/fork-network/src/cli.rs index b5bbccebeea..d2ea6539c93 100644 --- a/tools/fork-network/src/cli.rs +++ b/tools/fork-network/src/cli.rs @@ -506,7 +506,7 @@ impl ForkNetworkCommand { // Keeps track of accounts that have a full access key. let mut has_full_key = HashSet::new(); // Lets us lookup large values in the `State` columns. - let trie_storage = TrieDBStorage::new(store.clone(), shard_uid); + let trie_storage = TrieDBStorage::new(store.trie_store(), shard_uid); // Iterate over the whole flat storage and do the necessary changes to have access to all accounts. let mut index_delayed_receipt = 0; diff --git a/tools/fork-network/src/single_shard_storage_mutator.rs b/tools/fork-network/src/single_shard_storage_mutator.rs index 76e74fee5fd..0461b840ee1 100644 --- a/tools/fork-network/src/single_shard_storage_mutator.rs +++ b/tools/fork-network/src/single_shard_storage_mutator.rs @@ -171,7 +171,7 @@ impl SingleShardStorageMutator { mem_tries.write().unwrap().delete_until_height(fake_block_height - 1); } tracing::info!(?shard_uid, num_updates, "committing"); - update.set_ser( + update.store_update().set_ser( DBCol::Misc, format!("FORK_TOOL_SHARD_ID:{}", shard_uid.shard_id).as_bytes(), &state_root, diff --git a/tools/state-viewer/src/apply_chain_range.rs b/tools/state-viewer/src/apply_chain_range.rs index 2a07740b234..fc32dc8529d 100644 --- a/tools/state-viewer/src/apply_chain_range.rs +++ b/tools/state-viewer/src/apply_chain_range.rs @@ -303,7 +303,7 @@ fn apply_block_from_range( flat_storage.update_flat_head(&block_hash).unwrap(); // Apply trie changes to trie node caches. - let mut fake_store_update = read_store.store_update(); + let mut fake_store_update = read_store.trie_store().store_update(); apply_result.trie_changes.insertions_into(&mut fake_store_update); apply_result.trie_changes.deletions_into(&mut fake_store_update); } else { diff --git a/tools/state-viewer/src/cli.rs b/tools/state-viewer/src/cli.rs index 661c821a2b2..d51b8baab1a 100644 --- a/tools/state-viewer/src/cli.rs +++ b/tools/state-viewer/src/cli.rs @@ -14,6 +14,7 @@ use near_primitives::sharding::ChunkHash; use near_primitives::trie_key::col; use near_primitives::types::{BlockHeight, ShardId, StateRoot}; use near_primitives_core::types::EpochHeight; +use near_store::adapter::StoreAdapter; use near_store::{Mode, NodeStorage, Store, Temperature}; use nearcore::{load_config, NearConfig}; use std::path::{Path, PathBuf}; @@ -923,7 +924,7 @@ impl ViewTrieCmd { match self.format { ViewTrieFormat::Full => { view_trie( - store, + store.trie_store(), hash, self.shard_id, self.shard_version, @@ -937,7 +938,7 @@ impl ViewTrieCmd { } ViewTrieFormat::Pretty => { view_trie_leaves( - store, + store.trie_store(), hash, self.shard_id, self.shard_version, diff --git a/tools/state-viewer/src/commands.rs b/tools/state-viewer/src/commands.rs index 2d9bc200101..50a94a65952 100644 --- a/tools/state-viewer/src/commands.rs +++ b/tools/state-viewer/src/commands.rs @@ -40,6 +40,8 @@ use near_primitives::trie_key::TrieKey; use near_primitives::types::{BlockHeight, EpochId, ShardId}; use near_primitives::version::PROTOCOL_VERSION; use near_primitives_core::types::{Balance, EpochHeight}; +use near_store::adapter::trie_store::TrieStoreAdapter; +use near_store::adapter::StoreAdapter; use near_store::flat::FlatStorageChunkView; use near_store::flat::FlatStorageManager; use near_store::TrieStorage; @@ -1093,7 +1095,7 @@ pub(crate) fn print_epoch_analysis( } } -fn get_trie(store: Store, hash: CryptoHash, shard_id: u32, shard_version: u32) -> Trie { +fn get_trie(store: TrieStoreAdapter, hash: CryptoHash, shard_id: u32, shard_version: u32) -> Trie { let shard_uid = ShardUId { version: shard_version, shard_id }; let trie_config: TrieConfig = Default::default(); let shard_cache = TrieCache::new(&trie_config, shard_uid, true); @@ -1102,7 +1104,7 @@ fn get_trie(store: Store, hash: CryptoHash, shard_id: u32, shard_version: u32) - } pub(crate) fn view_trie( - store: Store, + store: TrieStoreAdapter, hash: CryptoHash, shard_id: u32, shard_version: u32, @@ -1126,7 +1128,7 @@ pub(crate) fn view_trie( } pub(crate) fn view_trie_leaves( - store: Store, + store: TrieStoreAdapter, state_root_hash: CryptoHash, shard_id: u32, shard_version: u32, @@ -1175,7 +1177,7 @@ pub(crate) fn contract_accounts( &ShardLayout::get_simple_nightshade_layout(), ); // Use simple non-caching storage, we don't expect many duplicate lookups while iterating. - let storage = TrieDBStorage::new(store.clone(), shard_uid); + let storage = TrieDBStorage::new(store.trie_store(), shard_uid); // We don't need flat state to traverse all accounts. let flat_storage_chunk_view = None; Trie::new(Arc::new(storage), state_root, flat_storage_chunk_view) @@ -1225,7 +1227,12 @@ pub(crate) fn print_state_stats(home_dir: &Path, store: Store, near_config: Near let flat_storage_manager = runtime.get_flat_storage_manager(); for shard_uid in shard_layout.shard_uids() { - print_state_stats_for_shard_uid(&store, &flat_storage_manager, block_hash, shard_uid); + print_state_stats_for_shard_uid( + store.trie_store(), + &flat_storage_manager, + block_hash, + shard_uid, + ); } } @@ -1259,13 +1266,13 @@ pub(crate) fn maybe_print_db_stats(store: Option) { /// Prints the state statistics for a single shard. fn print_state_stats_for_shard_uid( - store: &Store, + store: TrieStoreAdapter, flat_storage_manager: &FlatStorageManager, block_hash: CryptoHash, shard_uid: ShardUId, ) { flat_storage_manager.create_flat_storage_for_shard(shard_uid).unwrap(); - let trie_storage = TrieDBStorage::new(store.clone(), shard_uid); + let trie_storage = TrieDBStorage::new(store, shard_uid); let chunk_view = flat_storage_manager.chunk_view(shard_uid, block_hash).unwrap(); let mut state_stats = StateStats::default(); diff --git a/tools/state-viewer/src/trie_iteration_benchmark.rs b/tools/state-viewer/src/trie_iteration_benchmark.rs index ea6c211fdb3..031a23b569f 100644 --- a/tools/state-viewer/src/trie_iteration_benchmark.rs +++ b/tools/state-viewer/src/trie_iteration_benchmark.rs @@ -8,6 +8,7 @@ use near_primitives::trie_key::trie_key_parsers::{ parse_account_id_from_access_key_key, parse_account_id_from_trie_key_with_separator, }; use near_primitives_core::types::ShardId; +use near_store::adapter::StoreAdapter; use near_store::{ShardUId, Store, Trie, TrieDBStorage}; use nearcore::NearConfig; use std::cell::RefCell; @@ -148,7 +149,7 @@ impl TrieIterationBenchmarkCmd { // corresponds to the current epoch id. In practice shouldn't // matter as the shard layout doesn't change. let state_root = chunk_header.prev_state_root(); - let storage = TrieDBStorage::new(store.clone(), shard_uid); + let storage = TrieDBStorage::new(store.trie_store(), shard_uid); let flat_storage_chunk_view = None; Trie::new(Arc::new(storage), state_root, flat_storage_chunk_view) } From 9ad42e0b30838ff7ad0bcc1ca3d40b90e7d11409 Mon Sep 17 00:00:00 2001 From: Shreyan Gupta Date: Fri, 27 Sep 2024 03:08:13 -0700 Subject: [PATCH 33/49] [fix] Fix inconsistent block hash (#12161) Uncovered a weird issue by setting "no_cache" feature. Another reason why the store layer cache should be removed. Please take a look at issue https://github.com/near/nearcore/issues/12160 --- integration-tests/src/tests/client/process_blocks.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/src/tests/client/process_blocks.rs b/integration-tests/src/tests/client/process_blocks.rs index c4c991c0110..11d1bd34229 100644 --- a/integration-tests/src/tests/client/process_blocks.rs +++ b/integration-tests/src/tests/client/process_blocks.rs @@ -98,7 +98,7 @@ pub(crate) fn produce_blocks_from_height_with_protocol_version( let next_height = height + blocks_number; for i in height..next_height { let mut block = env.clients[0].produce_block(i).unwrap().unwrap(); - block.mut_header().set_latest_protocol_version(protocol_version); + set_block_protocol_version(&mut block, env.get_client_id(0), protocol_version); env.process_block(0, block.clone(), Provenance::PRODUCED); for j in 1..env.clients.len() { env.process_block(j, block.clone(), Provenance::NONE); From 2bb0a65287cffcd4e52a19f1c361cf98c5f0a925 Mon Sep 17 00:00:00 2001 From: Aleksandr Logunov Date: Sat, 28 Sep 2024 00:08:14 +0400 Subject: [PATCH 34/49] chore: nayduck unblock adding to merge queue (#12167) --- .github/workflows/nayduck_ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nayduck_ci.yml b/.github/workflows/nayduck_ci.yml index 28e149bd4c7..ed0c6c9dfe7 100644 --- a/.github/workflows/nayduck_ci.yml +++ b/.github/workflows/nayduck_ci.yml @@ -1,14 +1,15 @@ name: CI Nayduck tests on: + pull_request: merge_group: - workflow_dispatch: jobs: nayduck_tests: - runs-on: "ubuntu-latest" + runs-on: ubuntu-latest environment: development timeout-minutes: 60 + if: github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' steps: - name: Install JQ json processor @@ -48,4 +49,5 @@ jobs: else echo "CI Nayduck tests are failing https://nayduck.nearone.org/#/run/$RUN_ID." echo "Fix them before merging" + exit 1 fi From c3668d1d17b503192ec67e671b8d6728bca9460a Mon Sep 17 00:00:00 2001 From: Shreyan Gupta Date: Fri, 27 Sep 2024 14:00:37 -0700 Subject: [PATCH 35/49] [resharding] Introduce ReshardingManager (#12149) The resharding manager should hopefully be a one stop shop for all things resharding. For now I'm keeping it as a member of Chain instead of Client but we can change that later. The resharding manager in the future should be able to do the following work - Figure out when to schedule a resharding event based on next epoch info, last block of epoch, shard layout etc. - Manage all the sync preprocessing work like - Split parent trie into two parts - Update ShardTries with info about the two new child tries - Send request to any async job (probably we'll need a ReshardingActor) - Manage all the sync postprocessing work like - Replace child tries with the newly created tries There are some complications related to accessing store components that I plan to simplify in the future --- chain/chain/src/chain.rs | 126 +++-------------- chain/chain/src/resharding/manager.rs | 146 ++++++++++++++++++++ chain/chain/src/resharding/mod.rs | 1 + chain/chain/src/resharding/resharding_v2.rs | 4 +- chain/client/src/client_actor.rs | 2 +- core/store/src/trie/resharding_v2.rs | 16 +-- 6 files changed, 175 insertions(+), 120 deletions(-) create mode 100644 chain/chain/src/resharding/manager.rs diff --git a/chain/chain/src/chain.rs b/chain/chain/src/chain.rs index 12f31d34d7d..e23b23b0c3a 100644 --- a/chain/chain/src/chain.rs +++ b/chain/chain/src/chain.rs @@ -9,6 +9,7 @@ use crate::migrations::check_if_block_is_first_with_chunk_of_version; use crate::missing_chunks::MissingChunksPool; use crate::orphan::{Orphan, OrphanBlockPool}; use crate::rayon_spawner::RayonAsyncComputationSpawner; +use crate::resharding::manager::ReshardingManager; use crate::sharding::shuffle_receipt_proofs; use crate::state_request_tracker::StateRequestTracker; use crate::state_snapshot_actor::SnapshotCallbacks; @@ -40,9 +41,7 @@ use itertools::Itertools; use lru::LruCache; use near_async::futures::{AsyncComputationSpawner, AsyncComputationSpawnerExt}; use near_async::time::{Clock, Duration, Instant}; -use near_chain_configs::{ - MutableConfigValue, MutableValidatorSigner, ReshardingConfig, ReshardingHandle, -}; +use near_chain_configs::{MutableConfigValue, MutableValidatorSigner}; use near_chain_primitives::error::{BlockKnownError, Error, LogTransientStorageError}; use near_epoch_manager::shard_tracker::ShardTracker; use near_epoch_manager::EpochManagerAdapter; @@ -90,18 +89,16 @@ use near_primitives::views::{ FinalExecutionOutcomeView, FinalExecutionOutcomeWithReceiptView, FinalExecutionStatus, LightClientBlockView, SignedTransactionView, }; -use near_store::adapter::{StoreAdapter, StoreUpdateAdapter}; +use near_store::adapter::StoreUpdateAdapter; use near_store::config::StateSnapshotType; use near_store::flat::{FlatStorageReadyStatus, FlatStorageStatus}; -use near_store::trie::mem::resharding::RetainMode; +use near_store::get_genesis_state_roots; use near_store::DBCol; -use near_store::{get_genesis_state_roots, PartialStorage}; use node_runtime::bootstrap_congestion_info; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt::{Debug, Formatter}; use std::num::NonZeroUsize; -use std::str::FromStr; use std::sync::Arc; use time::ext::InstantExt as _; use tracing::{debug, debug_span, error, info, warn, Span}; @@ -280,12 +277,8 @@ pub struct Chain { /// A callback to initiate state snapshot. snapshot_callbacks: Option, - /// Configuration for resharding. - pub(crate) resharding_config: MutableConfigValue, - - // A handle that allows the main process to interrupt resharding if needed. - // This typically happens when the main process is interrupted. - pub resharding_handle: ReshardingHandle, + /// Manages all tasks related to resharding. + pub resharding_manager: ReshardingManager, } impl Drop for Chain { @@ -366,6 +359,11 @@ impl Chain { state_roots, )?; let (sc, rc) = unbounded(); + let resharding_manager = ReshardingManager::new( + store.clone(), + epoch_manager.clone(), + MutableConfigValue::new(Default::default(), "resharding_config"), + ); Ok(Chain { clock: clock.clone(), chain_store, @@ -389,11 +387,7 @@ impl Chain { pending_state_patch: Default::default(), requested_state_parts: StateRequestTracker::new(), snapshot_callbacks: None, - resharding_config: MutableConfigValue::new( - ReshardingConfig::default(), - "resharding_config", - ), - resharding_handle: ReshardingHandle::new(), + resharding_manager, }) } @@ -541,6 +535,11 @@ impl Chain { // Even though the channel is unbounded, the channel size is practically bounded by the size // of blocks_in_processing, which is set to 5 now. let (sc, rc) = unbounded(); + let resharding_manager = ReshardingManager::new( + chain_store.store().clone(), + epoch_manager.clone(), + chain_config.resharding_config, + ); Ok(Chain { clock: clock.clone(), chain_store, @@ -564,8 +563,7 @@ impl Chain { pending_state_patch: Default::default(), requested_state_parts: StateRequestTracker::new(), snapshot_callbacks, - resharding_config: chain_config.resharding_config, - resharding_handle: ReshardingHandle::new(), + resharding_manager, }) } @@ -1850,88 +1848,6 @@ impl Chain { }); } - /// If shard layout changes after the given block, creates temporary - /// memtries for new shards to be able to process them in the next epoch. - /// Note this doesn't complete resharding, proper memtries are to be - /// created later. - fn process_memtrie_resharding_storage_update( - &mut self, - block: &Block, - shard_uid: ShardUId, - ) -> Result<(), Error> { - let block_hash = block.hash(); - let block_height = block.header().height(); - let prev_hash = block.header().prev_hash(); - if !self.epoch_manager.will_shard_layout_change(prev_hash)? { - return Ok(()); - } - - let next_epoch_id = self.epoch_manager.get_next_epoch_id_from_prev_block(prev_hash)?; - let next_shard_layout = self.epoch_manager.get_shard_layout(&next_epoch_id)?; - let children_shard_uids = - next_shard_layout.get_children_shards_uids(shard_uid.shard_id()).unwrap(); - - // Hack to ensure this logic is not applied before ReshardingV3. - // TODO(#12019): proper logic. - if next_shard_layout.version() < 3 || children_shard_uids.len() == 1 { - return Ok(()); - } - assert_eq!(children_shard_uids.len(), 2); - - let chunk_extra = self.get_chunk_extra(block_hash, &shard_uid)?; - let tries = self.runtime_adapter.get_tries(); - let Some(mem_tries) = tries.get_mem_tries(shard_uid) else { - // TODO(#12019): what if node doesn't have memtrie? just pause - // processing? - error!( - "Memtrie not loaded. Cannot process memtrie resharding storage - update for block {:?}, shard {:?}", - block_hash, shard_uid - ); - return Err(Error::Other("Memtrie not loaded".to_string())); - }; - - // TODO(#12019): take proper boundary account. - let boundary_account = AccountId::from_str("boundary.near").unwrap(); - - // TODO(#12019): leave only tracked shards. - for (new_shard_uid, retain_mode) in [ - (children_shard_uids[0], RetainMode::Left), - (children_shard_uids[1], RetainMode::Right), - ] { - let mut mem_tries = mem_tries.write().unwrap(); - let mem_trie_update = mem_tries.update(*chunk_extra.state_root(), true)?; - - let (trie_changes, _) = - mem_trie_update.retain_split_shard(boundary_account.clone(), retain_mode); - let partial_state = PartialState::default(); - let partial_storage = PartialStorage { nodes: partial_state }; - let mem_changes = trie_changes.mem_trie_changes.as_ref().unwrap(); - let new_state_root = mem_tries.apply_memtrie_changes(block_height, mem_changes); - // TODO(#12019): set all fields of `ChunkExtra`. Consider stronger - // typing. Clarify where it should happen when `State` and - // `FlatState` update is implemented. - let mut child_chunk_extra = ChunkExtra::clone(&chunk_extra); - *child_chunk_extra.state_root_mut() = new_state_root; - - let mut chain_store_update = ChainStoreUpdate::new(&mut self.chain_store); - chain_store_update.save_chunk_extra(block_hash, &new_shard_uid, child_chunk_extra); - chain_store_update.save_state_transition_data( - *block_hash, - new_shard_uid.shard_id(), - Some(partial_storage), - CryptoHash::default(), - ); - chain_store_update.commit()?; - - let mut store_update = self.chain_store.store().trie_store().store_update(); - tries.apply_insertions(&trie_changes, new_shard_uid, &mut store_update); - store_update.commit()?; - } - - Ok(()) - } - #[tracing::instrument(level = "debug", target = "chain", "postprocess_block_only", skip_all)] fn postprocess_block_only( &mut self, @@ -2039,7 +1955,11 @@ impl Chain { if need_storage_update { // TODO(#12019): consider adding to catchup flow. - self.process_memtrie_resharding_storage_update(&block, shard_uid)?; + self.resharding_manager.process_memtrie_resharding_storage_update( + &block, + shard_uid, + self.runtime_adapter.get_tries(), + )?; // Update flat storage head to be the last final block. Note that this update happens // in a separate db transaction from the update from block processing. This is intentional diff --git a/chain/chain/src/resharding/manager.rs b/chain/chain/src/resharding/manager.rs new file mode 100644 index 00000000000..c641aefd37d --- /dev/null +++ b/chain/chain/src/resharding/manager.rs @@ -0,0 +1,146 @@ +use std::str::FromStr; +use std::sync::Arc; + +use near_chain_configs::{MutableConfigValue, ReshardingConfig, ReshardingHandle}; +use near_chain_primitives::Error; +use near_epoch_manager::EpochManagerAdapter; +use near_primitives::block::Block; +use near_primitives::challenge::PartialState; +use near_primitives::hash::CryptoHash; +use near_primitives::shard_layout::get_block_shard_uid; +use near_primitives::stateless_validation::stored_chunk_state_transition_data::StoredChunkStateTransitionData; +use near_primitives::types::chunk_extra::ChunkExtra; +use near_primitives::types::AccountId; +use near_primitives::utils::get_block_shard_id; +use near_store::adapter::StoreUpdateAdapter; +use near_store::trie::mem::resharding::RetainMode; +use near_store::{DBCol, PartialStorage, ShardTries, ShardUId, Store}; + +pub struct ReshardingManager { + store: Store, + epoch_manager: Arc, + /// Configuration for resharding. + pub resharding_config: MutableConfigValue, + /// A handle that allows the main process to interrupt resharding if needed. + /// This typically happens when the main process is interrupted. + pub resharding_handle: ReshardingHandle, +} + +impl ReshardingManager { + pub fn new( + store: Store, + epoch_manager: Arc, + resharding_config: MutableConfigValue, + ) -> Self { + Self { store, epoch_manager, resharding_config, resharding_handle: ReshardingHandle::new() } + } + + /// If shard layout changes after the given block, creates temporary + /// memtries for new shards to be able to process them in the next epoch. + /// Note this doesn't complete resharding, proper memtries are to be + /// created later. + pub fn process_memtrie_resharding_storage_update( + &mut self, + block: &Block, + shard_uid: ShardUId, + tries: ShardTries, + ) -> Result<(), Error> { + let block_hash = block.hash(); + let block_height = block.header().height(); + let prev_hash = block.header().prev_hash(); + if !self.epoch_manager.will_shard_layout_change(prev_hash)? { + return Ok(()); + } + + let next_epoch_id = self.epoch_manager.get_next_epoch_id_from_prev_block(prev_hash)?; + let next_shard_layout = self.epoch_manager.get_shard_layout(&next_epoch_id)?; + let children_shard_uids = + next_shard_layout.get_children_shards_uids(shard_uid.shard_id()).unwrap(); + + // Hack to ensure this logic is not applied before ReshardingV3. + // TODO(#12019): proper logic. + if next_shard_layout.version() < 3 || children_shard_uids.len() == 1 { + return Ok(()); + } + assert_eq!(children_shard_uids.len(), 2); + + let chunk_extra = self.get_chunk_extra(block_hash, &shard_uid)?; + let Some(mem_tries) = tries.get_mem_tries(shard_uid) else { + // TODO(#12019): what if node doesn't have memtrie? just pause + // processing? + tracing::error!( + "Memtrie not loaded. Cannot process memtrie resharding storage + update for block {:?}, shard {:?}", + block_hash, + shard_uid + ); + return Err(Error::Other("Memtrie not loaded".to_string())); + }; + + // TODO(#12019): take proper boundary account. + let boundary_account = AccountId::from_str("boundary.near").unwrap(); + + // TODO(#12019): leave only tracked shards. + for (new_shard_uid, retain_mode) in [ + (children_shard_uids[0], RetainMode::Left), + (children_shard_uids[1], RetainMode::Right), + ] { + let mut mem_tries = mem_tries.write().unwrap(); + let mem_trie_update = mem_tries.update(*chunk_extra.state_root(), true)?; + + let (trie_changes, _) = + mem_trie_update.retain_split_shard(boundary_account.clone(), retain_mode); + let partial_state = PartialState::default(); + let partial_storage = PartialStorage { nodes: partial_state }; + let mem_changes = trie_changes.mem_trie_changes.as_ref().unwrap(); + let new_state_root = mem_tries.apply_memtrie_changes(block_height, mem_changes); + // TODO(#12019): set all fields of `ChunkExtra`. Consider stronger + // typing. Clarify where it should happen when `State` and + // `FlatState` update is implemented. + let mut child_chunk_extra = ChunkExtra::clone(&chunk_extra); + *child_chunk_extra.state_root_mut() = new_state_root; + + let state_transition_data = StoredChunkStateTransitionData { + base_state: partial_storage.nodes, + receipts_hash: CryptoHash::default(), + }; + + // TODO(store): Use proper store interface + let mut store_update = self.store.store_update(); + store_update.set_ser( + DBCol::ChunkExtra, + &get_block_shard_uid(block_hash, &new_shard_uid), + &child_chunk_extra, + )?; + store_update.set_ser( + DBCol::StateTransitionData, + &get_block_shard_id(block_hash, new_shard_uid.shard_id()), + &state_transition_data, + )?; + tries.apply_insertions( + &trie_changes, + new_shard_uid, + &mut store_update.trie_store_update(), + ); + store_update.commit()?; + } + + Ok(()) + } + + // TODO(store): Use proper store interface + fn get_chunk_extra( + &self, + block_hash: &CryptoHash, + shard_uid: &ShardUId, + ) -> Result, Error> { + let key = get_block_shard_uid(block_hash, shard_uid); + let value = self + .store + .get_ser(DBCol::ChunkExtra, &key) + .map_err(|e| Error::DBNotFoundErr(e.to_string()))?; + value.ok_or(Error::DBNotFoundErr( + format_args!("CHUNK EXTRA: {}:{:?}", block_hash, shard_uid).to_string(), + )) + } +} diff --git a/chain/chain/src/resharding/mod.rs b/chain/chain/src/resharding/mod.rs index 9358c434c8c..8a316f46fdd 100644 --- a/chain/chain/src/resharding/mod.rs +++ b/chain/chain/src/resharding/mod.rs @@ -1,3 +1,4 @@ +pub mod manager; pub mod resharding_v2; pub use resharding_v2 as v2; diff --git a/chain/chain/src/resharding/resharding_v2.rs b/chain/chain/src/resharding/resharding_v2.rs index 04f4de89d72..e2e56bde68e 100644 --- a/chain/chain/src/resharding/resharding_v2.rs +++ b/chain/chain/src/resharding/resharding_v2.rs @@ -366,8 +366,8 @@ impl Chain { state_root, next_epoch_shard_layout, curr_poll_time: Duration::ZERO, - config: self.resharding_config.clone(), - handle: self.resharding_handle.clone(), + config: self.resharding_manager.resharding_config.clone(), + handle: self.resharding_manager.resharding_handle.clone(), on_demand: true, }; diff --git a/chain/client/src/client_actor.rs b/chain/client/src/client_actor.rs index ca1af1f4513..f43121d4a3d 100644 --- a/chain/client/src/client_actor.rs +++ b/chain/client/src/client_actor.rs @@ -165,7 +165,7 @@ pub fn start_client( partial_witness_adapter, ) .unwrap(); - let resharding_handle = client.chain.resharding_handle.clone(); + let resharding_handle = client.chain.resharding_manager.resharding_handle.clone(); let client_sender_for_sync_jobs = LateBoundSender::::new(); let sync_jobs_actor = SyncJobsActor::new(client_sender_for_sync_jobs.as_multi_sender()); diff --git a/core/store/src/trie/resharding_v2.rs b/core/store/src/trie/resharding_v2.rs index 6233a3169df..7a15243dd7d 100644 --- a/core/store/src/trie/resharding_v2.rs +++ b/core/store/src/trie/resharding_v2.rs @@ -2,8 +2,8 @@ use crate::adapter::trie_store::TrieStoreUpdateAdapter; use crate::adapter::StoreUpdateAdapter; use crate::flat::FlatStateChanges; use crate::{ - get, get_delayed_receipt_indices, get_promise_yield_indices, set, ShardTries, Trie, - TrieAccess as _, TrieUpdate, + get, get_delayed_receipt_indices, get_promise_yield_indices, set, ShardTries, TrieAccess, + TrieUpdate, }; use borsh::BorshDeserialize; use bytesize::ByteSize; @@ -11,23 +11,11 @@ use near_primitives::account::id::AccountId; use near_primitives::errors::StorageError; use near_primitives::receipt::{PromiseYieldTimeout, Receipt}; use near_primitives::shard_layout::ShardUId; -use near_primitives::state_part::PartId; use near_primitives::trie_key::trie_key_parsers::parse_account_id_from_raw_key; use near_primitives::trie_key::TrieKey; use near_primitives::types::{StateChangeCause, StateRoot}; use std::collections::HashMap; -use super::iterator::TrieItem; - -impl Trie { - // TODO(#9446) remove function when shifting to flat storage iteration for resharding - pub fn get_trie_items_for_part(&self, part_id: PartId) -> Result, StorageError> { - let path_begin = self.find_state_part_boundary(part_id.idx, part_id.total)?; - let path_end = self.find_state_part_boundary(part_id.idx + 1, part_id.total)?; - self.disk_iter()?.get_trie_items(&path_begin, &path_end) - } -} - impl ShardTries { /// add `values` (key-value pairs of items stored in states) to build states for new shards /// `state_roots` contains state roots for the new shards From 1acf3bd6a77e2702a4cf0192dae50458251d25e7 Mon Sep 17 00:00:00 2001 From: Ivan Frolov <59515280+frolvanya@users.noreply.github.com> Date: Mon, 30 Sep 2024 05:09:38 -0400 Subject: [PATCH 36/49] docs: added info about new status codes (#11869) New `jsonrpc` version introduces new status codes that can be returned in case of failure during method execution --------- Co-authored-by: Yurii Koba --- chain/jsonrpc/CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/chain/jsonrpc/CHANGELOG.md b/chain/jsonrpc/CHANGELOG.md index 213b6cbefa6..f30b5795985 100644 --- a/chain/jsonrpc/CHANGELOG.md +++ b/chain/jsonrpc/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog + +## 2.2.0 + +* Starting from this version we decided to use nearcore's version system + +### Breaking changes + +* Updated `rpc_handler` function ([#11806](https://github.com/near/nearcore/pull/11806) and [#11822](https://github.com/nearprotocol/nearcore/pull/11822)). The `jsonrpc` will start returning other HTTP codes than 200 OK for some errors: + * On internal server error it will return 500 + * On timeout error it will return 408 + * On request validation error will return 400 + ## 0.2.3 * Added `send_tx` method which gives configurable execution guarantees options and potentially replaces existing `broadcast_tx_async`, `broadcast_tx_commit` From 68324b1aecf28fbb643ec237917b12c8136f6786 Mon Sep 17 00:00:00 2001 From: Yurii Koba Date: Mon, 30 Sep 2024 12:36:53 +0300 Subject: [PATCH 37/49] (jsonrpc): Make trait `RpcRequest` a public and reimport `near_jsonrpc_primitives` as `primitives` (#12169) This PR makes the `RpcRequest` trait public to allow for broader usage in external crates. Additionally, it re-imports `near_jsonrpc_primitives` as `primitives` to simplify references and improve code readability. This change is aimed at improving modularity and maintainability in the codebase. --- chain/jsonrpc/src/api/mod.rs | 2 +- chain/jsonrpc/src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/chain/jsonrpc/src/api/mod.rs b/chain/jsonrpc/src/api/mod.rs index 1feb48a44da..3d8f5feb899 100644 --- a/chain/jsonrpc/src/api/mod.rs +++ b/chain/jsonrpc/src/api/mod.rs @@ -22,7 +22,7 @@ mod status; mod transactions; mod validator; -pub(crate) trait RpcRequest: Sized { +pub trait RpcRequest: Sized { fn parse(value: Value) -> Result; } diff --git a/chain/jsonrpc/src/lib.rs b/chain/jsonrpc/src/lib.rs index 6a5e7e6c7a7..a8b499c0258 100644 --- a/chain/jsonrpc/src/lib.rs +++ b/chain/jsonrpc/src/lib.rs @@ -4,8 +4,7 @@ use actix_cors::Cors; use actix_web::http::header; use actix_web::HttpRequest; use actix_web::{get, http, middleware, web, App, Error as HttpError, HttpResponse, HttpServer}; -use api::RpcRequest; -pub use api::{RpcFrom, RpcInto}; +pub use api::{RpcFrom, RpcInto, RpcRequest}; use near_async::actix::ActixResult; use near_async::messaging::{ AsyncSendError, AsyncSender, CanSend, MessageWithCallback, SendAsync, Sender, @@ -19,6 +18,7 @@ use near_client::{ }; use near_client_primitives::types::GetSplitStorageInfo; pub use near_jsonrpc_client as client; +pub use near_jsonrpc_primitives as primitives; use near_jsonrpc_primitives::errors::{RpcError, RpcErrorKind}; use near_jsonrpc_primitives::message::{Message, Request}; use near_jsonrpc_primitives::types::config::{RpcProtocolConfigError, RpcProtocolConfigResponse}; From eb7ceec56264cc51d9b10df997cc0c07304dbc23 Mon Sep 17 00:00:00 2001 From: Waclaw Banasik Date: Mon, 30 Sep 2024 12:51:23 +0100 Subject: [PATCH 38/49] feat(resharding) - Introducing ShardLayoutV2 and nighshade layout v4 (#12066) Adding a new shard layout structure. It replaces the boundary accounts with a mapping from shard id to the account range of that shard. This is necessary in order to implement the account id to shard id mapping. Previously it relied on shard ids being contiguous and ordered by the account ranges, neither of which will be the case in this shard layout. --------- Co-authored-by: Andrea --- core/primitives/src/shard_layout.rs | 333 ++++++++++++++++++++++++--- core/store/src/adapter/flat_store.rs | 6 +- 2 files changed, 308 insertions(+), 31 deletions(-) diff --git a/core/primitives/src/shard_layout.rs b/core/primitives/src/shard_layout.rs index f15ab81b10d..766d453346e 100644 --- a/core/primitives/src/shard_layout.rs +++ b/core/primitives/src/shard_layout.rs @@ -1,9 +1,10 @@ use crate::hash::CryptoHash; use crate::types::{AccountId, NumShards}; use borsh::{BorshDeserialize, BorshSerialize}; +use itertools::Itertools; use near_primitives_core::types::ShardId; use near_schema_checker_lib::ProtocolSchema; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::{fmt, str}; /// This file implements two data structure `ShardLayout` and `ShardUId` @@ -52,6 +53,7 @@ pub type ShardVersion = u32; pub enum ShardLayout { V0(ShardLayoutV0), V1(ShardLayoutV1), + V2(ShardLayoutV2), } /// A shard layout that maps accounts evenly across all shards -- by calculate the hash of account @@ -67,11 +69,24 @@ pub struct ShardLayoutV0 { version: ShardVersion, } -/// A map that maps shards from the last shard layout to shards that it splits to in this shard layout. -/// Instead of using map, we just use a vec here because shard_id ranges from 0 to num_shards-1 -/// For example, if a shard layout with only shard 0 splits into shards 0, 1, 2, 3, the ShardsSplitMap -/// will be `[[0, 1, 2, 3]]` -type ShardSplitMap = Vec>; +/// Maps shards from the last shard layout to shards that it splits to in this +/// shard layout. Instead of using map, we just use a vec here because shard_id +/// ranges from 0 to num_shards-1. +/// +/// For example, if a shard layout with only shard 0 splits into shards 0, 1, +/// 2, 3, the ShardsSplitMap will be `[[0, 1, 2, 3]]` +type ShardsSplitMap = Vec>; + +/// A mapping from the parent shard to child shards. It maps shards from the +/// previous shard layout to shards that they split to in this shard layout. +/// This structure is first used in ShardLayoutV2. +/// +/// For example if a shard layout with shards [0, 2, 5] splits shard 2 into +/// shards [6, 7] the ShardSplitMapV3 will be: 0 => [0] 2 => [6, 7] 5 => [5] +type ShardsSplitMapV2 = BTreeMap>; + +/// A mapping from the child shard to the parent shard. +type ShardsParentMapV2 = BTreeMap; #[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)] pub struct ShardLayoutV1 { @@ -83,7 +98,7 @@ pub struct ShardLayoutV1 { /// Maps shards from the last shard layout to shards that it splits to in this shard layout, /// Useful for constructing states for the shards. /// None for the genesis shard layout - shards_split_map: Option, + shards_split_map: Option, /// Maps shard in this shard layout to their parent shard /// Since shard_ids always range from 0 to num_shards - 1, we use vec instead of a hashmap to_parent_shard_map: Option>, @@ -91,6 +106,69 @@ pub struct ShardLayoutV1 { version: ShardVersion, } +impl ShardLayoutV1 { + // In this shard layout the accounts are divided into ranges, each range is + // mapped to a shard. The shards are contiguous and start from 0. + fn account_id_to_shard_id(&self, account_id: &AccountId) -> ShardId { + let mut shard_id: ShardId = 0; + for boundary_account in &self.boundary_accounts { + if account_id < boundary_account { + break; + } + shard_id += 1; + } + shard_id + } +} + +/// Making the shard ids non-contiguous. +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct ShardLayoutV2 { + /// The boundary accounts are the accounts on boundaries between shards. + /// Each shard contains a range of accounts from one boundary account to + /// another - or the smallest or largest account possible. The total + /// number of shards is equal to the number of boundary accounts plus 1. + /// + /// The shard ids do not need to be contiguous or sorted. + boundary_accounts: Vec, + + /// The shard ids corresponding to the shards defined by the boundary + /// accounts. The invariant between boundary_accounts and shard_ids is that + /// boundary_accounts.len() + 1 == shard_ids.len(). + /// + /// The shard id at index i corresponds to the shard with account range: + /// [boundary_accounts[i -1], boundary_accounts[i]). + /// + shard_ids: Vec, + + /// A mapping from the parent shard to child shards. Maps shards from the + /// previous shard layout to shards that they split to in this shard layout. + shards_split_map: Option, + /// A mapping from the child shard to the parent shard. Maps shards in this + /// shard layout to their parent shards. + shards_parent_map: Option, + + /// The version of the shard layout. Starting from the ShardLayoutV2 the + /// version is no longer updated with every shard layout change and it does + /// not uniquely identify the shard layout. + version: ShardVersion, +} + +impl ShardLayoutV2 { + pub fn account_id_to_shard_id(&self, account_id: &AccountId) -> ShardId { + // TODO(resharding) - This could be optimized. + + let mut shard_id_index = 0; + for boundary_account in &self.boundary_accounts { + if account_id < boundary_account { + break; + } + shard_id_index += 1; + } + self.shard_ids[shard_id_index] + } +} + #[derive(Debug)] pub enum ShardLayoutError { InvalidShardIdError { shard_id: ShardId }, @@ -110,7 +188,7 @@ impl ShardLayout { /// Return a V1 Shardlayout pub fn v1( boundary_accounts: Vec, - shards_split_map: Option, + shards_split_map: Option, version: ShardVersion, ) -> Self { let to_parent_shard_map = if let Some(shards_split_map) = &shards_split_map { @@ -135,6 +213,52 @@ impl ShardLayout { }) } + /// Return a V2 Shardlayout + pub fn v2( + boundary_accounts: Vec, + shard_ids: Vec, + shards_split_map: Option, + ) -> Self { + // In the v2 layout the version is not updated with every shard layout. + const VERSION: ShardVersion = 3; + + assert_eq!(boundary_accounts.len() + 1, shard_ids.len()); + assert_eq!(boundary_accounts, boundary_accounts.iter().sorted().cloned().collect_vec()); + + let Some(shards_split_map) = shards_split_map else { + return Self::V2(ShardLayoutV2 { + boundary_accounts, + shard_ids, + shards_split_map: None, + shards_parent_map: None, + version: VERSION, + }); + }; + + let mut shards_parent_map = ShardsParentMapV2::new(); + for (&parent_shard_id, shard_ids) in shards_split_map.iter() { + for &shard_id in shard_ids { + let prev = shards_parent_map.insert(shard_id, parent_shard_id); + assert!(prev.is_none(), "no shard should appear in the map twice"); + } + } + + assert_eq!( + shard_ids.iter().copied().sorted().collect_vec(), + shards_parent_map.keys().copied().collect_vec() + ); + + let shards_split_map = Some(shards_split_map); + let shards_parent_map = Some(shards_parent_map); + Self::V2(ShardLayoutV2 { + boundary_accounts, + shard_ids, + shards_split_map, + shards_parent_map, + version: VERSION, + }) + } + /// Returns a V1 ShardLayout. It is only used in tests pub fn v1_test() -> Self { ShardLayout::v1( @@ -186,6 +310,41 @@ impl ShardLayout { ) } + /// Returns the simple nightshade layout, version 4, that will be used in + /// production. It adds a new boundary account splitting the "game.hot.tg" + /// shard into two smaller shards. This is the first layout used in the + /// Instant Resharding and it is the first one where the shard id contiguity + /// is broken. + /// + /// TODO(resharding) Determine the shard layout for v4. + /// This layout is provisional, the actual shard layout should be determined + /// based on the fresh data before the resharding. + pub fn get_simple_nightshade_layout_v4() -> ShardLayout { + // the boundary accounts in lexicographical order + let boundary_accounts = vec![ + "aurora".parse().unwrap(), + "aurora-0".parse().unwrap(), + "game.hot.tg".parse().unwrap(), + "game.hot.tg-0".parse().unwrap(), + "kkuuue2akv_1630967379.near".parse().unwrap(), + "tge-lockup.sweat".parse().unwrap(), + ]; + + let shard_ids = vec![0, 1, 6, 7, 3, 4, 5]; + + let shards_split_map = BTreeMap::from([ + (0, vec![0]), + (1, vec![1]), + (2, vec![6, 7]), + (3, vec![3]), + (4, vec![4]), + (5, vec![5]), + ]); + let shards_split_map = Some(shards_split_map); + + ShardLayout::v2(boundary_accounts, shard_ids, shards_split_map) + } + /// This layout is used only in resharding tests. It allows testing of any features which were /// introduced after the last layout upgrade in production. Currently it is built on top of V3. #[cfg(feature = "nightly")] @@ -224,6 +383,10 @@ impl ShardLayout { Some(shards_split_map) => shards_split_map.get(parent_shard_id as usize).cloned(), None => None, }, + Self::V2(v2) => match &v2.shards_split_map { + Some(shards_split_map) => shards_split_map.get(&parent_shard_id).cloned(), + None => None, + }, } } @@ -243,6 +406,10 @@ impl ShardLayout { Some(to_parent_shard_map) => *to_parent_shard_map.get(shard_id as usize).unwrap(), None => panic!("shard_layout has no parent shard"), }, + Self::V2(v2) => match &v2.shards_parent_map { + Some(to_parent_shard_map) => *to_parent_shard_map.get(&shard_id).unwrap(), + None => panic!("shard_layout has no parent shard"), + }, }; Ok(parent_shard_id) } @@ -252,6 +419,7 @@ impl ShardLayout { match self { Self::V0(v0) => v0.version, Self::V1(v1) => v1.version, + Self::V2(v2) => v2.version, } } @@ -259,11 +427,16 @@ impl ShardLayout { match self { Self::V0(v0) => v0.num_shards, Self::V1(v1) => (v1.boundary_accounts.len() + 1) as NumShards, + Self::V2(v2) => (v2.shard_ids.len()) as NumShards, } } - pub fn shard_ids(&self) -> impl Iterator { - 0..self.num_shards() + pub fn shard_ids(&self) -> impl Iterator + '_ { + match self { + Self::V0(_) => (0..self.num_shards()).collect_vec().into_iter(), + Self::V1(_) => (0..self.num_shards()).collect_vec().into_iter(), + Self::V2(v2) => v2.shard_ids.clone().into_iter(), + } } /// Returns an iterator that iterates over all the shard uids for all the @@ -275,7 +448,9 @@ impl ShardLayout { /// Maps an account to the shard that it belongs to given a shard_layout /// For V0, maps according to hash of account id -/// For V1, accounts are divided to ranges, each range of account is mapped to a shard. +/// For V1 and V2, accounts are divided to ranges, each range of account is mapped to a shard. +/// +/// TODO(wacban) This would be nicer as a method in ShardLayout pub fn account_id_to_shard_id(account_id: &AccountId, shard_layout: &ShardLayout) -> ShardId { match shard_layout { ShardLayout::V0(ShardLayoutV0 { num_shards, .. }) => { @@ -283,19 +458,8 @@ pub fn account_id_to_shard_id(account_id: &AccountId, shard_layout: &ShardLayout let (bytes, _) = stdx::split_array::<32, 8, 24>(hash.as_bytes()); u64::from_le_bytes(*bytes) % num_shards } - ShardLayout::V1(ShardLayoutV1 { boundary_accounts, .. }) => { - // Note: As we scale up the number of shards we can consider - // changing this method to do a binary search rather than linear - // scan. For the time being, with only 4 shards, this is perfectly fine. - let mut shard_id: ShardId = 0; - for boundary_account in boundary_accounts { - if account_id < boundary_account { - break; - } - shard_id += 1; - } - shard_id - } + ShardLayout::V1(v1) => v1.account_id_to_shard_id(account_id), + ShardLayout::V2(v2) => v2.account_id_to_shard_id(account_id), } } @@ -338,7 +502,12 @@ impl ShardUId { res } - pub fn next_shard_prefix(shard_uid_bytes: &[u8; 8]) -> [u8; 8] { + /// Get the db key which is strictly bigger than all keys in DB for this + /// shard and still doesn't include keys from other shards. + /// + /// Please note that the returned db key may not correspond to a valid shard + /// uid and it may not be used to get the next shard uid. + pub fn get_upper_bound_db_key(shard_uid_bytes: &[u8; 8]) -> [u8; 8] { let mut result = *shard_uid_bytes; for i in (0..8).rev() { if result[i] == u8::MAX { @@ -503,15 +672,16 @@ impl<'de> serde::de::Visitor<'de> for ShardUIdVisitor { mod tests { use crate::epoch_manager::{AllEpochConfig, EpochConfig, ValidatorSelectionConfig}; use crate::shard_layout::{account_id_to_shard_id, ShardLayout, ShardLayoutV1, ShardUId}; + use itertools::Itertools; use near_primitives_core::types::ProtocolVersion; use near_primitives_core::types::{AccountId, ShardId}; use near_primitives_core::version::{ProtocolFeature, PROTOCOL_VERSION}; use rand::distributions::Alphanumeric; use rand::rngs::StdRng; use rand::{Rng, SeedableRng}; - use std::collections::HashMap; + use std::collections::{BTreeMap, HashMap}; - use super::{ShardSplitMap, ShardVersion}; + use super::{ShardVersion, ShardsSplitMap}; // The old ShardLayoutV1, before fixed shards were removed. tests only #[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)] @@ -524,7 +694,7 @@ mod tests { /// Maps shards from the last shard layout to shards that it splits to in this shard layout, /// Useful for constructing states for the shards. /// None for the genesis shard layout - shards_split_map: Option, + shards_split_map: Option, /// Maps shard in this shard layout to their parent shard /// Since shard_ids always range from 0 to num_shards - 1, we use vec instead of a hashmap to_parent_shard_map: Option>, @@ -653,12 +823,64 @@ mod tests { ids.into_iter().map(|a| a.parse().unwrap()).collect() } + #[test] + fn test_shard_layout_v2() { + let shard_layout = get_test_shard_layout_v2(); + + // check accounts mapping in the middle of each range + assert_eq!(account_id_to_shard_id(&"aaa".parse().unwrap(), &shard_layout), 3); + assert_eq!(account_id_to_shard_id(&"ddd".parse().unwrap(), &shard_layout), 8); + assert_eq!(account_id_to_shard_id(&"mmm".parse().unwrap(), &shard_layout), 4); + assert_eq!(account_id_to_shard_id(&"rrr".parse().unwrap(), &shard_layout), 7); + + // check accounts mapping for the boundary accounts + assert_eq!(account_id_to_shard_id(&"ccc".parse().unwrap(), &shard_layout), 8); + assert_eq!(account_id_to_shard_id(&"kkk".parse().unwrap(), &shard_layout), 4); + assert_eq!(account_id_to_shard_id(&"ppp".parse().unwrap(), &shard_layout), 7); + + // check shard ids + assert_eq!(shard_layout.shard_ids().collect_vec(), vec![3, 8, 4, 7]); + + // check shard uids + let version = 3; + let u = |shard_id| ShardUId { shard_id, version }; + assert_eq!(shard_layout.shard_uids().collect_vec(), vec![u(3), u(8), u(4), u(7)]); + + // check parent + assert_eq!(shard_layout.get_parent_shard_id(3).unwrap(), 3); + assert_eq!(shard_layout.get_parent_shard_id(8).unwrap(), 1); + assert_eq!(shard_layout.get_parent_shard_id(4).unwrap(), 4); + assert_eq!(shard_layout.get_parent_shard_id(7).unwrap(), 1); + + // check child + assert_eq!(shard_layout.get_children_shards_ids(1).unwrap(), vec![7, 8]); + assert_eq!(shard_layout.get_children_shards_ids(3).unwrap(), vec![3]); + assert_eq!(shard_layout.get_children_shards_ids(4).unwrap(), vec![4]); + } + + fn get_test_shard_layout_v2() -> ShardLayout { + let b0 = "ccc".parse().unwrap(); + let b1 = "kkk".parse().unwrap(); + let b2 = "ppp".parse().unwrap(); + + let boundary_accounts = vec![b0, b1, b2]; + let shard_ids = vec![3, 8, 4, 7]; + + // the mapping from parent to the child + // shard 1 is split into shards 7 & 8 while other shards stay the same + let shards_split_map = BTreeMap::from([(1, vec![7, 8]), (3, vec![3]), (4, vec![4])]); + let shards_split_map = Some(shards_split_map); + + ShardLayout::v2(boundary_accounts, shard_ids, shards_split_map) + } + #[test] fn test_shard_layout_all() { let v0 = ShardLayout::v0(1, 0); let v1 = ShardLayout::get_simple_nightshade_layout(); let v2 = ShardLayout::get_simple_nightshade_layout_v2(); let v3 = ShardLayout::get_simple_nightshade_layout_v3(); + let v4 = ShardLayout::get_simple_nightshade_layout_v4(); insta::assert_snapshot!(serde_json::to_string_pretty(&v0).unwrap(), @r###" { @@ -769,6 +991,61 @@ mod tests { } } "###); + + insta::assert_snapshot!(serde_json::to_string_pretty(&v4).unwrap(), @r###" + { + "V2": { + "boundary_accounts": [ + "aurora", + "aurora-0", + "game.hot.tg", + "game.hot.tg-0", + "kkuuue2akv_1630967379.near", + "tge-lockup.sweat" + ], + "shard_ids": [ + 0, + 1, + 6, + 7, + 3, + 4, + 5 + ], + "shards_split_map": { + "0": [ + 0 + ], + "1": [ + 1 + ], + "2": [ + 6, + 7 + ], + "3": [ + 3 + ], + "4": [ + 4 + ], + "5": [ + 5 + ] + }, + "shards_parent_map": { + "0": 0, + "1": 1, + "3": 3, + "4": 4, + "5": 5, + "6": 2, + "7": 2 + }, + "version": 3 + } + } + "###); } #[test] diff --git a/core/store/src/adapter/flat_store.rs b/core/store/src/adapter/flat_store.rs index 07eabea27e6..6cb8d2e687e 100644 --- a/core/store/src/adapter/flat_store.rs +++ b/core/store/src/adapter/flat_store.rs @@ -162,12 +162,12 @@ impl FlatStoreAdapter { Some(from) => encode_flat_state_db_key(shard_uid, from), None => shard_uid.to_bytes().to_vec(), }; - // If right direction is unbounded, `ShardUId::next_shard_prefix` serves as + // If right direction is unbounded, `ShardUId::get_upper_bound_db_key` serves as // the key which is strictly bigger than all keys in DB for this shard and // still doesn't include keys from other shards. let db_key_to = match to { Some(to) => encode_flat_state_db_key(shard_uid, to), - None => ShardUId::next_shard_prefix(&shard_uid.to_bytes()).to_vec(), + None => ShardUId::get_upper_bound_db_key(&shard_uid.to_bytes()).to_vec(), }; let iter = self .store @@ -269,7 +269,7 @@ impl<'a> FlatStoreUpdateAdapter<'a> { // helper fn remove_range_by_shard_uid(&mut self, shard_uid: ShardUId, col: DBCol) { let key_from = shard_uid.to_bytes(); - let key_to = ShardUId::next_shard_prefix(&key_from); + let key_to = ShardUId::get_upper_bound_db_key(&key_from); self.store_update.delete_range(col, &key_from, &key_to); } } From 2b69e649f547473620c2dcecc0c76c75e2ecad06 Mon Sep 17 00:00:00 2001 From: Andrei <122784628+andrei-near@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:39:42 +0300 Subject: [PATCH 39/49] Nayduck dev tests workflow (#12170) Run more tests in Nayduck. Need this workflow to identify and fix flaky tests before making them blocking. Added handler for workflow cancel, test in https://github.com/near/nearcore/actions/runs/11107751344/job/30859091936 --- .github/workflows/nayduck_ci.yml | 10 ++++- .github/workflows/nayduck_ci_dev.yml | 62 ++++++++++++++++++++++++++++ nightly/ci_dev.txt | 2 + 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/nayduck_ci_dev.yml create mode 100644 nightly/ci_dev.txt diff --git a/.github/workflows/nayduck_ci.yml b/.github/workflows/nayduck_ci.yml index ed0c6c9dfe7..b1b993496d6 100644 --- a/.github/workflows/nayduck_ci.yml +++ b/.github/workflows/nayduck_ci.yml @@ -6,6 +6,7 @@ on: jobs: nayduck_tests: + name: "Run Nayduck CI tests" runs-on: ubuntu-latest environment: development timeout-minutes: 60 @@ -27,10 +28,12 @@ jobs: echo ${{ secrets.NAYDUCK_CODE }} > ~/.config/nayduck-code - name: Run Nayduck tests and wait for results + id: nayduck_run run: | NEW_TEST=$(python3 ./scripts/nayduck.py --test-file nightly/ci.txt) RUN_ID="$(echo $NEW_TEST | grep https | sed -E 's|.*\/run\/([0-9]+)|\1|' | sed 's/\x1B\[[0-9;]\{1,\}[A-Za-z]//g')" - URL="https://nayduck.nearone.org/api/run/$RUN_ID" + echo "nayduck_run_id=$RUN_ID" >> $GITHUB_OUTPUT + sleep 10 # wait all the tests to finish @@ -51,3 +54,8 @@ jobs: echo "Fix them before merging" exit 1 fi + + - name: Cleanup Nayduck tests on cancel + if: cancelled() + run: | + python3 ./scripts/nayduck.py -c ${{ steps.nayduck_run.outputs.nayduck_run_id }} diff --git a/.github/workflows/nayduck_ci_dev.yml b/.github/workflows/nayduck_ci_dev.yml new file mode 100644 index 00000000000..62c7a1fe876 --- /dev/null +++ b/.github/workflows/nayduck_ci_dev.yml @@ -0,0 +1,62 @@ +name: CI Nayduck DEV tests +on: + merge_group: + workflow_dispatch: + +jobs: + nayduck_dev_tests: + name: "Run Nayduck CI DEV tests" + runs-on: ubuntu-latest + environment: development + timeout-minutes: 120 + + steps: + - name: Install JQ json processor + run: sudo apt install jq + + - name: Checkout nearcore repository + uses: actions/checkout@v4 + + - name: Install required python modules + run: | + pip3 install -r ./pytest/requirements.txt + + - name: Create nayduck-code file + run: | + echo ${{ secrets.NAYDUCK_CODE }} > ~/.config/nayduck-code + + - name: Run Nayduck tests and wait for results + id: nayduck_run + run: | + # Wait 1 min to ensure Nayduck workers pool is not + # blocked while running merge-blocking tests from ci.txt + sleep 60 + NEW_TEST=$(python3 ./scripts/nayduck.py --test-file nightly/ci_dev.txt) + RUN_ID="$(echo $NEW_TEST | grep https | sed -E 's|.*\/run\/([0-9]+)|\1|' | sed 's/\x1B\[[0-9;]\{1,\}[A-Za-z]//g')" + echo "nayduck_run_id=$RUN_ID" >> $GITHUB_OUTPUT + + sleep 10 + + # wait all the tests to finish + while true; do + TEST_RESULTS=$(curl -s https://nayduck.nearone.org/api/run/$RUN_ID) + TESTS_NOT_READY=$(jq '.tests | .[] | select(.status == "RUNNING" or .status == "PENDING") ' <<< ${TEST_RESULTS} ) + if [ -z "$TESTS_NOT_READY" ]; then break; fi + echo "Tests are not ready yet. Sleeping 1 minute..." + sleep 60 + done + + UNSUCCESSFUL_TESTS=$(jq '.tests | .[] | select(.status != "PASSED" and .status != "IGNORED") ' <<< ${TEST_RESULTS} ) + if [ -z "$UNSUCCESSFUL_TESTS" ]; then + echo "Nayduck CI tests passed." + echo "Results available at https://nayduck.nearone.org/#/run/$RUN_ID" + else + echo "CI Nayduck tests are failing https://nayduck.nearone.org/#/run/$RUN_ID." + echo "Fix them before merging" + exit 1 + fi + + - name: Cleanup Nayduck tests on cancel + if: cancelled() + run: | + python3 ./scripts/nayduck.py -c ${{ steps.nayduck_run.outputs.nayduck_run_id }} \ No newline at end of file diff --git a/nightly/ci_dev.txt b/nightly/ci_dev.txt new file mode 100644 index 00000000000..95cdfb46eee --- /dev/null +++ b/nightly/ci_dev.txt @@ -0,0 +1,2 @@ +./sandbox.txt +./expensive.txt \ No newline at end of file From 359564cd4a411ca0148b30ae338d816401253efb Mon Sep 17 00:00:00 2001 From: Razvan Barbascu Date: Tue, 1 Oct 2024 00:12:52 +0100 Subject: [PATCH 40/49] feat(forknet): Move neard_runner.py to systemd (#12165) Created neard-runner.service that will manage the lifecycle of neard_runner.py. This will increase the reliability by restarting the process when it crashes and managing logs. To migrate an existing mocknet to the new service: ``` mirror run-cmd --cmd "pkill -f neard_runner" mirror restart-neard-runner --upload-program ``` The logs for the new services can be queried by: ``` # old logs journalctl -ru neard-runner # follow logs journalctl -fu neard-runner ``` --- pytest/tests/mocknet/remote_node.py | 37 ++++++++++++++++++----------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/pytest/tests/mocknet/remote_node.py b/pytest/tests/mocknet/remote_node.py index 3448cd19ab0..b608aef50f8 100644 --- a/pytest/tests/mocknet/remote_node.py +++ b/pytest/tests/mocknet/remote_node.py @@ -33,13 +33,10 @@ def init(self): cmd_utils.init_node(self.node) def mk_neard_runner_home(self, remove_home_dir): + cmd = f'mkdir -p {self.neard_runner_home}' if remove_home_dir: - cmd_utils.run_cmd( - self.node, - f'rm -rf {self.neard_runner_home} && mkdir -p {self.neard_runner_home}' - ) - else: - cmd_utils.run_cmd(self.node, f'mkdir -p {self.neard_runner_home}') + cmd = f'rm -rf {self.neard_runner_home} && {cmd}' + cmd_utils.run_cmd(self.node, cmd) def upload_neard_runner(self): self.node.machine.upload('tests/mocknet/helpers/neard_runner.py', @@ -68,16 +65,28 @@ def update_python(self): cmd_utils.run_cmd(self.node, cmd) def stop_neard_runner(self): - # this looks for python processes with neard_runner.py in the command line. the first word will - # be the pid, which we extract with the last awk command - self.node.machine.run( - 'kill $(ps -C python -o pid=,cmd= | grep neard_runner.py | awk \'{print $1};\')' - ) + self.node.machine.run('sudo systemctl stop neard-runner;\ + sudo systemctl reset-failed neard-runner') def start_neard_runner(self): - cmd_utils.run_in_background(self.node, f'{os.path.join(self.neard_runner_home, "venv/bin/python")} {os.path.join(self.neard_runner_home, "neard_runner.py")} ' \ - f'--home {self.neard_runner_home} --neard-home /home/ubuntu/.near ' \ - '--neard-logs /home/ubuntu/neard-logs --port 3000', 'neard-runner.txt') + USER = 'ubuntu' + NEARD_RUNNER_CMD = f'{self.neard_runner_home}/venv/bin/python {self.neard_runner_home}/neard_runner.py\ + --home {self.neard_runner_home}\ + --neard-home "/home/ubuntu/.near"\ + --neard-logs-dir "/home/ubuntu/neard-logs"\ + --port 3000' + + SYSTEMD_RUN_NEARD_RUNNER_CMD = f'sudo systemd-run -u neard-runner\ + --uid={USER} \ + --property=StartLimitIntervalSec=500\ + --property=StartLimitBurst=10\ + --property=DefaultDependencies=no\ + --property=TimeoutStartSec=300\ + --property=Restart=always\ + --property=RestartSec=5s\ + -- {NEARD_RUNNER_CMD}' + + self.node.machine.run(SYSTEMD_RUN_NEARD_RUNNER_CMD) def neard_runner_post(self, body): body = json.dumps(body) From 810e820c571d11e63a5f4824fe2c9fcca6132535 Mon Sep 17 00:00:00 2001 From: Aleksandr Logunov Date: Tue, 1 Oct 2024 21:44:00 +0400 Subject: [PATCH 41/49] test: testloop stub for resharding v3 (#12156) ### Goal Write stub for test for resharding v3 switch. For this, I want the chain to switch between shard layouts. And for that, I switch to `EpochConfigStore` as much as I can, which implies skipping `use_production_config`, overrides like `AllEpochConfig::config_max_kickout_stake`, `EpochConfig` generations from `GenesisConfig`. This is a big step towards #11265. The most visible changes are: Now TestLoop generates `genesis_and_epoch_config_store` instead of just `genesis`. Later we should have a separate `EpochConfigStoreBuilder` which may accept some data shared between genesis and epoch configs, e.g. validators set. This is done to minimise changes. `EpochManager::new_arc_handle` is the way how epoch manager is constructed on production nodes. Its logic is changed as follows: * if chain = mainnet/testnet, only `EpochConfigStore::for_chain_id` is used for getting epoch configs. * if `chain_id.starts_with("test-chain-")`, we use only `EpochConfig::from(genesis_config)` (see below!) * otherwise, we use only `Genesis::test_epoch_config`. **It doesn't use any genesis data**, just stays in this crate for now for convenience. This is for simple tests in single module. ### Achievements * `test_fix_min_stake_ratio` tests exactly what we want - we take `EpochConfigStore::for_chain_id("mainnet")` and see that it allows to include small validator after protocol upgrade. * In `test_resharding_v3` we define old and new shard layouts, and test the switch explicitly without hidden overrides. * Usage of hacky overrides is reduced. For example, `EpochManager::new_from_genesis_config_with_test_overrides` is removed. * If we want to launch forknet with custom epoch config, the behaviour will be more straightforward. For example, one can copy latest epoch config from mainnet to mocknet/ folder and add new condition to `for_epoch_id` for custom mocknet chain name. ### Failures Nayduck often configures epoch config through genesis, e.g. by setting `block_producer_kickout_threshold` to 0. It is much more work to change this configuration, so I add a hack: if chain_id starts with `test-chain-` - name which nayduck uses - epoch config is derived from genesis. Many old integration tests use this chain id as well. However, the improvement here is that we generate only **one** epoch config, without any overrides. epoch_length is sometimes taken from `ChainGenesis`, not from `EpochConfig`. To be safe, I set epoch length in both genesis and epoch configs. This still lacks testing on live node. Using this on canary or forknet could be insightful. --- chain/chain/src/runtime/mod.rs | 6 +- chain/chunks/src/client.rs | 1 - chain/client/src/client.rs | 6 +- chain/client/src/sync/state.rs | 26 +- .../client/src/test_utils/test_env_builder.rs | 48 +++- chain/epoch-manager/src/lib.rs | 99 ++++++-- chain/epoch-manager/src/reward_calculator.rs | 5 +- core/chain-configs/src/genesis_config.rs | 37 ++- core/chain-configs/src/lib.rs | 2 + core/chain-configs/src/test_genesis.rs | 226 +++++++----------- core/chain-configs/src/test_utils.rs | 70 ++++-- core/primitives-core/src/version.rs | 3 + core/primitives/src/epoch_manager.rs | 69 ++++-- core/primitives/src/shard_layout.rs | 8 + integration-tests/README.md | 4 +- integration-tests/src/test_loop/builder.rs | 31 ++- .../tests/chunk_validator_kickout.rs | 4 +- .../src/test_loop/tests/congestion_control.rs | 5 +- .../congestion_control_genesis_bootstrap.rs | 8 +- .../src/test_loop/tests/epoch_sync.rs | 18 +- .../test_loop/tests/fix_min_stake_ratio.rs | 18 +- .../src/test_loop/tests/in_memory_tries.rs | 9 +- integration-tests/src/test_loop/tests/mod.rs | 1 + .../tests/multinode_stateless_validators.rs | 4 +- .../tests/multinode_test_loop_example.rs | 4 +- .../src/test_loop/tests/resharding_v3.rs | 98 ++++++++ .../tests/simple_test_loop_example.rs | 2 +- .../src/test_loop/tests/syncing.rs | 10 +- .../tests/view_requests_to_archival_node.rs | 3 +- .../src/test_loop/utils/setups.rs | 4 +- .../tests/client/features/in_memory_tries.rs | 6 +- nearcore/src/config.rs | 10 +- 32 files changed, 574 insertions(+), 271 deletions(-) create mode 100644 integration-tests/src/test_loop/tests/resharding_v3.rs diff --git a/chain/chain/src/runtime/mod.rs b/chain/chain/src/runtime/mod.rs index 9bbfe219368..3713aa3e8bf 100644 --- a/chain/chain/src/runtime/mod.rs +++ b/chain/chain/src/runtime/mod.rs @@ -101,7 +101,11 @@ impl NightshadeRuntime { let runtime = Runtime::new(); let trie_viewer = TrieViewer::new(trie_viewer_state_size_limit, max_gas_burnt_view); let flat_storage_manager = FlatStorageManager::new(store.flat_store()); - let shard_uids: Vec<_> = genesis_config.shard_layout.shard_uids().collect(); + let epoch_config = epoch_manager + .read() + .get_config_for_protocol_version(genesis_config.protocol_version) + .unwrap(); + let shard_uids: Vec<_> = epoch_config.shard_layout.shard_uids().collect(); let tries = ShardTries::new( store.trie_store(), trie_config, diff --git a/chain/chunks/src/client.rs b/chain/chunks/src/client.rs index 8add457285a..8e00394519f 100644 --- a/chain/chunks/src/client.rs +++ b/chain/chunks/src/client.rs @@ -129,7 +129,6 @@ impl ShardedTransactionPool { "resharding the transaction pool" ); debug_assert!(old_shard_layout != new_shard_layout); - debug_assert!(old_shard_layout.version() + 1 == new_shard_layout.version()); let mut transactions = vec![]; diff --git a/chain/client/src/client.rs b/chain/client/src/client.rs index 943630212ac..9a13abeba08 100644 --- a/chain/client/src/client.rs +++ b/chain/client/src/client.rs @@ -2467,7 +2467,10 @@ impl Client { assert_eq!(sync_hash, state_sync_info.epoch_tail_hash); let network_adapter = self.network_adapter.clone(); - let shards_to_split = self.get_shards_to_split(sync_hash, &state_sync_info, &me)?; + // I *think* this is not relevant anymore, since we download + // already the next epoch's state. + // let shards_to_split = self.get_shards_to_split(sync_hash, &state_sync_info, &me)?; + let shards_to_split = HashMap::new(); let state_sync_timeout = self.config.state_sync_timeout; let block_header = self.chain.get_block(&sync_hash)?.header().clone(); let epoch_id = block_header.epoch_id(); @@ -2574,6 +2577,7 @@ impl Client { /// /// Returns a map from the shard_id to ShardSyncDownload only for those /// shards that need to be split. + #[allow(unused)] fn get_shards_to_split( &mut self, sync_hash: CryptoHash, diff --git a/chain/client/src/sync/state.rs b/chain/client/src/sync/state.rs index 681f32a01e1..d4dccac3e7e 100644 --- a/chain/client/src/sync/state.rs +++ b/chain/client/src/sync/state.rs @@ -291,7 +291,7 @@ impl StateSync { panic!("Resharding V2 scheduling is no longer supported") } ShardSyncStatus::ReshardingApplying => { - panic!("Resharding V2 scheduling is no longer supported") + panic!("Resharding V2 applying is no longer supported") } ShardSyncStatus::StateSyncDone => { shard_sync_done = true; @@ -1013,7 +1013,7 @@ impl StateSync { shard_uid: ShardUId, chain: &mut Chain, sync_hash: CryptoHash, - need_to_reshard: bool, + #[allow(unused)] need_to_reshard: bool, shard_sync_download: &mut ShardSyncDownload, ) -> Result { // Keep waiting until our shard is on the list of results @@ -1026,16 +1026,18 @@ impl StateSync { result?; chain.set_state_finalize(shard_uid.shard_id(), sync_hash)?; - if need_to_reshard { - // If the shard layout is changing in this epoch - we have to apply it right now. - let status = ShardSyncStatus::ReshardingScheduling; - *shard_sync_download = ShardSyncDownload { downloads: vec![], status }; - } else { - // If there is no layout change - we're done. - let status = ShardSyncStatus::StateSyncDone; - *shard_sync_download = ShardSyncDownload { downloads: vec![], status }; - shard_sync_done = true; - } + // I *think* this is not relevant anymore, since we download + // already the next epoch's state. + // if need_to_reshard { + // // If the shard layout is changing in this epoch - we have to apply it right now. + // let status = ShardSyncStatus::ReshardingScheduling; + // *shard_sync_download = ShardSyncDownload { downloads: vec![], status }; + // } + + // If there is no layout change - we're done. + let status = ShardSyncStatus::StateSyncDone; + *shard_sync_download = ShardSyncDownload { downloads: vec![], status }; + shard_sync_done = true; Ok(shard_sync_done) } diff --git a/chain/client/src/test_utils/test_env_builder.rs b/chain/client/src/test_utils/test_env_builder.rs index 1364296e135..38c3f08e59a 100644 --- a/chain/client/src/test_utils/test_env_builder.rs +++ b/chain/client/src/test_utils/test_env_builder.rs @@ -17,14 +17,14 @@ use near_epoch_manager::{EpochManager, EpochManagerAdapter, EpochManagerHandle}; use near_network::test_utils::MockPeerManagerAdapter; use near_parameters::RuntimeConfigStore; use near_primitives::epoch_info::RngSeed; -use near_primitives::epoch_manager::AllEpochConfigTestOverrides; +use near_primitives::epoch_manager::{AllEpochConfigTestOverrides, EpochConfig, EpochConfigStore}; use near_primitives::test_utils::create_test_signer; use near_primitives::types::{AccountId, NumShards}; use near_store::config::StateSnapshotType; use near_store::test_utils::create_test_store; use near_store::{NodeStorage, ShardUId, Store, StoreConfig, TrieConfig}; use near_vm_runner::{ContractRuntimeCache, FilesystemContractRuntimeCache}; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::path::PathBuf; use std::sync::Arc; @@ -47,6 +47,7 @@ impl EpochManagerKind { pub struct TestEnvBuilder { clock: Option, genesis_config: GenesisConfig, + epoch_config_store: Option, clients: Vec, validators: Vec, home_dirs: Option>, @@ -78,6 +79,7 @@ impl TestEnvBuilder { Self { clock: None, genesis_config, + epoch_config_store: None, clients, validators, home_dirs: None, @@ -114,6 +116,12 @@ impl TestEnvBuilder { self } + pub fn epoch_config_store(mut self, epoch_config_store: EpochConfigStore) -> Self { + assert!(self.epoch_config_store.is_none(), "Cannot set epoch_config_store twice"); + self.epoch_config_store = Some(epoch_config_store); + self + } + /// Sets random seed for each client according to the provided HashMap. pub fn clients_random_seeds(mut self, seeds: HashMap) -> Self { self.seeds = seeds; @@ -256,12 +264,32 @@ impl TestEnvBuilder { "Cannot set both num_shards and epoch_managers at the same time" ); let ret = self.ensure_stores(); + + // TODO(#11265): consider initialising epoch config separately as it + // should be decoupled from the genesis config. + // However, there are a lot of tests which only initialise genesis. + let mut base_epoch_config: EpochConfig = (&ret.genesis_config).into(); + if let Some(block_producer_kickout_threshold) = + test_overrides.block_producer_kickout_threshold + { + base_epoch_config.block_producer_kickout_threshold = block_producer_kickout_threshold; + } + if let Some(chunk_producer_kickout_threshold) = + test_overrides.chunk_producer_kickout_threshold + { + base_epoch_config.chunk_producer_kickout_threshold = chunk_producer_kickout_threshold; + } + let epoch_config_store = EpochConfigStore::test(BTreeMap::from_iter(vec![( + ret.genesis_config.protocol_version, + Arc::new(base_epoch_config), + )])); + let epoch_managers = (0..ret.clients.len()) .map(|i| { - EpochManager::new_arc_handle_with_test_overrides( + EpochManager::new_arc_handle_from_epoch_config_store( ret.stores.as_ref().unwrap()[i].clone(), &ret.genesis_config, - Some(test_overrides.clone()), + epoch_config_store.clone(), ) }) .collect(); @@ -274,6 +302,18 @@ impl TestEnvBuilder { if ret.epoch_managers.is_some() { return ret; } + if let Some(epoch_config_store) = &ret.epoch_config_store { + let epoch_managers = (0..ret.clients.len()) + .map(|i| { + EpochManager::new_arc_handle_from_epoch_config_store( + ret.stores.as_ref().unwrap()[i].clone(), + &ret.genesis_config, + epoch_config_store.clone(), + ) + }) + .collect(); + return ret.epoch_managers(epoch_managers); + } ret.epoch_managers_with_test_overrides(AllEpochConfigTestOverrides::default()) } diff --git a/chain/epoch-manager/src/lib.rs b/chain/epoch-manager/src/lib.rs index b905b975552..c0c5036247d 100644 --- a/chain/epoch-manager/src/lib.rs +++ b/chain/epoch-manager/src/lib.rs @@ -2,13 +2,12 @@ use crate::metrics::{PROTOCOL_VERSION_NEXT, PROTOCOL_VERSION_VOTES}; use near_cache::SyncLruCache; -use near_chain_configs::GenesisConfig; +use near_chain_configs::{Genesis, GenesisConfig}; use near_primitives::block::{BlockHeader, Tip}; use near_primitives::epoch_block_info::{BlockInfo, SlashState}; use near_primitives::epoch_info::EpochInfo; use near_primitives::epoch_manager::{ - AllEpochConfig, AllEpochConfigTestOverrides, EpochConfig, EpochSummary, ShardConfig, - AGGREGATOR_KEY, + AllEpochConfig, EpochConfig, EpochConfigStore, EpochSummary, ShardConfig, AGGREGATOR_KEY, }; use near_primitives::errors::EpochError; use near_primitives::hash::CryptoHash; @@ -179,17 +178,8 @@ impl EpochManager { store: Store, genesis_config: &GenesisConfig, ) -> Result { - Self::new_from_genesis_config_with_test_overrides(store, genesis_config, None) - } - - pub fn new_from_genesis_config_with_test_overrides( - store: Store, - genesis_config: &GenesisConfig, - test_overrides: Option, - ) -> Result { - let reward_calculator = RewardCalculator::new(genesis_config); - let all_epoch_config = - Self::new_all_epoch_config_with_test_overrides(genesis_config, test_overrides); + let reward_calculator = RewardCalculator::new(genesis_config, genesis_config.epoch_length); + let all_epoch_config = Self::new_all_epoch_config(genesis_config); Self::new( store, all_epoch_config, @@ -200,36 +190,95 @@ impl EpochManager { } pub fn new_arc_handle(store: Store, genesis_config: &GenesisConfig) -> Arc { - Self::new_arc_handle_with_test_overrides(store, genesis_config, None) + let chain_id = genesis_config.chain_id.as_str(); + if chain_id == near_primitives::chains::MAINNET + || chain_id == near_primitives::chains::TESTNET + { + let epoch_config_store = EpochConfigStore::for_chain_id(chain_id).unwrap(); + return Self::new_arc_handle_from_epoch_config_store( + store, + genesis_config, + epoch_config_store, + ); + } + + let epoch_config = if chain_id.starts_with("test-chain-") { + // We still do this for localnet as nayduck depends on it. + // TODO(#11265): remove this dependency for tests using + // `random_chain_id`. + EpochConfig::from(genesis_config) + } else { + Genesis::test_epoch_config( + genesis_config.num_block_producer_seats, + genesis_config.shard_layout.clone(), + genesis_config.epoch_length, + ) + }; + + let epoch_config_store = EpochConfigStore::test(BTreeMap::from_iter(vec![( + genesis_config.protocol_version, + Arc::new(epoch_config), + )])); + Self::new_arc_handle_from_epoch_config_store(store, genesis_config, epoch_config_store) } - pub fn new_arc_handle_with_test_overrides( + /// DEPRECATED. + /// Old version of deriving epoch config from genesis config. + /// Can be used for testing. + /// Keep it until #11265 is closed and the new code is released. + #[allow(unused)] + pub fn new_arc_handle_deprecated( store: Store, genesis_config: &GenesisConfig, - test_overrides: Option, ) -> Arc { + let reward_calculator = RewardCalculator::new(genesis_config, genesis_config.epoch_length); + let all_epoch_config = Self::new_all_epoch_config(genesis_config); Arc::new( - Self::new_from_genesis_config_with_test_overrides( + Self::new( store, - genesis_config, - test_overrides, + all_epoch_config, + genesis_config.protocol_version, + reward_calculator, + genesis_config.validators(), ) .unwrap() .into_handle(), ) } - fn new_all_epoch_config_with_test_overrides( + pub fn new_arc_handle_from_epoch_config_store( + store: Store, genesis_config: &GenesisConfig, - test_overrides: Option, - ) -> AllEpochConfig { + epoch_config_store: EpochConfigStore, + ) -> Arc { + let genesis_protocol_version = genesis_config.protocol_version; + let epoch_length = genesis_config.epoch_length; + let reward_calculator = RewardCalculator::new(genesis_config, epoch_length); + let all_epoch_config = AllEpochConfig::from_epoch_config_store( + genesis_config.chain_id.as_str(), + epoch_length, + epoch_config_store, + ); + Arc::new( + Self::new( + store, + all_epoch_config, + genesis_protocol_version, + reward_calculator, + genesis_config.validators(), + ) + .unwrap() + .into_handle(), + ) + } + + fn new_all_epoch_config(genesis_config: &GenesisConfig) -> AllEpochConfig { let initial_epoch_config = EpochConfig::from(genesis_config); - let epoch_config = AllEpochConfig::new_with_test_overrides( + let epoch_config = AllEpochConfig::new( genesis_config.use_production_config(), genesis_config.protocol_version, initial_epoch_config, &genesis_config.chain_id, - test_overrides, ); epoch_config } diff --git a/chain/epoch-manager/src/reward_calculator.rs b/chain/epoch-manager/src/reward_calculator.rs index 717fc1edf6b..f59f002ea13 100644 --- a/chain/epoch-manager/src/reward_calculator.rs +++ b/chain/epoch-manager/src/reward_calculator.rs @@ -38,16 +38,17 @@ pub struct RewardCalculator { } impl RewardCalculator { - pub fn new(config: &GenesisConfig) -> Self { + pub fn new(config: &GenesisConfig, epoch_length: u64) -> Self { RewardCalculator { max_inflation_rate: config.max_inflation_rate, num_blocks_per_year: config.num_blocks_per_year, - epoch_length: config.epoch_length, + epoch_length, protocol_reward_rate: config.protocol_reward_rate, protocol_treasury_account: config.protocol_treasury_account.clone(), num_seconds_per_year: NUM_SECONDS_IN_A_YEAR, } } + /// Calculate validator reward for an epoch based on their block and chunk production stats. /// Returns map of validators with their rewards and amount of newly minted tokens including to protocol's treasury. /// See spec . diff --git a/core/chain-configs/src/genesis_config.rs b/core/chain-configs/src/genesis_config.rs index 810de152981..cf5045905df 100644 --- a/core/chain-configs/src/genesis_config.rs +++ b/core/chain-configs/src/genesis_config.rs @@ -4,11 +4,15 @@ //! contains `RuntimeConfig`, but we keep it here for now until we figure //! out the better place. use crate::genesis_validate::validate_genesis; +use crate::{ + BLOCK_PRODUCER_KICKOUT_THRESHOLD, CHUNK_PRODUCER_KICKOUT_THRESHOLD, + CHUNK_VALIDATOR_ONLY_KICKOUT_THRESHOLD, FISHERMEN_THRESHOLD, PROTOCOL_UPGRADE_STAKE_THRESHOLD, +}; use anyhow::Context; use chrono::{DateTime, Utc}; use near_config_utils::ValidationError; use near_parameters::{RuntimeConfig, RuntimeConfigView}; -use near_primitives::epoch_manager::EpochConfig; +use near_primitives::epoch_manager::{EpochConfig, ValidatorSelectionConfig}; use near_primitives::shard_layout::ShardLayout; use near_primitives::types::validator_stake::ValidatorStake; use near_primitives::types::StateRoot; @@ -748,6 +752,37 @@ impl Genesis { } } } + + // Create test-only epoch config. + // Not depends on genesis! + // TODO(#11265): move to crate with `EpochConfig`. + pub fn test_epoch_config( + num_block_producer_seats: NumSeats, + shard_layout: ShardLayout, + epoch_length: BlockHeightDelta, + ) -> EpochConfig { + EpochConfig { + epoch_length, + num_block_producer_seats, + num_block_producer_seats_per_shard: vec![ + num_block_producer_seats; + shard_layout.shard_ids().count() + ], + avg_hidden_validator_seats_per_shard: vec![], + target_validator_mandates_per_shard: 68, + validator_max_kickout_stake_perc: 100, + online_min_threshold: Rational32::new(90, 100), + online_max_threshold: Rational32::new(99, 100), + minimum_stake_divisor: 10, + protocol_upgrade_stake_threshold: PROTOCOL_UPGRADE_STAKE_THRESHOLD, + block_producer_kickout_threshold: BLOCK_PRODUCER_KICKOUT_THRESHOLD, + chunk_producer_kickout_threshold: CHUNK_PRODUCER_KICKOUT_THRESHOLD, + chunk_validator_only_kickout_threshold: CHUNK_VALIDATOR_ONLY_KICKOUT_THRESHOLD, + fishermen_threshold: FISHERMEN_THRESHOLD, + shard_layout, + validator_selection_config: ValidatorSelectionConfig::default(), + } + } } /// Config for changes applied to state dump. diff --git a/core/chain-configs/src/lib.rs b/core/chain-configs/src/lib.rs index 9ff0e4fd57e..b38b70dc85a 100644 --- a/core/chain-configs/src/lib.rs +++ b/core/chain-configs/src/lib.rs @@ -91,3 +91,5 @@ pub const TRANSACTION_VALIDITY_PERIOD: NumBlocks = 100; /// Number of seats for block producers pub const NUM_BLOCK_PRODUCER_SEATS: NumSeats = 50; + +pub const FAST_EPOCH_LENGTH: BlockHeightDelta = 60; diff --git a/core/chain-configs/src/test_genesis.rs b/core/chain-configs/src/test_genesis.rs index 7f3e826d7d6..e6c6488b823 100644 --- a/core/chain-configs/src/test_genesis.rs +++ b/core/chain-configs/src/test_genesis.rs @@ -1,7 +1,9 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::sync::Arc; use near_crypto::PublicKey; use near_primitives::account::{AccessKey, Account}; +use near_primitives::epoch_manager::{EpochConfig, EpochConfigStore}; use near_primitives::hash::CryptoHash; use near_primitives::shard_layout::ShardLayout; use near_primitives::state_record::StateRecord; @@ -35,19 +37,14 @@ pub struct TestGenesisBuilder { protocol_version: Option, genesis_height: Option, epoch_length: Option, - shard_layout: Option, min_max_gas_price: Option<(Balance, Balance)>, gas_limit: Option, transaction_validity_period: Option, validators: Option, - minimum_validators_per_shard: Option, - target_validator_mandates_per_shard: Option, protocol_treasury_account: Option, - shuffle_shard_assignment_for_chunk_producers: Option, - kickouts_config: Option, - minimum_stake_ratio: Option, max_inflation_rate: Option, user_accounts: Vec, + epoch_config: Option, } #[derive(Debug, Clone)] @@ -64,13 +61,6 @@ enum ValidatorsSpec { }, } -#[derive(Debug, Clone)] -struct KickoutsConfig { - block_producer_kickout_threshold: u8, - chunk_producer_kickout_threshold: u8, - chunk_validator_only_kickout_threshold: u8, -} - #[derive(Debug, Clone)] struct UserAccount { account_id: AccountId, @@ -83,6 +73,18 @@ impl TestGenesisBuilder { Default::default() } + pub fn epoch_config_mut(&mut self) -> &mut EpochConfig { + if self.epoch_config.is_none() { + let mut epoch_config = + Genesis::test_epoch_config(1, ShardLayout::v0_single_shard(), 100); + epoch_config.block_producer_kickout_threshold = 0; + epoch_config.chunk_producer_kickout_threshold = 0; + epoch_config.chunk_validator_only_kickout_threshold = 0; + self.epoch_config = Some(epoch_config); + } + self.epoch_config.as_mut().unwrap() + } + pub fn chain_id(&mut self, chain_id: String) -> &mut Self { self.chain_id = Some(chain_id); self @@ -119,21 +121,23 @@ impl TestGenesisBuilder { } pub fn shard_layout_single(&mut self) -> &mut Self { - self.shard_layout = Some(ShardLayout::v0_single_shard()); + self.epoch_config_mut().shard_layout = ShardLayout::v0_single_shard(); self } pub fn shard_layout_simple_v1(&mut self, boundary_accounts: &[&str]) -> &mut Self { - self.shard_layout = Some(ShardLayout::v1( + self.epoch_config_mut().shard_layout = ShardLayout::v1( boundary_accounts.iter().map(|a| a.parse().unwrap()).collect(), None, 1, - )); + ); self } + // TODO(#11265): move this and relevant methods to epoch config builder. + // In dynamic resharding world, shard layout will not be static. pub fn shard_layout(&mut self, shard_layout: ShardLayout) -> &mut Self { - self.shard_layout = Some(shard_layout); + self.epoch_config_mut().shard_layout = shard_layout; self } @@ -203,7 +207,8 @@ impl TestGenesisBuilder { } pub fn minimum_stake_ratio(&mut self, minimum_stake_ratio: Rational32) -> &mut Self { - self.minimum_stake_ratio = Some(minimum_stake_ratio); + self.epoch_config_mut().validator_selection_config.minimum_stake_ratio = + minimum_stake_ratio; self } @@ -216,7 +221,8 @@ impl TestGenesisBuilder { &mut self, minimum_validators_per_shard: NumSeats, ) -> &mut Self { - self.minimum_validators_per_shard = Some(minimum_validators_per_shard); + self.epoch_config_mut().validator_selection_config.minimum_validators_per_shard = + minimum_validators_per_shard; self } @@ -224,7 +230,8 @@ impl TestGenesisBuilder { &mut self, target_validator_mandates_per_shard: NumSeats, ) -> &mut Self { - self.target_validator_mandates_per_shard = Some(target_validator_mandates_per_shard); + self.epoch_config_mut().target_validator_mandates_per_shard = + target_validator_mandates_per_shard; self } @@ -237,37 +244,36 @@ impl TestGenesisBuilder { } pub fn shuffle_shard_assignment_for_chunk_producers(&mut self, shuffle: bool) -> &mut Self { - self.shuffle_shard_assignment_for_chunk_producers = Some(shuffle); + self.epoch_config_mut() + .validator_selection_config + .shuffle_shard_assignment_for_chunk_producers = shuffle; self } pub fn kickouts_disabled(&mut self) -> &mut Self { - self.kickouts_config = Some(KickoutsConfig { - block_producer_kickout_threshold: 0, - chunk_producer_kickout_threshold: 0, - chunk_validator_only_kickout_threshold: 0, - }); + let epoch_config = self.epoch_config_mut(); + epoch_config.block_producer_kickout_threshold = 0; + epoch_config.chunk_producer_kickout_threshold = 0; + epoch_config.chunk_validator_only_kickout_threshold = 0; self } /// Validators with performance below 80% are kicked out, similarly to /// mainnet as of 28 Jun 2024. pub fn kickouts_standard_80_percent(&mut self) -> &mut Self { - self.kickouts_config = Some(KickoutsConfig { - block_producer_kickout_threshold: 80, - chunk_producer_kickout_threshold: 80, - chunk_validator_only_kickout_threshold: 80, - }); + let epoch_config = self.epoch_config_mut(); + epoch_config.block_producer_kickout_threshold = 80; + epoch_config.chunk_producer_kickout_threshold = 80; + epoch_config.chunk_validator_only_kickout_threshold = 80; self } /// Only chunk validator-only nodes can be kicked out. pub fn kickouts_for_chunk_validators_only(&mut self) -> &mut Self { - self.kickouts_config = Some(KickoutsConfig { - block_producer_kickout_threshold: 0, - chunk_producer_kickout_threshold: 0, - chunk_validator_only_kickout_threshold: 50, - }); + let epoch_config = self.epoch_config_mut(); + epoch_config.block_producer_kickout_threshold = 0; + epoch_config.chunk_producer_kickout_threshold = 0; + epoch_config.chunk_validator_only_kickout_threshold = 50; self } @@ -284,29 +290,24 @@ impl TestGenesisBuilder { self } - pub fn build(&self) -> Genesis { + pub fn build(mut self) -> (Genesis, EpochConfigStore) { let chain_id = self.chain_id.clone().unwrap_or_else(|| { let default = "test".to_string(); tracing::warn!("Genesis chain_id not explicitly set, defaulting to {:?}.", default); default }); - let genesis_time = self.genesis_time.unwrap_or_else(|| { - let default = chrono::Utc::now(); - tracing::warn!( - "Genesis genesis_time not explicitly set, defaulting to current time {:?}.", - default - ); - default - }); let protocol_version = self.protocol_version.unwrap_or_else(|| { let default = PROTOCOL_VERSION; tracing::warn!("Genesis protocol_version not explicitly set, defaulting to latest protocol version {:?}.", default); default }); - let genesis_height = self.genesis_height.unwrap_or_else(|| { - let default = 1; + let validator_specs = self.validators.clone().unwrap_or_else(|| { + let default = ValidatorsSpec::DesiredRoles { + block_and_chunk_producers: vec!["validator0".to_string()], + chunk_validators_only: vec![], + }; tracing::warn!( - "Genesis genesis_height not explicitly set, defaulting to {:?}.", + "Genesis validators not explicitly set, defaulting to a single validator setup {:?}.", default ); default @@ -316,11 +317,38 @@ impl TestGenesisBuilder { tracing::warn!("Genesis epoch_length not explicitly set, defaulting to {:?}.", default); default }); - let shard_layout = self.shard_layout.clone().unwrap_or_else(|| { + + let derived_validator_setup = derive_validator_setup(validator_specs); + + let mut epoch_config = self.epoch_config_mut().clone(); + epoch_config.num_block_producer_seats = derived_validator_setup.num_block_producer_seats; + epoch_config.validator_selection_config.num_chunk_producer_seats = + derived_validator_setup.num_chunk_producer_seats; + epoch_config.validator_selection_config.num_chunk_validator_seats = + derived_validator_setup.num_chunk_validator_seats; + let epoch_config_store = EpochConfigStore::test(BTreeMap::from_iter(vec![( + protocol_version, + Arc::new(epoch_config), + )])); + let shard_layout = + epoch_config_store.get_config(protocol_version).as_ref().shard_layout.clone(); + + let genesis_time = self.genesis_time.unwrap_or_else(|| { + let default = chrono::Utc::now(); + tracing::warn!( + "Genesis genesis_time not explicitly set, defaulting to current time {:?}.", + default + ); + default + }); + + let genesis_height = self.genesis_height.unwrap_or_else(|| { + let default = 1; tracing::warn!( - "Genesis shard_layout not explicitly set, defaulting to single shard layout." + "Genesis genesis_height not explicitly set, defaulting to {:?}.", + default ); - ShardLayout::v0_single_shard() + default }); let (min_gas_price, max_gas_price) = self.min_max_gas_price.unwrap_or_else(|| { let default = (0, 0); @@ -340,35 +368,7 @@ impl TestGenesisBuilder { ); default }); - let validator_specs = self.validators.clone().unwrap_or_else(|| { - let default = ValidatorsSpec::DesiredRoles { - block_and_chunk_producers: vec!["validator0".to_string()], - chunk_validators_only: vec![], - }; - tracing::warn!( - "Genesis validators not explicitly set, defaulting to a single validator setup {:?}.", - default - ); - default - }); - let derived_validator_setup = derive_validator_setup(validator_specs); - let minimum_validators_per_shard = self.minimum_validators_per_shard.unwrap_or_else(|| { - let default = 1; - tracing::warn!( - "Genesis minimum_validators_per_shard not explicitly set, defaulting to {:?}.", - default - ); - default - }); - let target_validator_mandates_per_shard = - self.target_validator_mandates_per_shard.unwrap_or_else(|| { - let default = 68; - tracing::warn!( - "Genesis minimum_validators_per_shard not explicitly set, defaulting to {:?}.", - default - ); - default - }); + let protocol_treasury_account: AccountId = self .protocol_treasury_account .clone() @@ -382,37 +382,6 @@ impl TestGenesisBuilder { }) .parse() .unwrap(); - let shuffle_shard_assignment_for_chunk_producers = self - .shuffle_shard_assignment_for_chunk_producers - .unwrap_or_else(|| { - let default = false; - tracing::warn!( - "Genesis shuffle_shard_assignment_for_chunk_producers not explicitly set, defaulting to {:?}.", - default - ); - default - }); - let kickouts_config = self.kickouts_config.clone().unwrap_or_else(|| { - let default = KickoutsConfig { - block_producer_kickout_threshold: 0, - chunk_producer_kickout_threshold: 0, - chunk_validator_only_kickout_threshold: 0, - }; - tracing::warn!( - "Genesis kickouts_config not explicitly set, defaulting to disabling kickouts.", - ); - default - }); - let minimum_stake_ratio = self.minimum_stake_ratio.unwrap_or_else(|| { - // Set minimum stake ratio to zero; that way, we don't have to worry about - // chunk producers not having enough stake to be selected as desired. - let default = Rational32::new(0, 1); - tracing::warn!( - "Genesis minimum_stake_ratio not explicitly set, defaulting to {:?}.", - default - ); - default - }); let max_inflation_rate = self.max_inflation_rate.unwrap_or_else(|| { let default = Rational32::new(1, 1); tracing::warn!( @@ -504,11 +473,6 @@ impl TestGenesisBuilder { gas_limit, dynamic_resharding: false, fishermen_threshold: 0, - block_producer_kickout_threshold: kickouts_config.block_producer_kickout_threshold, - chunk_producer_kickout_threshold: kickouts_config.chunk_producer_kickout_threshold, - chunk_validator_only_kickout_threshold: kickouts_config - .chunk_validator_only_kickout_threshold, - target_validator_mandates_per_shard, transaction_validity_period, protocol_version, protocol_treasury_account, @@ -520,33 +484,29 @@ impl TestGenesisBuilder { total_supply, max_kickout_stake_perc: 100, validators: derived_validator_setup.validators, + shard_layout: shard_layout.clone(), num_block_producer_seats: derived_validator_setup.num_block_producer_seats, - num_chunk_only_producer_seats: 0, - minimum_stake_ratio, - minimum_validators_per_shard, - minimum_stake_divisor: 10, - shuffle_shard_assignment_for_chunk_producers, num_block_producer_seats_per_shard: shard_layout .shard_ids() - .map(|_| minimum_validators_per_shard) + .map(|_| derived_validator_setup.num_block_producer_seats) .collect(), - avg_hidden_validator_seats_per_shard: Vec::new(), - shard_layout, + num_chunk_only_producer_seats: 0, + minimum_stake_divisor: 10, max_inflation_rate, protocol_upgrade_stake_threshold: Rational32::new(8, 10), - // Hack to ensure that `FixMinStakeRatio` is tested. - // TODO(#11265): always use production config or `EpochConfigStore` - // instance for testing. - use_production_config: self.minimum_stake_ratio.is_some(), num_chunk_producer_seats: derived_validator_setup.num_chunk_producer_seats, num_chunk_validator_seats: derived_validator_setup.num_chunk_validator_seats, chunk_producer_assignment_changes_limit: 5, + ..Default::default() }; - Genesis { - config: genesis_config, - contents: GenesisContents::Records { records: GenesisRecords(records) }, - } + ( + Genesis { + config: genesis_config, + contents: GenesisContents::Records { records: GenesisRecords(records) }, + }, + epoch_config_store, + ) } } diff --git a/core/chain-configs/src/test_utils.rs b/core/chain-configs/src/test_utils.rs index 8df1fd78eb6..955f482753a 100644 --- a/core/chain-configs/src/test_utils.rs +++ b/core/chain-configs/src/test_utils.rs @@ -3,20 +3,16 @@ use near_primitives::account::{AccessKey, Account}; use near_primitives::hash::CryptoHash; use near_primitives::shard_layout::ShardLayout; use near_primitives::state_record::StateRecord; -use near_primitives::types::{ - AccountId, AccountInfo, Balance, BlockHeightDelta, NumSeats, NumShards, -}; +use near_primitives::types::{AccountId, AccountInfo, Balance, NumSeats, NumShards}; use near_primitives::utils::{from_timestamp, generate_random_string}; use near_primitives::version::PROTOCOL_VERSION; use near_time::Clock; use num_rational::Ratio; use crate::{ - Genesis, GenesisConfig, BLOCK_PRODUCER_KICKOUT_THRESHOLD, CHUNK_PRODUCER_KICKOUT_THRESHOLD, - CHUNK_VALIDATOR_ONLY_KICKOUT_THRESHOLD, FISHERMEN_THRESHOLD, GAS_PRICE_ADJUSTMENT_RATE, - INITIAL_GAS_LIMIT, MAX_INFLATION_RATE, MIN_GAS_PRICE, NEAR_BASE, NUM_BLOCKS_PER_YEAR, - PROTOCOL_REWARD_RATE, PROTOCOL_TREASURY_ACCOUNT, PROTOCOL_UPGRADE_STAKE_THRESHOLD, - TRANSACTION_VALIDITY_PERIOD, + Genesis, GenesisConfig, FAST_EPOCH_LENGTH, GAS_PRICE_ADJUSTMENT_RATE, INITIAL_GAS_LIMIT, + MAX_INFLATION_RATE, MIN_GAS_PRICE, NEAR_BASE, NUM_BLOCKS_PER_YEAR, PROTOCOL_REWARD_RATE, + PROTOCOL_TREASURY_ACCOUNT, TRANSACTION_VALIDITY_PERIOD, }; /// Initial balance used in tests. @@ -25,8 +21,6 @@ pub const TESTING_INIT_BALANCE: Balance = 1_000_000_000 * NEAR_BASE; /// Validator's stake used in tests. pub const TESTING_INIT_STAKE: Balance = 50_000_000 * NEAR_BASE; -pub const FAST_EPOCH_LENGTH: BlockHeightDelta = 60; - impl GenesisConfig { pub fn test(clock: Clock) -> Self { GenesisConfig { @@ -52,7 +46,7 @@ impl Genesis { clock: Clock, accounts: Vec, num_validator_seats: NumSeats, - num_validator_seats_per_shard: Vec, + _num_validator_seats_per_shard: Vec, shard_layout: ShardLayout, ) -> Self { let mut validators = vec![]; @@ -78,21 +72,13 @@ impl Genesis { ); } add_protocol_account(&mut records); + let epoch_config = + Self::test_epoch_config(num_validator_seats, shard_layout, FAST_EPOCH_LENGTH); let config = GenesisConfig { protocol_version: PROTOCOL_VERSION, genesis_time: from_timestamp(clock.now_utc().unix_timestamp_nanos() as u64), chain_id: random_chain_id(), - num_block_producer_seats: num_validator_seats, - num_block_producer_seats_per_shard: num_validator_seats_per_shard.clone(), - avg_hidden_validator_seats_per_shard: vec![0; num_validator_seats_per_shard.len()], dynamic_resharding: false, - protocol_upgrade_stake_threshold: PROTOCOL_UPGRADE_STAKE_THRESHOLD, - epoch_length: FAST_EPOCH_LENGTH, - gas_limit: INITIAL_GAS_LIMIT, - gas_price_adjustment_rate: GAS_PRICE_ADJUSTMENT_RATE, - block_producer_kickout_threshold: BLOCK_PRODUCER_KICKOUT_THRESHOLD, - chunk_producer_kickout_threshold: CHUNK_PRODUCER_KICKOUT_THRESHOLD, - chunk_validator_only_kickout_threshold: CHUNK_VALIDATOR_ONLY_KICKOUT_THRESHOLD, validators, protocol_reward_rate: PROTOCOL_REWARD_RATE, total_supply: get_initial_supply(&records), @@ -100,9 +86,47 @@ impl Genesis { num_blocks_per_year: NUM_BLOCKS_PER_YEAR, protocol_treasury_account: PROTOCOL_TREASURY_ACCOUNT.parse().unwrap(), transaction_validity_period: TRANSACTION_VALIDITY_PERIOD, - fishermen_threshold: FISHERMEN_THRESHOLD, + gas_limit: INITIAL_GAS_LIMIT, + gas_price_adjustment_rate: GAS_PRICE_ADJUSTMENT_RATE, min_gas_price: MIN_GAS_PRICE, - shard_layout, + + // epoch config parameters + num_block_producer_seats: epoch_config.num_block_producer_seats, + num_block_producer_seats_per_shard: epoch_config.num_block_producer_seats_per_shard, + avg_hidden_validator_seats_per_shard: epoch_config.avg_hidden_validator_seats_per_shard, + protocol_upgrade_stake_threshold: epoch_config.protocol_upgrade_stake_threshold, + epoch_length: epoch_config.epoch_length, + block_producer_kickout_threshold: epoch_config.block_producer_kickout_threshold, + chunk_producer_kickout_threshold: epoch_config.chunk_producer_kickout_threshold, + chunk_validator_only_kickout_threshold: epoch_config + .chunk_validator_only_kickout_threshold, + fishermen_threshold: epoch_config.fishermen_threshold, + shard_layout: epoch_config.shard_layout, + target_validator_mandates_per_shard: epoch_config.target_validator_mandates_per_shard, + max_kickout_stake_perc: epoch_config.validator_max_kickout_stake_perc, + online_min_threshold: epoch_config.online_min_threshold, + online_max_threshold: epoch_config.online_max_threshold, + minimum_stake_divisor: epoch_config.minimum_stake_divisor, + num_chunk_producer_seats: epoch_config + .validator_selection_config + .num_chunk_producer_seats, + num_chunk_validator_seats: epoch_config + .validator_selection_config + .num_chunk_validator_seats, + num_chunk_only_producer_seats: epoch_config + .validator_selection_config + .num_chunk_only_producer_seats, + minimum_validators_per_shard: epoch_config + .validator_selection_config + .minimum_validators_per_shard, + minimum_stake_ratio: epoch_config.validator_selection_config.minimum_stake_ratio, + chunk_producer_assignment_changes_limit: epoch_config + .validator_selection_config + .chunk_producer_assignment_changes_limit, + shuffle_shard_assignment_for_chunk_producers: epoch_config + .validator_selection_config + .shuffle_shard_assignment_for_chunk_producers, + ..Default::default() }; Genesis::new(config, records.into()).unwrap() diff --git a/core/primitives-core/src/version.rs b/core/primitives-core/src/version.rs index 63c16f50278..508c8265998 100644 --- a/core/primitives-core/src/version.rs +++ b/core/primitives-core/src/version.rs @@ -172,6 +172,8 @@ pub enum ProtocolFeature { ChunkEndorsementsInBlockHeader, /// Store receipts in State in the StateStoredReceipt format. StateStoredReceipt, + /// Resharding V3 + SimpleNightshadeV4, } impl ProtocolFeature { @@ -248,6 +250,7 @@ impl ProtocolFeature { // TODO(#11201): When stabilizing this feature in mainnet, also remove the temporary code // that always enables this for mocknet (see config_mocknet function). ProtocolFeature::ShuffleShardAssignments => 143, + ProtocolFeature::SimpleNightshadeV4 => 145, } } diff --git a/core/primitives/src/epoch_manager.rs b/core/primitives/src/epoch_manager.rs index 6198093345a..abf74b10fa3 100644 --- a/core/primitives/src/epoch_manager.rs +++ b/core/primitives/src/epoch_manager.rs @@ -8,7 +8,7 @@ use crate::types::{ use borsh::{BorshDeserialize, BorshSerialize}; use near_primitives_core::checked_feature; use near_primitives_core::hash::CryptoHash; -use near_primitives_core::version::ProtocolFeature; +use near_primitives_core::version::{ProtocolFeature, PROTOCOL_VERSION}; use near_schema_checker_lib::ProtocolSchema; use smart_default::SmartDefault; use std::collections::{BTreeMap, HashMap}; @@ -78,7 +78,7 @@ impl ShardConfig { /// Testing overrides to apply to the EpochConfig returned by the `for_protocol_version`. /// All fields should be optional and the default should be a no-op. -#[derive(Clone, Default)] +#[derive(Clone, Debug, Default)] pub struct AllEpochConfigTestOverrides { pub block_producer_kickout_threshold: Option, pub chunk_producer_kickout_threshold: Option, @@ -87,21 +87,25 @@ pub struct AllEpochConfigTestOverrides { /// AllEpochConfig manages protocol configs that might be changing throughout epochs (hence EpochConfig). /// The main function in AllEpochConfig is ::for_protocol_version which takes a protocol version /// and returns the EpochConfig that should be used for this protocol version. -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct AllEpochConfig { /// Store for EpochConfigs, provides configs per protocol version. /// Initialized only for production, ie. when `use_protocol_version` is true. config_store: Option, + /// Chain Id. Some parameters are specific to certain chains. + chain_id: String, + epoch_length: BlockHeightDelta, + /// The fields below are DEPRECATED. + /// Epoch config must be controlled by `config_store` only. + /// TODO(#11265): remove these fields. /// Whether this is for production (i.e., mainnet or testnet). This is a temporary implementation /// to allow us to change protocol config for mainnet and testnet without changing the genesis config - use_production_config: bool, + _use_production_config: bool, /// EpochConfig from genesis - genesis_epoch_config: EpochConfig, - /// Chain Id. Some parameters are specific to certain chains. - chain_id: String, + _genesis_epoch_config: EpochConfig, /// Testing overrides to apply to the EpochConfig returned by the `for_protocol_version`. - test_overrides: AllEpochConfigTestOverrides, + _test_overrides: AllEpochConfigTestOverrides, } impl AllEpochConfig { @@ -120,6 +124,26 @@ impl AllEpochConfig { ) } + pub fn from_epoch_config_store( + chain_id: &str, + epoch_length: BlockHeightDelta, + epoch_config_store: EpochConfigStore, + ) -> Self { + let genesis_epoch_config = epoch_config_store.get_config(PROTOCOL_VERSION).as_ref().clone(); + Self { + config_store: Some(epoch_config_store), + chain_id: chain_id.to_string(), + epoch_length, + // The fields below must be DEPRECATED. Don't use it for epoch + // config creation. + // TODO(#11265): remove them. + _use_production_config: false, + _genesis_epoch_config: genesis_epoch_config, + _test_overrides: AllEpochConfigTestOverrides::default(), + } + } + + /// DEPRECATED. pub fn new_with_test_overrides( use_production_config: bool, genesis_protocol_version: ProtocolVersion, @@ -135,10 +159,11 @@ impl AllEpochConfig { }; let all_epoch_config = Self { config_store: config_store.clone(), - use_production_config, - genesis_epoch_config, chain_id: chain_id.to_string(), - test_overrides: test_overrides.unwrap_or_default(), + epoch_length: genesis_epoch_config.epoch_length, + _use_production_config: use_production_config, + _genesis_epoch_config: genesis_epoch_config, + _test_overrides: test_overrides.unwrap_or_default(), }; // Sanity check: Validate that the stored genesis config equals to the config generated for the genesis protocol version. // Note that we cannot do this in unittests because we do not have direct access to the genesis config for mainnet/testnet. @@ -156,7 +181,13 @@ impl AllEpochConfig { pub fn for_protocol_version(&self, protocol_version: ProtocolVersion) -> EpochConfig { if self.config_store.is_some() { - self.config_store.as_ref().unwrap().get_config(protocol_version).as_ref().clone() + let mut config = + self.config_store.as_ref().unwrap().get_config(protocol_version).as_ref().clone(); + // TODO(#11265): epoch length is overridden in many tests so we + // need to support it here. Consider removing `epoch_length` from + // EpochConfig. + config.epoch_length = self.epoch_length; + config } else { self.generate_epoch_config(protocol_version) } @@ -164,11 +195,11 @@ impl AllEpochConfig { /// TODO(#11265): Remove this and use the stored configs only. pub fn generate_epoch_config(&self, protocol_version: ProtocolVersion) -> EpochConfig { - let mut config = self.genesis_epoch_config.clone(); + let mut config = self._genesis_epoch_config.clone(); Self::config_mocknet(&mut config, &self.chain_id); - if !self.use_production_config { + if !self._use_production_config { return config; } @@ -184,7 +215,7 @@ impl AllEpochConfig { Self::config_chunk_endorsement_thresholds(&mut config, protocol_version); - Self::config_test_overrides(&mut config, &self.test_overrides); + Self::config_test_overrides(&mut config, &self._test_overrides); config } @@ -429,7 +460,7 @@ static CONFIGS: &[(&str, ProtocolVersion, &str)] = &[ ]; /// Store for `[EpochConfig]` per protocol version.` -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct EpochConfigStore { store: BTreeMap>, } @@ -457,10 +488,14 @@ impl EpochConfigStore { } } + pub fn test(store: BTreeMap>) -> Self { + Self { store } + } + /// Returns the EpochConfig for the given protocol version. /// This panics if no config is found for the given version, thus the initialization via `for_chain_id` should /// only be performed for chains with some configs stored in files. - fn get_config(&self, protocol_version: ProtocolVersion) -> &Arc { + pub fn get_config(&self, protocol_version: ProtocolVersion) -> &Arc { self.store .range((Bound::Unbounded, Bound::Included(protocol_version))) .next_back() diff --git a/core/primitives/src/shard_layout.rs b/core/primitives/src/shard_layout.rs index 766d453346e..1a15b88161a 100644 --- a/core/primitives/src/shard_layout.rs +++ b/core/primitives/src/shard_layout.rs @@ -423,6 +423,14 @@ impl ShardLayout { } } + pub fn boundary_accounts(&self) -> &Vec { + match self { + Self::V1(v1) => &v1.boundary_accounts, + Self::V2(v2) => &v2.boundary_accounts, + _ => panic!("ShardLayout::V0 doesn't have boundary accounts"), + } + } + fn num_shards(&self) -> NumShards { match self { Self::V0(v0) => v0.num_shards, diff --git a/integration-tests/README.md b/integration-tests/README.md index a0ebe7f3ccf..82406f993f1 100644 --- a/integration-tests/README.md +++ b/integration-tests/README.md @@ -40,10 +40,10 @@ genesis_builder for account in &accounts { genesis_builder.add_user_account_simple(account.clone(), initial_balance); } -let genesis = genesis_builder.build(); +let (genesis, epoch_config_store) = genesis_builder.build(); let TestLoopEnv { mut test_loop, datas: node_datas } = - builder.genesis(genesis).clients(client_accounts).build(); + builder.genesis(genesis).epoch_config_store(epoch_config_store).clients(client_accounts).build(); ``` ## 2. Trigger and execute events diff --git a/integration-tests/src/test_loop/builder.rs b/integration-tests/src/test_loop/builder.rs index 087866a89ab..9bc3bfb6572 100644 --- a/integration-tests/src/test_loop/builder.rs +++ b/integration-tests/src/test_loop/builder.rs @@ -26,6 +26,7 @@ use near_epoch_manager::shard_tracker::{ShardTracker, TrackedConfig}; use near_epoch_manager::{EpochManager, EpochManagerAdapter}; use near_network::test_loop::{TestLoopNetworkSharedState, TestLoopPeerManagerActor}; use near_parameters::RuntimeConfigStore; +use near_primitives::epoch_manager::EpochConfigStore; use near_primitives::network::PeerId; use near_primitives::test_utils::create_test_signer; use near_primitives::types::AccountId; @@ -44,6 +45,7 @@ use super::utils::network::{chunk_endorsement_dropper, partial_encoded_chunks_dr pub(crate) struct TestLoopBuilder { test_loop: TestLoopV2, genesis: Option, + epoch_config_store: Option, clients: Vec, /// Overrides the stores; rather than constructing fresh new stores, use /// the provided ones (to test with existing data). @@ -77,6 +79,7 @@ impl TestLoopBuilder { Self { test_loop: TestLoopV2::new(), genesis: None, + epoch_config_store: None, clients: vec![], stores_override: None, test_loop_data_dir: None, @@ -102,6 +105,11 @@ impl TestLoopBuilder { self } + pub(crate) fn epoch_config_store(mut self, epoch_config_store: EpochConfigStore) -> Self { + self.epoch_config_store = Some(epoch_config_store); + self + } + /// Set the clients for the test loop. pub(crate) fn clients(mut self, clients: Vec) -> Self { self.clients = clients; @@ -229,7 +237,8 @@ impl TestLoopBuilder { let partial_witness_adapter = LateBoundSender::new(); let sync_jobs_adapter = LateBoundSender::new(); - let genesis = self.genesis.clone().unwrap(); + let genesis = self.genesis.as_ref().unwrap(); + let epoch_config_store = self.epoch_config_store.as_ref().unwrap(); let mut client_config = ClientConfig::test(true, 600, 2000, 4, is_archival, true, false); client_config.max_block_wait_delay = Duration::seconds(6); client_config.state_sync_enabled = true; @@ -261,9 +270,10 @@ impl TestLoopBuilder { // Configure tracked shards. // * single shard tracking for validators // * all shard tracking for non-validators (RPCs and archival) - let num_block_producer = genesis.config.num_block_producer_seats; - let num_chunk_producer = genesis.config.num_chunk_producer_seats; - let num_chunk_validator = genesis.config.num_chunk_validator_seats; + let epoch_config = epoch_config_store.get_config(genesis.config.protocol_version); + let num_block_producer = epoch_config.num_block_producer_seats; + let num_chunk_producer = epoch_config.validator_selection_config.num_chunk_producer_seats; + let num_chunk_validator = epoch_config.validator_selection_config.num_chunk_validator_seats; let validator_num = num_block_producer.max(num_chunk_producer).max(num_chunk_validator) as usize; if idx < validator_num { @@ -299,7 +309,11 @@ impl TestLoopBuilder { let sync_jobs_actor = SyncJobsActor::new(client_adapter.as_multi_sender()); let chain_genesis = ChainGenesis::new(&genesis.config); - let epoch_manager = EpochManager::new_arc_handle(store.clone(), &genesis.config); + let epoch_manager = EpochManager::new_arc_handle_from_epoch_config_store( + store.clone(), + &genesis.config, + epoch_config_store.clone(), + ); let shard_tracker = ShardTracker::new(TrackedConfig::from_config(&client_config), epoch_manager.clone()); @@ -376,8 +390,11 @@ impl TestLoopBuilder { // ViewClientActorInner. Otherwise, we use the regular versions created above. let (view_epoch_manager, view_shard_tracker, view_runtime_adapter) = if let Some(split_store) = &split_store { - let view_epoch_manager = - EpochManager::new_arc_handle(split_store.clone(), &genesis.config); + let view_epoch_manager = EpochManager::new_arc_handle_from_epoch_config_store( + split_store.clone(), + &genesis.config, + epoch_config_store.clone(), + ); let view_shard_tracker = ShardTracker::new( TrackedConfig::from_config(&client_config), epoch_manager.clone(), diff --git a/integration-tests/src/test_loop/tests/chunk_validator_kickout.rs b/integration-tests/src/test_loop/tests/chunk_validator_kickout.rs index e74b4c7a4cc..33cf83e9eb9 100644 --- a/integration-tests/src/test_loop/tests/chunk_validator_kickout.rs +++ b/integration-tests/src/test_loop/tests/chunk_validator_kickout.rs @@ -84,10 +84,10 @@ fn run_test_chunk_validator_kickout(accounts: Vec, test_case: TestCas for account in &accounts { genesis_builder.add_user_account_simple(account.clone(), initial_balance); } - let genesis = genesis_builder.build(); + let (genesis, epoch_config_store) = genesis_builder.build(); let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = - builder.genesis(genesis).clients(clients).build(); + builder.genesis(genesis).epoch_config_store(epoch_config_store).clients(clients).build(); // Run chain until our targeted chunk validator is (not) kicked out. let client_handle = node_datas[0].client_sender.actor_handle(); diff --git a/integration-tests/src/test_loop/tests/congestion_control.rs b/integration-tests/src/test_loop/tests/congestion_control.rs index 0c37e2c4d1d..07c6e659221 100644 --- a/integration-tests/src/test_loop/tests/congestion_control.rs +++ b/integration-tests/src/test_loop/tests/congestion_control.rs @@ -91,9 +91,10 @@ fn setup(accounts: &Vec) -> (TestLoopEnv, AccountId) { for account in accounts { genesis_builder.add_user_account_simple(account.clone(), initial_balance); } - let genesis = genesis_builder.build(); + let (genesis, epoch_config_store) = genesis_builder.build(); - let env = builder.genesis(genesis).clients(clients).build(); + let env = + builder.genesis(genesis).epoch_config_store(epoch_config_store).clients(clients).build(); (env, rpc_id.clone()) } diff --git a/integration-tests/src/test_loop/tests/congestion_control_genesis_bootstrap.rs b/integration-tests/src/test_loop/tests/congestion_control_genesis_bootstrap.rs index 530a4931d23..7fb2a09cfb1 100644 --- a/integration-tests/src/test_loop/tests/congestion_control_genesis_bootstrap.rs +++ b/integration-tests/src/test_loop/tests/congestion_control_genesis_bootstrap.rs @@ -41,8 +41,12 @@ fn test_congestion_control_genesis_bootstrap() { genesis_builder.add_user_account_simple(clients[i].clone(), initial_balance); } - let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = - builder.genesis(genesis_builder.build()).clients(clients.clone()).build(); + let (genesis, epoch_config_store) = genesis_builder.build(); + let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = builder + .genesis(genesis) + .epoch_config_store(epoch_config_store) + .clients(clients.clone()) + .build(); test_loop.run_for(Duration::seconds(5)); diff --git a/integration-tests/src/test_loop/tests/epoch_sync.rs b/integration-tests/src/test_loop/tests/epoch_sync.rs index e94edfc17cc..a26160c7f25 100644 --- a/integration-tests/src/test_loop/tests/epoch_sync.rs +++ b/integration-tests/src/test_loop/tests/epoch_sync.rs @@ -4,6 +4,7 @@ use near_chain_configs::test_genesis::TestGenesisBuilder; use near_chain_configs::{Genesis, GenesisConfig}; use near_client::test_utils::test_loop::ClientQueries; use near_o11y::testonly::init_test_logger; +use near_primitives::epoch_manager::EpochConfigStore; use near_primitives::types::AccountId; use near_store::{DBCol, Store}; use tempfile::TempDir; @@ -27,6 +28,7 @@ use std::rc::Rc; struct TestNetworkSetup { tempdir: TempDir, genesis: Genesis, + epoch_config_store: EpochConfigStore, accounts: Vec, stores: Vec, } @@ -54,10 +56,13 @@ fn setup_initial_blockchain(num_clients: usize) -> TestNetworkSetup { for account in &accounts { genesis_builder.add_user_account_simple(account.clone(), initial_balance); } - let genesis = genesis_builder.build(); + let (genesis, epoch_config_store) = genesis_builder.build(); - let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = - builder.genesis(genesis.clone()).clients(clients).build(); + let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = builder + .genesis(genesis.clone()) + .epoch_config_store(epoch_config_store.clone()) + .clients(clients) + .build(); let first_epoch_tracked_shards = { let clients = node_datas @@ -104,18 +109,19 @@ fn setup_initial_blockchain(num_clients: usize) -> TestNetworkSetup { let tempdir = TestLoopEnv { test_loop, datas: node_datas, tempdir } .shutdown_and_drain_remaining_events(Duration::seconds(5)); - TestNetworkSetup { tempdir, genesis, accounts, stores } + TestNetworkSetup { tempdir, genesis, epoch_config_store, accounts, stores } } fn bootstrap_node_via_epoch_sync(setup: TestNetworkSetup, source_node: usize) -> TestNetworkSetup { tracing::info!("Starting new TestLoopEnv with new node"); - let TestNetworkSetup { genesis, accounts, mut stores, tempdir } = setup; + let TestNetworkSetup { genesis, epoch_config_store, accounts, mut stores, tempdir } = setup; let num_existing_clients = stores.len(); let clients = accounts.iter().take(num_existing_clients + 1).cloned().collect_vec(); stores.push(create_test_store()); // new node starts empty. let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = TestLoopBuilder::new() .genesis(genesis.clone()) + .epoch_config_store(epoch_config_store.clone()) .clients(clients) .stores_override_hot_only(stores) .test_loop_data_dir(tempdir) @@ -250,7 +256,7 @@ fn bootstrap_node_via_epoch_sync(setup: TestNetworkSetup, source_node: usize) -> let tempdir = TestLoopEnv { test_loop, datas: node_datas, tempdir } .shutdown_and_drain_remaining_events(Duration::seconds(5)); - TestNetworkSetup { tempdir, genesis, accounts, stores } + TestNetworkSetup { tempdir, genesis, epoch_config_store, accounts, stores } } // Test that a new node that only has genesis can use Epoch Sync to bring itself diff --git a/integration-tests/src/test_loop/tests/fix_min_stake_ratio.rs b/integration-tests/src/test_loop/tests/fix_min_stake_ratio.rs index b38941f3951..c80b6ecad0e 100644 --- a/integration-tests/src/test_loop/tests/fix_min_stake_ratio.rs +++ b/integration-tests/src/test_loop/tests/fix_min_stake_ratio.rs @@ -9,9 +9,9 @@ use near_async::time::Duration; use near_chain_configs::test_genesis::TestGenesisBuilder; use near_network::client::ProcessTxRequest; use near_o11y::testonly::init_test_logger; +use near_primitives::epoch_manager::EpochConfigStore; use near_primitives::hash::CryptoHash; use near_primitives::num_rational::Rational32; -use near_primitives::shard_layout::ShardLayout; use near_primitives::test_utils::create_user_test_signer; use near_primitives::transaction::SignedTransaction; use near_primitives::types::AccountId; @@ -56,27 +56,29 @@ fn test_fix_min_stake_ratio() { }, ]; + // Take epoch configuration before the protocol upgrade, where minimum + // stake ratio was 1/6250. + let epoch_config_store = EpochConfigStore::for_chain_id("mainnet").unwrap(); + let protocol_version = ProtocolFeature::FixMinStakeRatio.protocol_version() - 1; + // Create chain with version before FixMinStakeRatio was enabled. // Check that the small validator is not included in the validator set. let mut genesis_builder = TestGenesisBuilder::new(); genesis_builder .genesis_time_from_clock(&builder.clock()) - .shard_layout(ShardLayout::get_simple_nightshade_layout_v3()) - .protocol_version(ProtocolFeature::FixMinStakeRatio.protocol_version() - 1) + .shard_layout(epoch_config_store.get_config(protocol_version).as_ref().shard_layout.clone()) + .protocol_version(protocol_version) .epoch_length(epoch_length) .validators_raw(validators, 1, 2) - // For genesis, set high minimum stake ratio so that small validator - // will be excluded from the validator set. - .minimum_stake_ratio(Rational32::new(1, 6_250)) // Disable validator rewards. .max_inflation_rate(Rational32::new(0, 1)); for account in &accounts { genesis_builder.add_user_account_simple(account.clone(), initial_balance); } - let genesis = genesis_builder.build(); + let (genesis, _) = genesis_builder.build(); let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = - builder.genesis(genesis).clients(clients).build(); + builder.genesis(genesis).epoch_config_store(epoch_config_store).clients(clients).build(); let client_sender = node_datas[0].client_sender.clone(); let client_handle = node_datas[0].client_sender.actor_handle(); diff --git a/integration-tests/src/test_loop/tests/in_memory_tries.rs b/integration-tests/src/test_loop/tests/in_memory_tries.rs index 4506eabcc39..3d63b5a90f0 100644 --- a/integration-tests/src/test_loop/tests/in_memory_tries.rs +++ b/integration-tests/src/test_loop/tests/in_memory_tries.rs @@ -46,10 +46,13 @@ fn test_load_memtrie_after_empty_chunks() { for account in &accounts { genesis_builder.add_user_account_simple(account.clone(), initial_balance); } - let genesis = genesis_builder.build(); + let (genesis, epoch_config_store) = genesis_builder.build(); - let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = - builder.genesis(genesis).clients(client_accounts).build(); + let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = builder + .genesis(genesis) + .epoch_config_store(epoch_config_store) + .clients(client_accounts) + .build(); execute_money_transfers(&mut test_loop, &node_datas, &accounts); diff --git a/integration-tests/src/test_loop/tests/mod.rs b/integration-tests/src/test_loop/tests/mod.rs index 29c5ab9348b..22b47c27565 100644 --- a/integration-tests/src/test_loop/tests/mod.rs +++ b/integration-tests/src/test_loop/tests/mod.rs @@ -7,6 +7,7 @@ pub mod in_memory_tries; pub mod max_receipt_size; pub mod multinode_stateless_validators; pub mod multinode_test_loop_example; +mod resharding_v3; pub mod simple_test_loop_example; pub mod syncing; pub mod view_requests_to_archival_node; diff --git a/integration-tests/src/test_loop/tests/multinode_stateless_validators.rs b/integration-tests/src/test_loop/tests/multinode_stateless_validators.rs index c4bef83ed5a..6dcaad085db 100644 --- a/integration-tests/src/test_loop/tests/multinode_stateless_validators.rs +++ b/integration-tests/src/test_loop/tests/multinode_stateless_validators.rs @@ -56,10 +56,10 @@ fn test_stateless_validators_with_multi_test_loop() { for account in &accounts { genesis_builder.add_user_account_simple(account.clone(), initial_balance); } - let genesis = genesis_builder.build(); + let (genesis, epoch_config_store) = genesis_builder.build(); let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = - builder.genesis(genesis).clients(clients).build(); + builder.genesis(genesis).epoch_config_store(epoch_config_store).clients(clients).build(); // Capture the initial validator info in the first epoch. let client_handle = node_datas[0].client_sender.actor_handle(); diff --git a/integration-tests/src/test_loop/tests/multinode_test_loop_example.rs b/integration-tests/src/test_loop/tests/multinode_test_loop_example.rs index 1af69a249a0..3acdd1aa795 100644 --- a/integration-tests/src/test_loop/tests/multinode_test_loop_example.rs +++ b/integration-tests/src/test_loop/tests/multinode_test_loop_example.rs @@ -37,10 +37,10 @@ fn test_client_with_multi_test_loop() { for account in &accounts { genesis_builder.add_user_account_simple(account.clone(), initial_balance); } - let genesis = genesis_builder.build(); + let (genesis, epoch_config_store) = genesis_builder.build(); let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = - builder.genesis(genesis).clients(clients).build(); + builder.genesis(genesis).epoch_config_store(epoch_config_store).clients(clients).build(); let first_epoch_tracked_shards = { let clients = node_datas diff --git a/integration-tests/src/test_loop/tests/resharding_v3.rs b/integration-tests/src/test_loop/tests/resharding_v3.rs new file mode 100644 index 00000000000..ac10f425c27 --- /dev/null +++ b/integration-tests/src/test_loop/tests/resharding_v3.rs @@ -0,0 +1,98 @@ +use itertools::Itertools; +use near_async::test_loop::data::TestLoopData; +use near_async::time::Duration; +use near_chain_configs::test_genesis::TestGenesisBuilder; +use near_o11y::testonly::init_test_logger; +use near_primitives::epoch_manager::EpochConfigStore; +use near_primitives::shard_layout::ShardLayout; +use near_primitives::types::{AccountId, ShardId}; +use near_primitives::version::{ProtocolFeature, PROTOCOL_VERSION}; +use std::collections::BTreeMap; +use std::sync::Arc; + +use crate::test_loop::builder::TestLoopBuilder; +use crate::test_loop::env::TestLoopEnv; +use crate::test_loop::utils::ONE_NEAR; + +/// Stub for checking Resharding V3. +/// After uncommenting panics with +/// StorageInconsistentState("Failed to find root node ... in memtrie") +#[test] +#[ignore] +fn test_resharding_v3() { + if !ProtocolFeature::SimpleNightshadeV4.enabled(PROTOCOL_VERSION) { + return; + } + + init_test_logger(); + let builder = TestLoopBuilder::new(); + + let initial_balance = 1_000_000 * ONE_NEAR; + let epoch_length = 10; + let accounts = + (0..8).map(|i| format!("account{}", i).parse().unwrap()).collect::>(); + let clients = accounts.iter().cloned().collect_vec(); + let block_and_chunk_producers = (0..8).map(|idx| accounts[idx].as_str()).collect_vec(); + // TODO: set up chunk validator-only nodes. + + // Prepare shard split configuration. + let base_epoch_config_store = EpochConfigStore::for_chain_id("mainnet").unwrap(); + let base_protocol_version = ProtocolFeature::SimpleNightshadeV4.protocol_version() - 1; + let mut base_epoch_config = + base_epoch_config_store.get_config(base_protocol_version).as_ref().clone(); + base_epoch_config.validator_selection_config.shuffle_shard_assignment_for_chunk_producers = + false; + let base_shard_layout = base_epoch_config.shard_layout.clone(); + let mut epoch_config = base_epoch_config.clone(); + let mut boundary_accounts = base_shard_layout.boundary_accounts().clone(); + let mut shard_ids: Vec<_> = base_shard_layout.shard_ids().collect(); + let max_shard_id = *shard_ids.iter().max().unwrap(); + let last_shard_id = shard_ids.pop().unwrap(); + let mut shards_split_map: BTreeMap> = + shard_ids.iter().map(|shard_id| (*shard_id, vec![*shard_id])).collect(); + shard_ids.extend([max_shard_id + 1, max_shard_id + 2]); + shards_split_map.insert(last_shard_id, vec![max_shard_id + 1, max_shard_id + 2]); + boundary_accounts.push(AccountId::try_from("x.near".to_string()).unwrap()); + epoch_config.shard_layout = + ShardLayout::v2(boundary_accounts, shard_ids, Some(shards_split_map)); + let expected_num_shards = epoch_config.shard_layout.shard_ids().count(); + let epoch_config_store = EpochConfigStore::test(BTreeMap::from_iter(vec![ + (base_protocol_version, Arc::new(base_epoch_config)), + (base_protocol_version + 1, Arc::new(epoch_config)), + ])); + + let mut genesis_builder = TestGenesisBuilder::new(); + genesis_builder + .genesis_time_from_clock(&builder.clock()) + .shard_layout(base_shard_layout) + .protocol_version(base_protocol_version) + .epoch_length(epoch_length) + .validators_desired_roles(&block_and_chunk_producers, &[]); + for account in &accounts { + genesis_builder.add_user_account_simple(account.clone(), initial_balance); + } + let (genesis, _) = genesis_builder.build(); + + let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = + builder.genesis(genesis).epoch_config_store(epoch_config_store).clients(clients).build(); + + let client_handle = node_datas[0].client_sender.actor_handle(); + let success_condition = |test_loop_data: &mut TestLoopData| -> bool { + let client = &test_loop_data.get(&client_handle).client; + let tip = client.chain.head().unwrap(); + let epoch_height = + client.epoch_manager.get_epoch_height_from_prev_block(&tip.prev_block_hash).unwrap(); + assert!(epoch_height < 5); + let epoch_config = client.epoch_manager.get_epoch_config(&tip.epoch_id).unwrap(); + return epoch_config.shard_layout.shard_ids().count() == expected_num_shards; + }; + + test_loop.run_until( + success_condition, + // Timeout at producing 5 epochs, approximately. + Duration::seconds((5 * epoch_length) as i64), + ); + + TestLoopEnv { test_loop, datas: node_datas, tempdir } + .shutdown_and_drain_remaining_events(Duration::seconds(20)); +} diff --git a/integration-tests/src/test_loop/tests/simple_test_loop_example.rs b/integration-tests/src/test_loop/tests/simple_test_loop_example.rs index 17da7fca629..ebeec2ea319 100644 --- a/integration-tests/src/test_loop/tests/simple_test_loop_example.rs +++ b/integration-tests/src/test_loop/tests/simple_test_loop_example.rs @@ -58,7 +58,7 @@ fn test_client_with_simple_test_loop() { for account in &accounts { genesis_builder.add_user_account_simple(account.clone(), initial_balance); } - let genesis = genesis_builder.build(); + let (genesis, _) = genesis_builder.build(); let store = create_test_store(); initialize_genesis_state(store.clone(), &genesis, None); diff --git a/integration-tests/src/test_loop/tests/syncing.rs b/integration-tests/src/test_loop/tests/syncing.rs index 952fd57c893..2896fa5e782 100644 --- a/integration-tests/src/test_loop/tests/syncing.rs +++ b/integration-tests/src/test_loop/tests/syncing.rs @@ -44,10 +44,13 @@ fn test_sync_from_genesis() { for account in &accounts { genesis_builder.add_user_account_simple(account.clone(), initial_balance); } - let genesis = genesis_builder.build(); + let (genesis, epoch_config_store) = genesis_builder.build(); - let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = - builder.genesis(genesis.clone()).clients(clients).build(); + let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = builder + .genesis(genesis.clone()) + .epoch_config_store(epoch_config_store.clone()) + .clients(clients) + .build(); let first_epoch_tracked_shards = { let clients = node_datas @@ -98,6 +101,7 @@ fn test_sync_from_genesis() { let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = TestLoopBuilder::new() .genesis(genesis.clone()) + .epoch_config_store(epoch_config_store) .clients(clients) .stores_override(stores) .test_loop_data_dir(tempdir) diff --git a/integration-tests/src/test_loop/tests/view_requests_to_archival_node.rs b/integration-tests/src/test_loop/tests/view_requests_to_archival_node.rs index e07698fd0d7..fc8531552c9 100644 --- a/integration-tests/src/test_loop/tests/view_requests_to_archival_node.rs +++ b/integration-tests/src/test_loop/tests/view_requests_to_archival_node.rs @@ -74,10 +74,11 @@ fn test_view_requests_to_archival_node() { for account in &accounts { genesis_builder.add_user_account_simple(account.clone(), initial_balance); } - let genesis = genesis_builder.build(); + let (genesis, epoch_config_store) = genesis_builder.build(); let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = builder .genesis(genesis) + .epoch_config_store(epoch_config_store) .clients(all_clients) .archival_clients(archival_clients) .gc_num_epochs_to_keep(GC_NUM_EPOCHS_TO_KEEP) diff --git a/integration-tests/src/test_loop/utils/setups.rs b/integration-tests/src/test_loop/utils/setups.rs index 1160f712674..97bb16828f0 100644 --- a/integration-tests/src/test_loop/utils/setups.rs +++ b/integration-tests/src/test_loop/utils/setups.rs @@ -48,7 +48,7 @@ pub fn standard_setup_1() -> TestLoopEnv { for account in accounts { genesis_builder.add_user_account_simple(account.clone(), initial_balance); } - let genesis = genesis_builder.build(); + let (genesis, epoch_config_store) = genesis_builder.build(); - builder.genesis(genesis).clients(clients).build() + builder.genesis(genesis).epoch_config_store(epoch_config_store).clients(clients).build() } diff --git a/integration-tests/src/tests/client/features/in_memory_tries.rs b/integration-tests/src/tests/client/features/in_memory_tries.rs index c49d815e7aa..15a56945fc6 100644 --- a/integration-tests/src/tests/client/features/in_memory_tries.rs +++ b/integration-tests/src/tests/client/features/in_memory_tries.rs @@ -67,7 +67,7 @@ fn test_in_memory_trie_node_consistency() { for account in &accounts { genesis_builder.add_user_account_simple(account.clone(), initial_balance); } - let genesis = genesis_builder.build(); + let (genesis, _) = genesis_builder.build(); // Create two stores, one for each node. We'll be reusing the stores later // to emulate node restarts. @@ -474,12 +474,12 @@ fn test_in_memory_trie_consistency_with_state_sync_base_case(track_all_shards: b for account in &accounts { genesis_builder.add_user_account_simple(account.clone(), initial_balance); } - let genesis = genesis_builder.build(); - + let (genesis, epoch_config_store) = genesis_builder.build(); let stores = (0..NUM_VALIDATORS).map(|_| create_test_store()).collect::>(); let mut env = TestEnv::builder(&genesis.config) .clock(clock.clock()) .clients((0..NUM_VALIDATORS).map(|i| format!("account{}", i).parse().unwrap()).collect()) + .epoch_config_store(epoch_config_store) .stores(stores) .maybe_track_all_shards(track_all_shards) .nightshade_runtimes_with_trie_config( diff --git a/nearcore/src/config.rs b/nearcore/src/config.rs index 91dfc0a31ca..57a2d8d1fac 100644 --- a/nearcore/src/config.rs +++ b/nearcore/src/config.rs @@ -5,8 +5,8 @@ use bytesize::ByteSize; use near_async::time::{Clock, Duration}; use near_chain::runtime::NightshadeRuntime; use near_chain_configs::test_utils::{ - add_account_with_key, add_protocol_account, random_chain_id, FAST_EPOCH_LENGTH, - TESTING_INIT_BALANCE, TESTING_INIT_STAKE, + add_account_with_key, add_protocol_account, random_chain_id, TESTING_INIT_BALANCE, + TESTING_INIT_STAKE, }; use near_chain_configs::{ default_enable_multiline_logging, default_epoch_sync, @@ -22,9 +22,9 @@ use near_chain_configs::{ ClientConfig, EpochSyncConfig, GCConfig, Genesis, GenesisConfig, GenesisValidationMode, LogSummaryStyle, MutableConfigValue, MutableValidatorSigner, ReshardingConfig, StateSyncConfig, BLOCK_PRODUCER_KICKOUT_THRESHOLD, CHUNK_PRODUCER_KICKOUT_THRESHOLD, - CHUNK_VALIDATOR_ONLY_KICKOUT_THRESHOLD, EXPECTED_EPOCH_LENGTH, FISHERMEN_THRESHOLD, - GAS_PRICE_ADJUSTMENT_RATE, GENESIS_CONFIG_FILENAME, INITIAL_GAS_LIMIT, MAX_INFLATION_RATE, - MIN_BLOCK_PRODUCTION_DELAY, MIN_GAS_PRICE, NEAR_BASE, NUM_BLOCKS_PER_YEAR, + CHUNK_VALIDATOR_ONLY_KICKOUT_THRESHOLD, EXPECTED_EPOCH_LENGTH, FAST_EPOCH_LENGTH, + FISHERMEN_THRESHOLD, GAS_PRICE_ADJUSTMENT_RATE, GENESIS_CONFIG_FILENAME, INITIAL_GAS_LIMIT, + MAX_INFLATION_RATE, MIN_BLOCK_PRODUCTION_DELAY, MIN_GAS_PRICE, NEAR_BASE, NUM_BLOCKS_PER_YEAR, NUM_BLOCK_PRODUCER_SEATS, PROTOCOL_REWARD_RATE, PROTOCOL_UPGRADE_STAKE_THRESHOLD, TRANSACTION_VALIDITY_PERIOD, }; From ef812f9f3a2e6d3a490194be1e143d069ca04659 Mon Sep 17 00:00:00 2001 From: TinyFoxy Date: Wed, 2 Oct 2024 21:42:13 +0800 Subject: [PATCH 42/49] chore(docs): use master link of `NEP-539` (#12183) since `NEP-539` was merged, it's time to use master link of `NEP-539` in the docs to render better reading experience. :) --- docs/architecture/how/receipt-congestion.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/architecture/how/receipt-congestion.md b/docs/architecture/how/receipt-congestion.md index 0b0010a055d..34f51eec3eb 100644 --- a/docs/architecture/how/receipt-congestion.md +++ b/docs/architecture/how/receipt-congestion.md @@ -48,7 +48,7 @@ For a finite amount of time, we can accept more inflow than outflow, we just hav Next, we look at ideas one at a time before combining some of them into the cross-shard congestion design proposed in -[NEP-539](https://github.com/near/NEPs/pull/539). +[NEP-539](https://github.com/near/NEPs/blob/master/neps/nep-0539.md). ## Idea 1: Compute the minimum max-flow and stay below that limit @@ -185,7 +185,7 @@ that are not on the critical path. ## Putting it all together -The proposal in [NEP-539](https://github.com/near/NEPs/pull/539) combines all +The proposal in [NEP-539](https://github.com/near/NEPs/blob/master/neps/nep-0539.md) combines all ideas 2, 3, and 4. We have a limit of how much memory we consider to be normal operations (for @@ -208,4 +208,4 @@ it back cannot lead to a slowed down global throughput. Another design decision was to linearly interpolate the limits, as opposed to binary on and off states. This way, we don't have to be too precise in finding the right parameters, as the system should balance itself around a specific -limit that works for each workload. \ No newline at end of file +limit that works for each workload. From acfe0ed1f1ea3654837f0e26b364fb062311409c Mon Sep 17 00:00:00 2001 From: Olga Telezhnaya Date: Wed, 2 Oct 2024 15:26:50 +0100 Subject: [PATCH 43/49] Fix release version to 2.3.0 in rpc changelog (#12185) --- chain/jsonrpc/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chain/jsonrpc/CHANGELOG.md b/chain/jsonrpc/CHANGELOG.md index f30b5795985..bc9bc2dbea9 100644 --- a/chain/jsonrpc/CHANGELOG.md +++ b/chain/jsonrpc/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog -## 2.2.0 +## 2.3.0 * Starting from this version we decided to use nearcore's version system From 2b882c855c22f2a98d83c294c3f44d137c2095cc Mon Sep 17 00:00:00 2001 From: Andrei <122784628+andrei-near@users.noreply.github.com> Date: Thu, 3 Oct 2024 10:04:13 +0300 Subject: [PATCH 44/49] Fuzz binaries build workflow fix (#12184) add rand-std to crypto crate successful run https://github.com/near/nearcore/actions/runs/11143409511/job/30968440679 --- .github/workflows/master_fuzzer_binaries.yml | 2 +- .github/workflows/ondemand_fuzzer_binaries.yml | 2 +- core/crypto/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/master_fuzzer_binaries.yml b/.github/workflows/master_fuzzer_binaries.yml index 715a2eb8b62..80c0994de47 100644 --- a/.github/workflows/master_fuzzer_binaries.yml +++ b/.github/workflows/master_fuzzer_binaries.yml @@ -7,7 +7,7 @@ on: jobs: build_fuzzers: name: Build Fuzzers - runs-on: "ubuntu-20.04-32core" + runs-on: "ubuntu-22.04-16core" permissions: contents: "read" diff --git a/.github/workflows/ondemand_fuzzer_binaries.yml b/.github/workflows/ondemand_fuzzer_binaries.yml index b2f05916127..006192abb84 100644 --- a/.github/workflows/ondemand_fuzzer_binaries.yml +++ b/.github/workflows/ondemand_fuzzer_binaries.yml @@ -23,7 +23,7 @@ on: jobs: build_fuzzers: name: Build Fuzzers - runs-on: "ubuntu-20.04-32core" + runs-on: "ubuntu-22.04-16core" permissions: contents: "read" diff --git a/core/crypto/Cargo.toml b/core/crypto/Cargo.toml index 82835b8d15a..3eeeacfa366 100644 --- a/core/crypto/Cargo.toml +++ b/core/crypto/Cargo.toml @@ -44,7 +44,7 @@ curve25519-dalek = { workspace = true, features = ["rand_core"] } [features] default = ["rand"] rand = ["secp256k1/rand", "rand/getrandom", "ed25519-dalek/rand_core"] - +rand-std = ["secp256k1/rand-std"] protocol_schema = [ "near-schema-checker-lib/protocol_schema", ] From a9bf310048cc036537e59394ce4d6cd414dfd966 Mon Sep 17 00:00:00 2001 From: Marcelo Diop-Gonzalez Date: Fri, 4 Oct 2024 00:28:15 -0400 Subject: [PATCH 45/49] fix(mirror): fix a crash in the mirror traffic generator (#12168) In queue_block(), we lock the TxTracker struct and add some data to it related to the current block's worth of transactions we're queuing. This includes references to transactions that will be queued to the tx_block_queue passed to that function. But we actually add these to the tx_block_queue only after we release the lock on the TxTracker, so another thread that's waiting for that lock might start looking through it and trying to find the transactions whose references were added to the TxTracker in queue_block(), but before these blocks get added to the tx_block_queue. This leads to a possible crash in get_tx() if we try to look up one of these transactions: ``` thread 'actix-rt|system:0|arbiter:0' panicked at tools/mirror/src/chain_tracker.rs:366:14: called `Result::unwrap()` on an `Err` value: 100 stack backtrace: 0: rust_begin_unwind 1: core::panicking::panic_fmt 2: core::result::unwrap_failed 3: near_mirror::chain_tracker::TxTracker::get_tx 4: near_mirror::chain_tracker::TxTracker::try_set_nonces ``` We can fix it by keeping the lock on the TxTracker until after we queue this new batch of transactions onto the tx_block_queue. This is a more involved change than it would seem, because we can't hold the lock across await points, and so we need to add some logic to do the async operations before we take the lock. In the end we get logic here that is closer to what it looked like before https://github.com/near/nearcore/pull/11916. In the future it would make things simpler to move the tx_block_queue back into the TxTracker, but for now this should work --- tools/mirror/src/chain_tracker.rs | 192 ++++++++++++++++-------------- tools/mirror/src/lib.rs | 3 + 2 files changed, 106 insertions(+), 89 deletions(-) diff --git a/tools/mirror/src/chain_tracker.rs b/tools/mirror/src/chain_tracker.rs index 81dca68a34b..fd46214506c 100644 --- a/tools/mirror/src/chain_tracker.rs +++ b/tools/mirror/src/chain_tracker.rs @@ -240,46 +240,66 @@ impl TxTracker { } } - async fn initialize_target_nonce( - lock: &Mutex, + // Makes sure that there's something written in the DB for this access key. + // This function is called before calling initialize_target_nonce(), which sets + // in-memory data associated with this nonce. It would make sense to do this part at the same time, + // But since we can't hold the lock across awaits, we need to do this separately first if we want to + // keep the lock on Self for the entirety of sections of code that make updates to it. + // + // So this function must be called before calling initialize_target_nonce() for a given access key + async fn store_target_nonce( target_view_client: &Addr, db: &DB, access_key: &(AccountId, PublicKey), + ) -> anyhow::Result<()> { + if crate::read_target_nonce(db, &access_key.0, &access_key.1)?.is_some() { + return Ok(()); + } + let nonce = + crate::fetch_access_key_nonce(target_view_client, &access_key.0, &access_key.1).await?; + let t = LatestTargetNonce { nonce, pending_outcomes: HashSet::new() }; + crate::put_target_nonce(db, &access_key.0, &access_key.1, &t)?; + + Ok(()) + } + + fn initialize_target_nonce( + &mut self, + db: &DB, + access_key: &(AccountId, PublicKey), source_height: Option, ) -> anyhow::Result<()> { - let info = match crate::read_target_nonce(db, &access_key.0, &access_key.1)? { - Some(t) => NonceInfo { - target_nonce: TargetNonce { - nonce: t.nonce, - pending_outcomes: t - .pending_outcomes - .into_iter() - .map(NonceUpdater::ChainObjectId) - .collect(), - }, - last_height: source_height, - txs_awaiting_nonce: BTreeSet::new(), - queued_txs: BTreeSet::new(), + // We unwrap() because store_target_nonce() must be called before calling this function. + let t = crate::read_target_nonce(db, &access_key.0, &access_key.1)?.unwrap(); + let info = NonceInfo { + target_nonce: TargetNonce { + nonce: t.nonce, + pending_outcomes: t + .pending_outcomes + .into_iter() + .map(NonceUpdater::ChainObjectId) + .collect(), }, - None => { - let nonce = - crate::fetch_access_key_nonce(target_view_client, &access_key.0, &access_key.1) - .await?; - let t = LatestTargetNonce { nonce, pending_outcomes: HashSet::new() }; - crate::put_target_nonce(db, &access_key.0, &access_key.1, &t)?; - NonceInfo { - target_nonce: TargetNonce { nonce: t.nonce, pending_outcomes: HashSet::new() }, - last_height: source_height, - txs_awaiting_nonce: BTreeSet::new(), - queued_txs: BTreeSet::new(), - } - } + last_height: source_height, + txs_awaiting_nonce: BTreeSet::new(), + queued_txs: BTreeSet::new(), }; - let mut me = lock.lock().unwrap(); - me.nonces.insert(access_key.clone(), info); + self.nonces.insert(access_key.clone(), info); Ok(()) } + fn get_target_nonce<'a>( + &'a mut self, + db: &DB, + access_key: &(AccountId, PublicKey), + source_height: Option, + ) -> anyhow::Result<&'a mut NonceInfo> { + if !self.nonces.contains_key(access_key) { + self.initialize_target_nonce(db, &access_key, source_height)?; + } + Ok(self.nonces.get_mut(access_key).unwrap()) + } + pub(crate) async fn next_nonce( lock: &Mutex, target_view_client: &Addr, @@ -290,12 +310,9 @@ impl TxTracker { ) -> anyhow::Result { let source_height = Some(source_height); let access_key = (signer_id.clone(), public_key.clone()); - if !lock.lock().unwrap().nonces.contains_key(&access_key) { - Self::initialize_target_nonce(lock, target_view_client, db, &access_key, source_height) - .await?; - } + Self::store_target_nonce(target_view_client, db, &access_key).await?; let mut me = lock.lock().unwrap(); - let info = me.nonces.get_mut(&access_key).unwrap(); + let info = me.get_target_nonce(db, &access_key, source_height).unwrap(); if source_height > info.last_height { info.last_height = source_height; } @@ -320,16 +337,16 @@ impl TxTracker { secret_key: &SecretKey, ) -> anyhow::Result { let access_key = (signer_id.clone(), public_key.clone()); - if !lock.lock().unwrap().nonces.contains_key(&access_key) { - Self::initialize_target_nonce(lock, target_view_client, db, &access_key, None).await?; - let mut me = lock.lock().unwrap(); + Self::store_target_nonce(target_view_client, db, &access_key).await?; + let mut me = lock.lock().unwrap(); + if !me.nonces.contains_key(&access_key) { + me.initialize_target_nonce(db, &access_key, None)?; let info = me.nonces.get_mut(&access_key).unwrap(); if let Some(nonce) = &mut info.target_nonce.nonce { *nonce += 1; } return Ok(info.target_nonce.clone()); } - let mut me = lock.lock().unwrap(); let mut first_nonce = None; let txs = me.nonces.get(&access_key).unwrap().queued_txs.clone(); if !txs.is_empty() { @@ -369,9 +386,10 @@ impl TxTracker { &mut chunk.txs[tx_ref.tx_idx] } - async fn insert_access_key_updates( - lock: &Mutex, - target_view_client: &Addr, + // This function sets in-memory info for any access keys that will be touched by this transaction (`tx_ref`). + // store_target_nonce() must have been called beforehand for each of these. + fn insert_access_key_updates( + &mut self, db: &DB, tx_ref: &TxRef, nonce_updates: &HashSet<(AccountId, PublicKey)>, @@ -379,18 +397,7 @@ impl TxTracker { ) -> anyhow::Result<()> { let source_height = Some(source_height); for access_key in nonce_updates.iter() { - if !lock.lock().unwrap().nonces.contains_key(access_key) { - Self::initialize_target_nonce( - lock, - target_view_client, - db, - &access_key, - source_height, - ) - .await?; - } - let mut me = lock.lock().unwrap(); - let info = me.nonces.get_mut(&access_key).unwrap(); + let info = self.get_target_nonce(db, &access_key, source_height).unwrap(); if info.last_height < source_height { info.last_height = source_height; @@ -398,8 +405,7 @@ impl TxTracker { info.target_nonce.pending_outcomes.insert(NonceUpdater::TxRef(tx_ref.clone())); } if !nonce_updates.is_empty() { - let mut me = lock.lock().unwrap(); - assert!(me + assert!(self .updater_to_keys .insert(NonceUpdater::TxRef(tx_ref.clone()), nonce_updates.clone()) .is_none()); @@ -407,28 +413,39 @@ impl TxTracker { Ok(()) } - // This is the non-async portion of queue_block() that returns a list of access key updates we need - // to call insert_access_key_updates() for, which we'll do after calling this function. Otherwise - // we would have to lock and unlock the mutex on every transaction to avoid holding it across await points - fn queue_txs<'a>( - lock: &Mutex, - block: &'a MappedBlock, - ) -> anyhow::Result)>> { - let mut nonce_updates = Vec::new(); - let mut me = lock.lock().unwrap(); - me.height_queued = Some(block.source_height); - me.next_heights.pop_front().unwrap(); + async fn store_access_key_updates( + block: &MappedBlock, + target_view_client: &Addr, + db: &DB, + ) -> anyhow::Result<()> { + for c in block.chunks.iter() { + for tx in c.txs.iter() { + let updates = match tx { + crate::TargetChainTx::Ready(tx) => &tx.nonce_updates, + crate::TargetChainTx::AwaitingNonce(tx) => &tx.nonce_updates, + }; + for access_key in updates.iter() { + Self::store_target_nonce(target_view_client, db, access_key).await?; + } + } + } + Ok(()) + } + + fn queue_txs(&mut self, block: &MappedBlock, db: &DB) -> anyhow::Result<()> { + self.height_queued = Some(block.source_height); + self.next_heights.pop_front().unwrap(); for c in block.chunks.iter() { if !c.txs.is_empty() { - me.nonempty_height_queued = Some(block.source_height); + self.nonempty_height_queued = Some(block.source_height); } for (tx_idx, tx) in c.txs.iter().enumerate() { let tx_ref = TxRef { source_height: block.source_height, shard_id: c.shard_id, tx_idx }; match tx { crate::TargetChainTx::Ready(tx) => { - let info = me + let info = self .nonces .get_mut(&( tx.target_tx.transaction.signer_id().clone(), @@ -436,12 +453,15 @@ impl TxTracker { )) .unwrap(); info.queued_txs.insert(tx_ref.clone()); - if !tx.nonce_updates.is_empty() { - nonce_updates.push((tx_ref, &tx.nonce_updates)); - } + self.insert_access_key_updates( + db, + &tx_ref, + &tx.nonce_updates, + block.source_height, + )?; } crate::TargetChainTx::AwaitingNonce(tx) => { - let info = me + let info = self .nonces .get_mut(&( tx.target_tx.signer_id().clone(), @@ -450,14 +470,17 @@ impl TxTracker { .unwrap(); info.txs_awaiting_nonce.insert(tx_ref.clone()); info.queued_txs.insert(tx_ref.clone()); - if !tx.nonce_updates.is_empty() { - nonce_updates.push((tx_ref, &tx.nonce_updates)); - } + self.insert_access_key_updates( + db, + &tx_ref, + &tx.nonce_updates, + block.source_height, + )?; } }; } } - Ok(nonce_updates) + Ok(()) } pub(crate) async fn queue_block( @@ -467,18 +490,9 @@ impl TxTracker { target_view_client: &Addr, db: &DB, ) -> anyhow::Result<()> { - let key_updates = Self::queue_txs(lock, &block)?; - for (tx_ref, nonce_updates) in key_updates { - Self::insert_access_key_updates( - lock, - target_view_client, - db, - &tx_ref, - nonce_updates, - block.source_height, - ) - .await?; - } + Self::store_access_key_updates(&block, target_view_client, db).await?; + let mut me = lock.lock().unwrap(); + me.queue_txs(&block, db)?; tx_block_queue.lock().unwrap().push_back(block); Ok(()) } diff --git a/tools/mirror/src/lib.rs b/tools/mirror/src/lib.rs index a131c66f5bb..7c82f62e294 100644 --- a/tools/mirror/src/lib.rs +++ b/tools/mirror/src/lib.rs @@ -1989,6 +1989,9 @@ impl TxMirror { let tracker2 = tracker.clone(); let index_target_thread = actix::Arbiter::new(); + // TODO: Consider moving this back to the TxTracker struct. Separating these made certain things easier, but now it + // means we need to be careful about the lock order to avoid deadlocks. We keep the convention that the TxTracker is + // always locked first. let tx_block_queue = Arc::new(Mutex::new(VecDeque::new())); let tx_block_queue2 = tx_block_queue.clone(); From 0aa1343f4964eaa24d3b9204d957d9576d94ded9 Mon Sep 17 00:00:00 2001 From: Marcelo Diop-Gonzalez Date: Fri, 4 Oct 2024 00:29:48 -0400 Subject: [PATCH 46/49] fix(tests): fix state sync test errors (#12150) These tests create node home dirs in tempfiles, but these are dropped too soon, and the directories are removed while the nodes are still running. This is visible in the logs as snapshot creation failure messages. After https://github.com/near/nearcore/pull/12147, these no longer cause test failures, but the underlying failures to create snapshots are still there. Fix it by keeping them around in an Arc in the enclosing scopes Note that every now and then these tests still fail with many messages showing `Received an invalid block during state sync` followed by a test timeout, but it seems to be for an unrelated reason. --- .../src/tests/client/sync_state_nodes.rs | 95 +++++++++++++------ 1 file changed, 67 insertions(+), 28 deletions(-) diff --git a/integration-tests/src/tests/client/sync_state_nodes.rs b/integration-tests/src/tests/client/sync_state_nodes.rs index 71129748dbe..008b76b1e5c 100644 --- a/integration-tests/src/tests/client/sync_state_nodes.rs +++ b/integration-tests/src/tests/client/sync_state_nodes.rs @@ -46,8 +46,16 @@ fn sync_state_nodes() { let mut near1 = load_test_config("test1", port1, genesis.clone()); near1.network_config.peer_store.boot_nodes = convert_boot_nodes(vec![]); near1.client_config.min_num_peers = 0; + + // In this test and the ones below, we have an Arc, that we make sure to keep alive by cloning it + // and keeping the original one around after we pass the clone to run_actix(). Otherwise it will be dropped early + // and the directories will actually be removed while the nodes are running. + let _dir1 = Arc::new(tempfile::Builder::new().prefix("sync_nodes_1").tempdir().unwrap()); + let dir1 = _dir1.clone(); + let _dir2 = Arc::new(tempfile::Builder::new().prefix("sync_nodes_2").tempdir().unwrap()); + let dir2 = _dir2.clone(); + run_actix(async move { - let dir1 = tempfile::Builder::new().prefix("sync_nodes_1").tempdir().unwrap(); let nearcore::NearNode { view_client: view_client1, .. } = start_with_config(dir1.path(), near1).expect("start_with_config"); @@ -61,6 +69,7 @@ fn sync_state_nodes() { let view_client2_holder2 = view_client2_holder.clone(); let arbiters_holder2 = arbiters_holder2.clone(); let genesis2 = genesis.clone(); + let dir2 = dir2.clone(); let actor = view_client1.send(GetBlock::latest().with_span_context()); let actor = actor.then(move |res| { @@ -78,10 +87,6 @@ fn sync_state_nodes() { near2.network_config.peer_store.boot_nodes = convert_boot_nodes(vec![("test1", *port1)]); - let dir2 = tempfile::Builder::new() - .prefix("sync_nodes_2") - .tempdir() - .unwrap(); let nearcore::NearNode { view_client: view_client2, arbiters, @@ -125,6 +130,8 @@ fn sync_state_nodes() { ) .start(); }); + drop(_dir1); + drop(_dir2); }); } @@ -148,6 +155,15 @@ fn sync_state_nodes_multishard() { ); genesis.config.epoch_length = 150; // so that by the time test2 joins it is not kicked out yet + let _dir1 = Arc::new(tempfile::Builder::new().prefix("sync_nodes_1").tempdir().unwrap()); + let dir1 = _dir1.clone(); + let _dir2 = Arc::new(tempfile::Builder::new().prefix("sync_nodes_2").tempdir().unwrap()); + let dir2 = _dir2.clone(); + let _dir3 = Arc::new(tempfile::Builder::new().prefix("sync_nodes_3").tempdir().unwrap()); + let dir3 = _dir3.clone(); + let _dir4 = Arc::new(tempfile::Builder::new().prefix("sync_nodes_4").tempdir().unwrap()); + let dir4 = _dir4.clone(); + run_actix(async move { let (port1, port2, port3, port4) = ( tcp::ListenerAddr::reserve_for_test(), @@ -181,14 +197,10 @@ fn sync_state_nodes_multishard() { near4.client_config.max_block_production_delay = near1.client_config.max_block_production_delay; - let dir1 = tempfile::Builder::new().prefix("sync_nodes_1").tempdir().unwrap(); let nearcore::NearNode { view_client: view_client1, .. } = start_with_config(dir1.path(), near1).expect("start_with_config"); - let dir3 = tempfile::Builder::new().prefix("sync_nodes_3").tempdir().unwrap(); start_with_config(dir3.path(), near3).expect("start_with_config"); - - let dir4 = tempfile::Builder::new().prefix("sync_nodes_4").tempdir().unwrap(); start_with_config(dir4.path(), near4).expect("start_with_config"); let view_client2_holder = Arc::new(RwLock::new(None)); @@ -201,6 +213,7 @@ fn sync_state_nodes_multishard() { let view_client2_holder2 = view_client2_holder.clone(); let arbiter_holder2 = arbiter_holder2.clone(); let genesis2 = genesis.clone(); + let dir2 = dir2.clone(); let actor = view_client1.send(GetBlock::latest().with_span_context()); let actor = actor.then(move |res| { @@ -225,10 +238,6 @@ fn sync_state_nodes_multishard() { ("test4", *port4), ]); - let dir2 = tempfile::Builder::new() - .prefix("sync_nodes_2") - .tempdir() - .unwrap(); let nearcore::NearNode { view_client: view_client2, arbiters, @@ -280,6 +289,10 @@ fn sync_state_nodes_multishard() { ) .start(); }); + drop(_dir1); + drop(_dir2); + drop(_dir3); + drop(_dir4); }); } @@ -298,9 +311,15 @@ fn sync_empty_state() { ); genesis.config.epoch_length = 20; + let _dir1 = Arc::new(tempfile::Builder::new().prefix("sync_nodes_1").tempdir().unwrap()); + let dir1 = _dir1.clone(); + let _dir2 = Arc::new(tempfile::Builder::new().prefix("sync_nodes_2").tempdir().unwrap()); + let dir2 = _dir2.clone(); + run_actix(async move { let (port1, port2) = (tcp::ListenerAddr::reserve_for_test(), tcp::ListenerAddr::reserve_for_test()); + // State sync triggers when header head is two epochs in the future. // Produce more blocks to make sure that state sync gets triggered when the second node starts. let state_sync_horizon = 10; @@ -312,10 +331,8 @@ fn sync_empty_state() { near1.client_config.min_block_production_delay = Duration::milliseconds(200); near1.client_config.max_block_production_delay = Duration::milliseconds(400); - let dir1 = tempfile::Builder::new().prefix("sync_nodes_1").tempdir().unwrap(); let nearcore::NearNode { view_client: view_client1, .. } = start_with_config(dir1.path(), near1).expect("start_with_config"); - let dir2 = Arc::new(tempfile::Builder::new().prefix("sync_nodes_2").tempdir().unwrap()); let view_client2_holder = Arc::new(RwLock::new(None)); let arbiters_holder = Arc::new(RwLock::new(vec![])); @@ -403,6 +420,8 @@ fn sync_empty_state() { ) .start(); }); + drop(_dir1); + drop(_dir2); }); } @@ -426,6 +445,14 @@ fn sync_state_dump() { // start, sync headers and find a dump of state. genesis.config.epoch_length = 30; + let _dump_dir = + Arc::new(tempfile::Builder::new().prefix("state_dump_1").tempdir().unwrap()); + let dump_dir = _dump_dir.clone(); + let _dir1 = Arc::new(tempfile::Builder::new().prefix("sync_nodes_1").tempdir().unwrap()); + let dir1 = _dir1.clone(); + let _dir2 = Arc::new(tempfile::Builder::new().prefix("sync_nodes_2").tempdir().unwrap()); + let dir2 = _dir2.clone(); + run_actix(async move { let (port1, port2) = (tcp::ListenerAddr::reserve_for_test(), tcp::ListenerAddr::reserve_for_test()); @@ -440,7 +467,7 @@ fn sync_state_dump() { near1.client_config.min_block_production_delay = Duration::milliseconds(300); near1.client_config.max_block_production_delay = Duration::milliseconds(600); near1.client_config.tracked_shards = vec![0]; // Track all shards. - let dump_dir = tempfile::Builder::new().prefix("state_dump_1").tempdir().unwrap(); + near1.client_config.state_sync.dump = Some(DumpConfig { location: Filesystem { root_dir: dump_dir.path().to_path_buf() }, restart_dump_for_shards: None, @@ -449,14 +476,12 @@ fn sync_state_dump() { }); near1.config.store.state_snapshot_enabled = true; - let dir1 = tempfile::Builder::new().prefix("sync_nodes_1").tempdir().unwrap(); let nearcore::NearNode { view_client: view_client1, // State sync dumper should be kept in the scope to avoid dropping it, which stops the state dumper loop. state_sync_dumper: _dumper, .. } = start_with_config(dir1.path(), near1).expect("start_with_config"); - let dir2 = tempfile::Builder::new().prefix("sync_nodes_2").tempdir().unwrap(); let view_client2_holder = Arc::new(RwLock::new(None)); let arbiters_holder = Arc::new(RwLock::new(vec![])); @@ -542,6 +567,9 @@ fn sync_state_dump() { .unwrap(); System::current().stop(); }); + drop(_dump_dir); + drop(_dir1); + drop(_dir2); }); } @@ -741,6 +769,10 @@ fn test_state_sync_headers() { heavy_test(|| { init_test_logger(); + let _dir1 = + Arc::new(tempfile::Builder::new().prefix("test_state_sync_headers").tempdir().unwrap()); + let dir1 = _dir1.clone(); + run_actix(async { let mut genesis = Genesis::test(vec!["test1".parse().unwrap()], 1); // Increase epoch_length if the test is flaky. @@ -750,8 +782,6 @@ fn test_state_sync_headers() { load_test_config("test1", tcp::ListenerAddr::reserve_for_test(), genesis.clone()); near1.client_config.min_num_peers = 0; near1.client_config.tracked_shards = vec![0]; // Track all shards. - let dir1 = - tempfile::Builder::new().prefix("test_state_sync_headers").tempdir().unwrap(); near1.config.store.state_snapshot_enabled = true; let nearcore::NearNode { view_client: view_client1, .. } = @@ -924,6 +954,7 @@ fn test_state_sync_headers() { .unwrap(); System::current().stop(); }); + drop(_dir1); }); } @@ -933,6 +964,20 @@ fn test_state_sync_headers_no_tracked_shards() { heavy_test(|| { init_test_logger(); + let _dir1 = Arc::new( + tempfile::Builder::new() + .prefix("test_state_sync_headers_no_tracked_shards_1") + .tempdir() + .unwrap(), + ); + let dir1 = _dir1.clone(); + let _dir2 = Arc::new( + tempfile::Builder::new() + .prefix("test_state_sync_headers_no_tracked_shards_2") + .tempdir() + .unwrap(), + ); + let dir2 = _dir2.clone(); run_actix(async { let mut genesis = Genesis::test(vec!["test1".parse().unwrap()], 1); // Increase epoch_length if the test is flaky. @@ -943,10 +988,6 @@ fn test_state_sync_headers_no_tracked_shards() { let mut near1 = load_test_config("test1", port1, genesis.clone()); near1.client_config.min_num_peers = 0; near1.client_config.tracked_shards = vec![0]; // Track all shards, it is a validator. - let dir1 = tempfile::Builder::new() - .prefix("test_state_sync_headers_no_tracked_shards_1") - .tempdir() - .unwrap(); near1.config.store.state_snapshot_enabled = false; near1.config.state_sync_enabled = false; near1.client_config.state_sync_enabled = false; @@ -959,10 +1000,6 @@ fn test_state_sync_headers_no_tracked_shards() { convert_boot_nodes(vec![("test1", *port1)]); near2.client_config.min_num_peers = 0; near2.client_config.tracked_shards = vec![]; // Track no shards. - let dir2 = tempfile::Builder::new() - .prefix("test_state_sync_headers_no_tracked_shards_2") - .tempdir() - .unwrap(); near2.config.store.state_snapshot_enabled = true; near2.config.state_sync_enabled = false; near2.client_config.state_sync_enabled = false; @@ -1082,5 +1119,7 @@ fn test_state_sync_headers_no_tracked_shards() { .unwrap(); System::current().stop(); }); + drop(_dir1); + drop(_dir2); }); } From 7b6fae6813f7634ebd1c9cfefd0bf7210c1b1ba3 Mon Sep 17 00:00:00 2001 From: Simonas Kazlauskas Date: Tue, 8 Oct 2024 11:12:43 +0300 Subject: [PATCH 47/49] yield_resume: fix resume per-byte cost (#12192) This was intended to match the cost of the per-byte of function call payloads, but an unfortunate two-letter typo led to it becoming the same as the base cost instead. This change requires a protocol version bump (to 73) but otherwise is fairly straightforward in that I just set the cost of the fee to the incorrect value back in 67 and corrected it in code and protocol version 73. --- core/parameters/res/runtime_configs/67.yaml | 2 +- core/parameters/res/runtime_configs/69.yaml | 4 - core/parameters/res/runtime_configs/73.yaml | 1 + .../res/runtime_configs/parameters.snap | 2 +- core/parameters/src/config_store.rs | 1 + core/parameters/src/cost.rs | 2 +- ...meters__config_store__tests__129.json.snap | 2 +- ...ameters__config_store__tests__73.json.snap | 267 ++++++++++++++++++ ...config_store__tests__testnet_129.json.snap | 2 +- ..._config_store__tests__testnet_73.json.snap | 267 ++++++++++++++++++ 10 files changed, 541 insertions(+), 9 deletions(-) create mode 100644 core/parameters/res/runtime_configs/73.yaml create mode 100644 core/parameters/src/snapshots/near_parameters__config_store__tests__73.json.snap create mode 100644 core/parameters/src/snapshots/near_parameters__config_store__tests__testnet_73.json.snap diff --git a/core/parameters/res/runtime_configs/67.yaml b/core/parameters/res/runtime_configs/67.yaml index 6d27cda6089..e5b377d3eaa 100644 --- a/core/parameters/res/runtime_configs/67.yaml +++ b/core/parameters/res/runtime_configs/67.yaml @@ -2,4 +2,4 @@ yield_resume: { old: false, new: true } wasm_yield_create_base: { old: 300_000_000_000_000, new: 153_411_779_276 } wasm_yield_create_byte: { old: 300_000_000_000_000, new: 15_643_988 } wasm_yield_resume_base: { old: 300_000_000_000_000, new: 1_195_627_285_210 } -wasm_yield_resume_byte: { old: 300_000_000_000_000, new: 17_212_011 } +wasm_yield_resume_byte: { old: 300_000_000_000_000, new: 1_195_627_285_210 } diff --git a/core/parameters/res/runtime_configs/69.yaml b/core/parameters/res/runtime_configs/69.yaml index 16b426fd9b4..89354a7a281 100644 --- a/core/parameters/res/runtime_configs/69.yaml +++ b/core/parameters/res/runtime_configs/69.yaml @@ -67,7 +67,3 @@ data_receipt_creation_per_byte: { execution: 17_212_011, } } -wasm_yield_resume_byte: { - old: 17_212_011, - new: 47_683_715 -} \ No newline at end of file diff --git a/core/parameters/res/runtime_configs/73.yaml b/core/parameters/res/runtime_configs/73.yaml new file mode 100644 index 00000000000..148bdeb6c81 --- /dev/null +++ b/core/parameters/res/runtime_configs/73.yaml @@ -0,0 +1 @@ +wasm_yield_resume_byte: { old: 1_195_627_285_210 , new: 47_683_715 } diff --git a/core/parameters/res/runtime_configs/parameters.snap b/core/parameters/res/runtime_configs/parameters.snap index 2162039ede0..6da38e85491 100644 --- a/core/parameters/res/runtime_configs/parameters.snap +++ b/core/parameters/res/runtime_configs/parameters.snap @@ -147,7 +147,7 @@ wasm_alt_bn128_g1_sum_element 5_000_000_000 wasm_yield_create_base 153_411_779_276 wasm_yield_create_byte 15_643_988 wasm_yield_resume_base 1_195_627_285_210 -wasm_yield_resume_byte 47_683_715 +wasm_yield_resume_byte 1_195_627_285_210 wasm_bls12381_p1_sum_base 16_500_000_000 wasm_bls12381_p1_sum_element 6_000_000_000 wasm_bls12381_p2_sum_base 18_600_000_000 diff --git a/core/parameters/src/config_store.rs b/core/parameters/src/config_store.rs index a194030edd5..78e30e40578 100644 --- a/core/parameters/src/config_store.rs +++ b/core/parameters/src/config_store.rs @@ -49,6 +49,7 @@ static CONFIG_DIFFS: &[(ProtocolVersion, &str)] = &[ (70, include_config!("70.yaml")), // Increase main_storage_proof_size_soft_limit and introduces StateStoredReceipt (72, include_config!("72.yaml")), + (73, include_config!("73.yaml")), (129, include_config!("129.yaml")), ]; diff --git a/core/parameters/src/cost.rs b/core/parameters/src/cost.rs index 4067fc4487d..fb3db1ee3eb 100644 --- a/core/parameters/src/cost.rs +++ b/core/parameters/src/cost.rs @@ -390,7 +390,7 @@ impl ExtCosts { ExtCosts::yield_create_base => Parameter::WasmYieldCreateBase, ExtCosts::yield_create_byte => Parameter::WasmYieldCreateByte, ExtCosts::yield_resume_base => Parameter::WasmYieldResumeBase, - ExtCosts::yield_resume_byte => Parameter::WasmYieldResumeBase, + ExtCosts::yield_resume_byte => Parameter::WasmYieldResumeByte, ExtCosts::bls12381_p1_sum_base => Parameter::WasmBls12381P1SumBase, ExtCosts::bls12381_p1_sum_element => Parameter::WasmBls12381P1SumElement, ExtCosts::bls12381_p2_sum_base => Parameter::WasmBls12381P2SumBase, diff --git a/core/parameters/src/snapshots/near_parameters__config_store__tests__129.json.snap b/core/parameters/src/snapshots/near_parameters__config_store__tests__129.json.snap index cc12b4cb769..5cc5a524c67 100644 --- a/core/parameters/src/snapshots/near_parameters__config_store__tests__129.json.snap +++ b/core/parameters/src/snapshots/near_parameters__config_store__tests__129.json.snap @@ -174,7 +174,7 @@ expression: config_view "yield_create_base": 153411779276, "yield_create_byte": 15643988, "yield_resume_base": 1195627285210, - "yield_resume_byte": 1195627285210, + "yield_resume_byte": 47683715, "bls12381_p1_sum_base": 16500000000, "bls12381_p1_sum_element": 6000000000, "bls12381_p2_sum_base": 18600000000, diff --git a/core/parameters/src/snapshots/near_parameters__config_store__tests__73.json.snap b/core/parameters/src/snapshots/near_parameters__config_store__tests__73.json.snap new file mode 100644 index 00000000000..f858b308c6d --- /dev/null +++ b/core/parameters/src/snapshots/near_parameters__config_store__tests__73.json.snap @@ -0,0 +1,267 @@ +--- +source: core/parameters/src/config_store.rs +expression: config_view +--- +{ + "storage_amount_per_byte": "10000000000000000000", + "transaction_costs": { + "action_receipt_creation_config": { + "send_sir": 108059500000, + "send_not_sir": 108059500000, + "execution": 108059500000 + }, + "data_receipt_creation_config": { + "base_cost": { + "send_sir": 36486732312, + "send_not_sir": 36486732312, + "execution": 36486732312 + }, + "cost_per_byte": { + "send_sir": 17212011, + "send_not_sir": 47683715, + "execution": 17212011 + } + }, + "action_creation_config": { + "create_account_cost": { + "send_sir": 3850000000000, + "send_not_sir": 3850000000000, + "execution": 3850000000000 + }, + "deploy_contract_cost": { + "send_sir": 184765750000, + "send_not_sir": 184765750000, + "execution": 184765750000 + }, + "deploy_contract_cost_per_byte": { + "send_sir": 6812999, + "send_not_sir": 47683715, + "execution": 64572944 + }, + "function_call_cost": { + "send_sir": 200000000000, + "send_not_sir": 200000000000, + "execution": 780000000000 + }, + "function_call_cost_per_byte": { + "send_sir": 2235934, + "send_not_sir": 47683715, + "execution": 2235934 + }, + "transfer_cost": { + "send_sir": 115123062500, + "send_not_sir": 115123062500, + "execution": 115123062500 + }, + "stake_cost": { + "send_sir": 141715687500, + "send_not_sir": 141715687500, + "execution": 102217625000 + }, + "add_key_cost": { + "full_access_cost": { + "send_sir": 101765125000, + "send_not_sir": 101765125000, + "execution": 101765125000 + }, + "function_call_cost": { + "send_sir": 102217625000, + "send_not_sir": 102217625000, + "execution": 102217625000 + }, + "function_call_cost_per_byte": { + "send_sir": 1925331, + "send_not_sir": 47683715, + "execution": 1925331 + } + }, + "delete_key_cost": { + "send_sir": 94946625000, + "send_not_sir": 94946625000, + "execution": 94946625000 + }, + "delete_account_cost": { + "send_sir": 147489000000, + "send_not_sir": 147489000000, + "execution": 147489000000 + }, + "delegate_cost": { + "send_sir": 200000000000, + "send_not_sir": 200000000000, + "execution": 200000000000 + } + }, + "storage_usage_config": { + "num_bytes_account": 100, + "num_extra_bytes_record": 40 + }, + "burnt_gas_reward": [ + 3, + 10 + ], + "pessimistic_gas_price_inflation_ratio": [ + 103, + 100 + ] + }, + "wasm_config": { + "ext_costs": { + "base": 264768111, + "contract_loading_base": 35445963, + "contract_loading_bytes": 1089295, + "read_memory_base": 2609863200, + "read_memory_byte": 3801333, + "write_memory_base": 2803794861, + "write_memory_byte": 2723772, + "read_register_base": 2517165186, + "read_register_byte": 98562, + "write_register_base": 2865522486, + "write_register_byte": 3801564, + "utf8_decoding_base": 3111779061, + "utf8_decoding_byte": 291580479, + "utf16_decoding_base": 3543313050, + "utf16_decoding_byte": 163577493, + "sha256_base": 4540970250, + "sha256_byte": 24117351, + "keccak256_base": 5879491275, + "keccak256_byte": 21471105, + "keccak512_base": 5811388236, + "keccak512_byte": 36649701, + "ripemd160_base": 853675086, + "ripemd160_block": 680107584, + "ed25519_verify_base": 210000000000, + "ed25519_verify_byte": 9000000, + "ecrecover_base": 278821988457, + "log_base": 3543313050, + "log_byte": 13198791, + "storage_write_base": 64196736000, + "storage_write_key_byte": 70482867, + "storage_write_value_byte": 31018539, + "storage_write_evicted_byte": 32117307, + "storage_read_base": 56356845749, + "storage_read_key_byte": 30952533, + "storage_read_value_byte": 5611004, + "storage_large_read_overhead_base": 1, + "storage_large_read_overhead_byte": 1, + "storage_remove_base": 53473030500, + "storage_remove_key_byte": 38220384, + "storage_remove_ret_value_byte": 11531556, + "storage_has_key_base": 54039896625, + "storage_has_key_byte": 30790845, + "storage_iter_create_prefix_base": 0, + "storage_iter_create_prefix_byte": 0, + "storage_iter_create_range_base": 0, + "storage_iter_create_from_byte": 0, + "storage_iter_create_to_byte": 0, + "storage_iter_next_base": 0, + "storage_iter_next_key_byte": 0, + "storage_iter_next_value_byte": 0, + "touching_trie_node": 16101955926, + "read_cached_trie_node": 2280000000, + "promise_and_base": 1465013400, + "promise_and_per_promise": 5452176, + "promise_return": 560152386, + "validator_stake_base": 911834726400, + "validator_total_stake_base": 911834726400, + "contract_compile_base": 0, + "contract_compile_bytes": 0, + "alt_bn128_g1_multiexp_base": 713000000000, + "alt_bn128_g1_multiexp_element": 320000000000, + "alt_bn128_g1_sum_base": 3000000000, + "alt_bn128_g1_sum_element": 5000000000, + "alt_bn128_pairing_check_base": 9686000000000, + "alt_bn128_pairing_check_element": 5102000000000, + "yield_create_base": 153411779276, + "yield_create_byte": 15643988, + "yield_resume_base": 1195627285210, + "yield_resume_byte": 47683715, + "bls12381_p1_sum_base": 16500000000, + "bls12381_p1_sum_element": 6000000000, + "bls12381_p2_sum_base": 18600000000, + "bls12381_p2_sum_element": 15000000000, + "bls12381_g1_multiexp_base": 16500000000, + "bls12381_g1_multiexp_element": 930000000000, + "bls12381_g2_multiexp_base": 18600000000, + "bls12381_g2_multiexp_element": 1995000000000, + "bls12381_map_fp_to_g1_base": 1500000000, + "bls12381_map_fp_to_g1_element": 252000000000, + "bls12381_map_fp2_to_g2_base": 1500000000, + "bls12381_map_fp2_to_g2_element": 900000000000, + "bls12381_pairing_base": 2130000000000, + "bls12381_pairing_element": 2130000000000, + "bls12381_p1_decompress_base": 15000000000, + "bls12381_p1_decompress_element": 81000000000, + "bls12381_p2_decompress_base": 15000000000, + "bls12381_p2_decompress_element": 165000000000 + }, + "grow_mem_cost": 1, + "regular_op_cost": 822756, + "vm_kind": "", + "disable_9393_fix": false, + "discard_custom_sections": true, + "storage_get_mode": "FlatStorage", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, + "math_extension": true, + "ed25519_verify": true, + "alt_bn128": true, + "function_call_weight": true, + "eth_implicit_accounts": true, + "yield_resume_host_functions": true, + "limit_config": { + "max_gas_burnt": 300000000000000, + "max_stack_height": 262144, + "contract_prepare_version": 2, + "initial_memory_pages": 1024, + "max_memory_pages": 2048, + "registers_memory_limit": 1073741824, + "max_register_size": 104857600, + "max_number_registers": 100, + "max_number_logs": 100, + "max_total_log_length": 16384, + "max_total_prepaid_gas": 300000000000000, + "max_actions_per_receipt": 100, + "max_number_bytes_method_names": 2000, + "max_length_method_name": 256, + "max_arguments_length": 4194304, + "max_length_returned_data": 4194304, + "max_contract_size": 4194304, + "max_transaction_size": 1572864, + "max_receipt_size": 4194304, + "max_length_storage_key": 2048, + "max_length_storage_value": 4194304, + "max_promises_per_function_call_action": 1024, + "max_number_input_data_dependencies": 128, + "max_functions_number_per_contract": 10000, + "wasmer2_stack_limit": 204800, + "max_locals_per_contract": 1000000, + "account_id_validity_rules_version": 1, + "yield_timeout_length_in_blocks": 200, + "max_yield_payload_size": 1024, + "per_receipt_storage_proof_size_limit": 4000000 + } + }, + "account_creation_config": { + "min_allowed_top_level_account_length": 65, + "registrar_account_id": "registrar" + }, + "congestion_control_config": { + "max_congestion_incoming_gas": 20000000000000000, + "max_congestion_outgoing_gas": 10000000000000000, + "max_congestion_memory_consumption": 1000000000, + "max_congestion_missed_chunks": 5, + "max_outgoing_gas": 300000000000000000, + "min_outgoing_gas": 1000000000000000, + "allowed_shard_outgoing_gas": 1000000000000000, + "max_tx_gas": 500000000000000, + "min_tx_gas": 20000000000000, + "reject_tx_congestion_threshold": 0.5, + "outgoing_receipts_usual_size_limit": 102400, + "outgoing_receipts_big_size_limit": 4718592 + }, + "witness_config": { + "main_storage_proof_size_soft_limit": 4000000, + "combined_transactions_size_limit": 4194304, + "new_transactions_validation_state_size_soft_limit": 572864 + } +} diff --git a/core/parameters/src/snapshots/near_parameters__config_store__tests__testnet_129.json.snap b/core/parameters/src/snapshots/near_parameters__config_store__tests__testnet_129.json.snap index cc12b4cb769..5cc5a524c67 100644 --- a/core/parameters/src/snapshots/near_parameters__config_store__tests__testnet_129.json.snap +++ b/core/parameters/src/snapshots/near_parameters__config_store__tests__testnet_129.json.snap @@ -174,7 +174,7 @@ expression: config_view "yield_create_base": 153411779276, "yield_create_byte": 15643988, "yield_resume_base": 1195627285210, - "yield_resume_byte": 1195627285210, + "yield_resume_byte": 47683715, "bls12381_p1_sum_base": 16500000000, "bls12381_p1_sum_element": 6000000000, "bls12381_p2_sum_base": 18600000000, diff --git a/core/parameters/src/snapshots/near_parameters__config_store__tests__testnet_73.json.snap b/core/parameters/src/snapshots/near_parameters__config_store__tests__testnet_73.json.snap new file mode 100644 index 00000000000..f858b308c6d --- /dev/null +++ b/core/parameters/src/snapshots/near_parameters__config_store__tests__testnet_73.json.snap @@ -0,0 +1,267 @@ +--- +source: core/parameters/src/config_store.rs +expression: config_view +--- +{ + "storage_amount_per_byte": "10000000000000000000", + "transaction_costs": { + "action_receipt_creation_config": { + "send_sir": 108059500000, + "send_not_sir": 108059500000, + "execution": 108059500000 + }, + "data_receipt_creation_config": { + "base_cost": { + "send_sir": 36486732312, + "send_not_sir": 36486732312, + "execution": 36486732312 + }, + "cost_per_byte": { + "send_sir": 17212011, + "send_not_sir": 47683715, + "execution": 17212011 + } + }, + "action_creation_config": { + "create_account_cost": { + "send_sir": 3850000000000, + "send_not_sir": 3850000000000, + "execution": 3850000000000 + }, + "deploy_contract_cost": { + "send_sir": 184765750000, + "send_not_sir": 184765750000, + "execution": 184765750000 + }, + "deploy_contract_cost_per_byte": { + "send_sir": 6812999, + "send_not_sir": 47683715, + "execution": 64572944 + }, + "function_call_cost": { + "send_sir": 200000000000, + "send_not_sir": 200000000000, + "execution": 780000000000 + }, + "function_call_cost_per_byte": { + "send_sir": 2235934, + "send_not_sir": 47683715, + "execution": 2235934 + }, + "transfer_cost": { + "send_sir": 115123062500, + "send_not_sir": 115123062500, + "execution": 115123062500 + }, + "stake_cost": { + "send_sir": 141715687500, + "send_not_sir": 141715687500, + "execution": 102217625000 + }, + "add_key_cost": { + "full_access_cost": { + "send_sir": 101765125000, + "send_not_sir": 101765125000, + "execution": 101765125000 + }, + "function_call_cost": { + "send_sir": 102217625000, + "send_not_sir": 102217625000, + "execution": 102217625000 + }, + "function_call_cost_per_byte": { + "send_sir": 1925331, + "send_not_sir": 47683715, + "execution": 1925331 + } + }, + "delete_key_cost": { + "send_sir": 94946625000, + "send_not_sir": 94946625000, + "execution": 94946625000 + }, + "delete_account_cost": { + "send_sir": 147489000000, + "send_not_sir": 147489000000, + "execution": 147489000000 + }, + "delegate_cost": { + "send_sir": 200000000000, + "send_not_sir": 200000000000, + "execution": 200000000000 + } + }, + "storage_usage_config": { + "num_bytes_account": 100, + "num_extra_bytes_record": 40 + }, + "burnt_gas_reward": [ + 3, + 10 + ], + "pessimistic_gas_price_inflation_ratio": [ + 103, + 100 + ] + }, + "wasm_config": { + "ext_costs": { + "base": 264768111, + "contract_loading_base": 35445963, + "contract_loading_bytes": 1089295, + "read_memory_base": 2609863200, + "read_memory_byte": 3801333, + "write_memory_base": 2803794861, + "write_memory_byte": 2723772, + "read_register_base": 2517165186, + "read_register_byte": 98562, + "write_register_base": 2865522486, + "write_register_byte": 3801564, + "utf8_decoding_base": 3111779061, + "utf8_decoding_byte": 291580479, + "utf16_decoding_base": 3543313050, + "utf16_decoding_byte": 163577493, + "sha256_base": 4540970250, + "sha256_byte": 24117351, + "keccak256_base": 5879491275, + "keccak256_byte": 21471105, + "keccak512_base": 5811388236, + "keccak512_byte": 36649701, + "ripemd160_base": 853675086, + "ripemd160_block": 680107584, + "ed25519_verify_base": 210000000000, + "ed25519_verify_byte": 9000000, + "ecrecover_base": 278821988457, + "log_base": 3543313050, + "log_byte": 13198791, + "storage_write_base": 64196736000, + "storage_write_key_byte": 70482867, + "storage_write_value_byte": 31018539, + "storage_write_evicted_byte": 32117307, + "storage_read_base": 56356845749, + "storage_read_key_byte": 30952533, + "storage_read_value_byte": 5611004, + "storage_large_read_overhead_base": 1, + "storage_large_read_overhead_byte": 1, + "storage_remove_base": 53473030500, + "storage_remove_key_byte": 38220384, + "storage_remove_ret_value_byte": 11531556, + "storage_has_key_base": 54039896625, + "storage_has_key_byte": 30790845, + "storage_iter_create_prefix_base": 0, + "storage_iter_create_prefix_byte": 0, + "storage_iter_create_range_base": 0, + "storage_iter_create_from_byte": 0, + "storage_iter_create_to_byte": 0, + "storage_iter_next_base": 0, + "storage_iter_next_key_byte": 0, + "storage_iter_next_value_byte": 0, + "touching_trie_node": 16101955926, + "read_cached_trie_node": 2280000000, + "promise_and_base": 1465013400, + "promise_and_per_promise": 5452176, + "promise_return": 560152386, + "validator_stake_base": 911834726400, + "validator_total_stake_base": 911834726400, + "contract_compile_base": 0, + "contract_compile_bytes": 0, + "alt_bn128_g1_multiexp_base": 713000000000, + "alt_bn128_g1_multiexp_element": 320000000000, + "alt_bn128_g1_sum_base": 3000000000, + "alt_bn128_g1_sum_element": 5000000000, + "alt_bn128_pairing_check_base": 9686000000000, + "alt_bn128_pairing_check_element": 5102000000000, + "yield_create_base": 153411779276, + "yield_create_byte": 15643988, + "yield_resume_base": 1195627285210, + "yield_resume_byte": 47683715, + "bls12381_p1_sum_base": 16500000000, + "bls12381_p1_sum_element": 6000000000, + "bls12381_p2_sum_base": 18600000000, + "bls12381_p2_sum_element": 15000000000, + "bls12381_g1_multiexp_base": 16500000000, + "bls12381_g1_multiexp_element": 930000000000, + "bls12381_g2_multiexp_base": 18600000000, + "bls12381_g2_multiexp_element": 1995000000000, + "bls12381_map_fp_to_g1_base": 1500000000, + "bls12381_map_fp_to_g1_element": 252000000000, + "bls12381_map_fp2_to_g2_base": 1500000000, + "bls12381_map_fp2_to_g2_element": 900000000000, + "bls12381_pairing_base": 2130000000000, + "bls12381_pairing_element": 2130000000000, + "bls12381_p1_decompress_base": 15000000000, + "bls12381_p1_decompress_element": 81000000000, + "bls12381_p2_decompress_base": 15000000000, + "bls12381_p2_decompress_element": 165000000000 + }, + "grow_mem_cost": 1, + "regular_op_cost": 822756, + "vm_kind": "", + "disable_9393_fix": false, + "discard_custom_sections": true, + "storage_get_mode": "FlatStorage", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, + "math_extension": true, + "ed25519_verify": true, + "alt_bn128": true, + "function_call_weight": true, + "eth_implicit_accounts": true, + "yield_resume_host_functions": true, + "limit_config": { + "max_gas_burnt": 300000000000000, + "max_stack_height": 262144, + "contract_prepare_version": 2, + "initial_memory_pages": 1024, + "max_memory_pages": 2048, + "registers_memory_limit": 1073741824, + "max_register_size": 104857600, + "max_number_registers": 100, + "max_number_logs": 100, + "max_total_log_length": 16384, + "max_total_prepaid_gas": 300000000000000, + "max_actions_per_receipt": 100, + "max_number_bytes_method_names": 2000, + "max_length_method_name": 256, + "max_arguments_length": 4194304, + "max_length_returned_data": 4194304, + "max_contract_size": 4194304, + "max_transaction_size": 1572864, + "max_receipt_size": 4194304, + "max_length_storage_key": 2048, + "max_length_storage_value": 4194304, + "max_promises_per_function_call_action": 1024, + "max_number_input_data_dependencies": 128, + "max_functions_number_per_contract": 10000, + "wasmer2_stack_limit": 204800, + "max_locals_per_contract": 1000000, + "account_id_validity_rules_version": 1, + "yield_timeout_length_in_blocks": 200, + "max_yield_payload_size": 1024, + "per_receipt_storage_proof_size_limit": 4000000 + } + }, + "account_creation_config": { + "min_allowed_top_level_account_length": 65, + "registrar_account_id": "registrar" + }, + "congestion_control_config": { + "max_congestion_incoming_gas": 20000000000000000, + "max_congestion_outgoing_gas": 10000000000000000, + "max_congestion_memory_consumption": 1000000000, + "max_congestion_missed_chunks": 5, + "max_outgoing_gas": 300000000000000000, + "min_outgoing_gas": 1000000000000000, + "allowed_shard_outgoing_gas": 1000000000000000, + "max_tx_gas": 500000000000000, + "min_tx_gas": 20000000000000, + "reject_tx_congestion_threshold": 0.5, + "outgoing_receipts_usual_size_limit": 102400, + "outgoing_receipts_big_size_limit": 4718592 + }, + "witness_config": { + "main_storage_proof_size_soft_limit": 4000000, + "combined_transactions_size_limit": 4194304, + "new_transactions_validation_state_size_soft_limit": 572864 + } +} From e129b315f96ee0860184f57fcad3184a864db0d4 Mon Sep 17 00:00:00 2001 From: Simonas Kazlauskas Date: Tue, 8 Oct 2024 15:43:47 +0300 Subject: [PATCH 48/49] chore: update futures-util (#12193) The previous version has been yanked and is therefore failing the audit check. --- Cargo.lock | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f72d48d5b7..8b344255e8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2647,9 +2647,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.21" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -2657,9 +2657,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" @@ -2674,38 +2674,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.21" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.70", ] [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", From d0a33c3bc24d0d8a9d774f5ad602b894f37d17dd Mon Sep 17 00:00:00 2001 From: Aleksandr Logunov Date: Wed, 9 Oct 2024 00:06:32 +0400 Subject: [PATCH 49/49] test: enable simple resharding v3 test (#12191) Do minimal changes to the code allowing to check that number of shards in the new epoch increased. This code can be reused to test each separate component for resharding v3: * non-contiguous shard ids * state sync * memtries resharding * ... To make the test pass, nodes must track all shards for now, because state sync is not implemented yet. So every node must think that it has enough state to skip state sync. Note that it doesn't mean at all that resharding works already. State is also not properly constructed yet, so tx processing will either be incorrect or crash the node. --- chain/chain/src/chain.rs | 8 ++- core/primitives/src/epoch_manager.rs | 9 ++++ integration-tests/src/test_loop/builder.rs | 20 +++++--- .../src/test_loop/tests/resharding_v3.rs | 49 +++++++++++++------ 4 files changed, 64 insertions(+), 22 deletions(-) diff --git a/chain/chain/src/chain.rs b/chain/chain/src/chain.rs index e23b23b0c3a..aa6e24b9581 100644 --- a/chain/chain/src/chain.rs +++ b/chain/chain/src/chain.rs @@ -2347,7 +2347,13 @@ impl Chain { ) -> bool { let result = epoch_manager.will_shard_layout_change(parent_hash); let will_shard_layout_change = match result { - Ok(will_shard_layout_change) => will_shard_layout_change, + Ok(_will_shard_layout_change) => { + // TODO(#11881): before state sync is fixed, we don't catch up + // split shards. Assume that all needed shards are tracked + // already. + // will_shard_layout_change, + false + } Err(err) => { // TODO(resharding) This is a problem, if this happens the node // will not perform resharding and fall behind the network. diff --git a/core/primitives/src/epoch_manager.rs b/core/primitives/src/epoch_manager.rs index abf74b10fa3..5beedb78b13 100644 --- a/core/primitives/src/epoch_manager.rs +++ b/core/primitives/src/epoch_manager.rs @@ -55,6 +55,15 @@ pub struct EpochConfig { pub validator_selection_config: ValidatorSelectionConfig, } +impl EpochConfig { + /// Total number of validator seats in the epoch since protocol version 69. + pub fn num_validators(&self) -> NumSeats { + self.num_block_producer_seats + .max(self.validator_selection_config.num_chunk_producer_seats) + .max(self.validator_selection_config.num_chunk_validator_seats) + } +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ShardConfig { pub num_block_producer_seats_per_shard: Vec, diff --git a/integration-tests/src/test_loop/builder.rs b/integration-tests/src/test_loop/builder.rs index 9bc3bfb6572..9c9903b1e75 100644 --- a/integration-tests/src/test_loop/builder.rs +++ b/integration-tests/src/test_loop/builder.rs @@ -72,6 +72,8 @@ pub(crate) struct TestLoopBuilder { config_modifier: Option>, /// Whether to do the warmup or not. See `skip_warmup` for more details. warmup: bool, + /// Whether all nodes must track all shards. + track_all_shards: bool, } impl TestLoopBuilder { @@ -91,6 +93,7 @@ impl TestLoopBuilder { runtime_config_store: None, config_modifier: None, warmup: true, + track_all_shards: false, } } @@ -170,6 +173,11 @@ impl TestLoopBuilder { self } + pub fn track_all_shards(mut self) -> Self { + self.track_all_shards = true; + self + } + /// Overrides the tempdir (which contains state dump, etc.) instead /// of creating a new one. pub fn test_loop_data_dir(mut self, dir: TempDir) -> Self { @@ -270,13 +278,11 @@ impl TestLoopBuilder { // Configure tracked shards. // * single shard tracking for validators // * all shard tracking for non-validators (RPCs and archival) - let epoch_config = epoch_config_store.get_config(genesis.config.protocol_version); - let num_block_producer = epoch_config.num_block_producer_seats; - let num_chunk_producer = epoch_config.validator_selection_config.num_chunk_producer_seats; - let num_chunk_validator = epoch_config.validator_selection_config.num_chunk_validator_seats; - let validator_num = - num_block_producer.max(num_chunk_producer).max(num_chunk_validator) as usize; - if idx < validator_num { + let is_validator = { + let epoch_config = epoch_config_store.get_config(genesis.config.protocol_version); + idx < epoch_config.num_validators() as usize + }; + if is_validator && !self.track_all_shards { client_config.tracked_shards = Vec::new(); } else { client_config.tracked_shards = vec![666]; diff --git a/integration-tests/src/test_loop/tests/resharding_v3.rs b/integration-tests/src/test_loop/tests/resharding_v3.rs index ac10f425c27..0dca672f4f2 100644 --- a/integration-tests/src/test_loop/tests/resharding_v3.rs +++ b/integration-tests/src/test_loop/tests/resharding_v3.rs @@ -15,10 +15,17 @@ use crate::test_loop::env::TestLoopEnv; use crate::test_loop::utils::ONE_NEAR; /// Stub for checking Resharding V3. -/// After uncommenting panics with -/// StorageInconsistentState("Failed to find root node ... in memtrie") +/// TODO(#11881): add the following scenarios: +/// - Shard ids should not be contiguous. For now we reuse existing shard id +/// which is incorrect!!! +/// - Nodes must not track all shards. State sync must succeed. +/// - Set up chunk validator-only nodes. State witness must pass validation. +/// - Consistent tx load. All txs must succeed. +/// - Delayed receipts, congestion control computation. +/// - Cross-shard receipts of all kinds, crossing resharding boundary. +/// - Shard layout v2 -> v2 transition. +/// - Shard layout can be taken from mainnet. #[test] -#[ignore] fn test_resharding_v3() { if !ProtocolFeature::SimpleNightshadeV4.enabled(PROTOCOL_VERSION) { return; @@ -28,12 +35,13 @@ fn test_resharding_v3() { let builder = TestLoopBuilder::new(); let initial_balance = 1_000_000 * ONE_NEAR; - let epoch_length = 10; + let epoch_length = 6; let accounts = (0..8).map(|i| format!("account{}", i).parse().unwrap()).collect::>(); - let clients = accounts.iter().cloned().collect_vec(); - let block_and_chunk_producers = (0..8).map(|idx| accounts[idx].as_str()).collect_vec(); - // TODO: set up chunk validator-only nodes. + // #12195 prevents number of BPs bigger than `epoch_length`. + let clients = vec![accounts[0].clone(), accounts[3].clone(), accounts[6].clone()]; + let block_and_chunk_producers = + clients.iter().map(|account: &AccountId| account.as_str()).collect_vec(); // Prepare shard split configuration. let base_epoch_config_store = EpochConfigStore::for_chain_id("mainnet").unwrap(); @@ -42,6 +50,12 @@ fn test_resharding_v3() { base_epoch_config_store.get_config(base_protocol_version).as_ref().clone(); base_epoch_config.validator_selection_config.shuffle_shard_assignment_for_chunk_producers = false; + // TODO(#11881): enable kickouts when blocks and chunks are produced + // properly. + base_epoch_config.block_producer_kickout_threshold = 0; + base_epoch_config.chunk_producer_kickout_threshold = 0; + base_epoch_config.chunk_validator_only_kickout_threshold = 0; + base_epoch_config.shard_layout = ShardLayout::v1(vec!["account3".parse().unwrap()], None, 3); let base_shard_layout = base_epoch_config.shard_layout.clone(); let mut epoch_config = base_epoch_config.clone(); let mut boundary_accounts = base_shard_layout.boundary_accounts().clone(); @@ -50,9 +64,12 @@ fn test_resharding_v3() { let last_shard_id = shard_ids.pop().unwrap(); let mut shards_split_map: BTreeMap> = shard_ids.iter().map(|shard_id| (*shard_id, vec![*shard_id])).collect(); - shard_ids.extend([max_shard_id + 1, max_shard_id + 2]); - shards_split_map.insert(last_shard_id, vec![max_shard_id + 1, max_shard_id + 2]); - boundary_accounts.push(AccountId::try_from("x.near".to_string()).unwrap()); + // TODO(#11881): keep this way until non-contiguous shard ids are supported. + // let new_shards = vec![max_shard_id + 1, max_shard_id + 2]; + let new_shards = vec![max_shard_id, max_shard_id + 1]; + shard_ids.extend(new_shards.clone()); + shards_split_map.insert(last_shard_id, new_shards); + boundary_accounts.push(AccountId::try_from("xyz.near".to_string()).unwrap()); epoch_config.shard_layout = ShardLayout::v2(boundary_accounts, shard_ids, Some(shards_split_map)); let expected_num_shards = epoch_config.shard_layout.shard_ids().count(); @@ -73,8 +90,12 @@ fn test_resharding_v3() { } let (genesis, _) = genesis_builder.build(); - let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = - builder.genesis(genesis).epoch_config_store(epoch_config_store).clients(clients).build(); + let TestLoopEnv { mut test_loop, datas: node_datas, tempdir } = builder + .genesis(genesis) + .epoch_config_store(epoch_config_store) + .clients(clients) + .track_all_shards() + .build(); let client_handle = node_datas[0].client_sender.actor_handle(); let success_condition = |test_loop_data: &mut TestLoopData| -> bool { @@ -89,8 +110,8 @@ fn test_resharding_v3() { test_loop.run_until( success_condition, - // Timeout at producing 5 epochs, approximately. - Duration::seconds((5 * epoch_length) as i64), + // Give enough time to produce ~6 epochs. + Duration::seconds((6 * epoch_length) as i64), ); TestLoopEnv { test_loop, datas: node_datas, tempdir }