diff --git a/.gitignore b/.gitignore index f4106d16321..c5a545232a6 100644 --- a/.gitignore +++ b/.gitignore @@ -61,4 +61,4 @@ rusty-tags.vi # Estimator generated files costs-*.txt names-to-stats.txt -data_dump_*.bin +data_dump_*.bin \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c41ee325a94..473409fc09a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ * New option `transaction_pool_size_limit` in `config.json` allows to limit the size of the node's transaction pool. By default the limit is set to 100 MB. [#3284](https://github.com/near/nearcore/issues/3284) * Database snapshots at the end of an epoch. This lets a node obtain state parts using flat storage. [#9090](https://github.com/near/nearcore/pull/9090) +* Number of transactions included in a chunk will be lowered if there is a congestion of more than 20000 delayed receipts in a shard. [#9222](https://github.com/near/nearcore/pull/9222) +* Our more efficient and scalable V2 routing protocol is implemented. It shadows the V1 protocol for now while we verify its performance. [#9187](https://github.com/near/nearcore/pull/9187) +* The default config now enables TIER1 outbound connections by default. [#9349](https://github.com/near/nearcore/pull/9349) +* State Sync from GCS is available for experimental use. [#9398](https://github.com/near/nearcore/pull/9398) +* Add prometheus metrics for the internal state of the doomslug. [#9458](https://github.com/near/nearcore/pull/9458) ## 1.35.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 21ac05b1323..c06a76113e6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,7 +63,7 @@ Proposal](https://github.com/near/NEPs/blob/master/neps/nep-0001.md) process. All the contributions to `nearcore` happen via Pull Requests. Please follow the following steps when creating a PR: -1. Fork the `nearcore` repository and create a new branch there to do you work. +1. Fork the `nearcore` repository and create a new branch there to do your work. 2. The branch can contain any number of commits. When merged, all commits will be squashed into a single commit. 3. The changes should be thoroughly tested. Please refer to [this @@ -100,10 +100,12 @@ following steps when creating a PR: need to pass before a PR can be merged. 2. When all the comments from the reviewer(s) have been addressed, they should approve the PR allowing a PR to be merged. -3. An approved PR can be merged by adding the `S-automerge` label to it. The -label can be added by the author if they have the appropriate access or by a -reviewer otherwise. PR authors can also apply label immediately after filing a -PR: removing an additional round-trip after PR is approved. +3. An approved PR can be merged by clicking the "Merge when ready" button. The +button can be clicked by the author if they have the appropriate access, or by a +reviewer otherwise. PR authors can also click the button immediately after filing +a PR; removing an additional round-trip after the PR gets approved. The PR author +will be notified by email by github if the PR fails to land, once it has entered +the merge queue (ie. after it has passed PR CI and gotten an approving review). ## Code review process diff --git a/Cargo.lock b/Cargo.lock index 28e7bd264de..497fe63e7fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2383,6 +2383,7 @@ dependencies = [ "near-primitives", "near-store", "near-test-contracts", + "near-vm-runner", "nearcore", "tempfile", ] @@ -2796,6 +2797,7 @@ dependencies = [ "futures", "hex", "insta", + "itertools", "near-actix-test-utils", "near-async", "near-chain", @@ -3470,6 +3472,7 @@ dependencies = [ "ansi_term", "assert_matches", "borsh 0.10.2", + "bytesize", "chrono", "crossbeam-channel", "delay-detector", @@ -3540,6 +3543,7 @@ dependencies = [ "derive-enum-from-into", "derive_more", "futures", + "itertools", "lru", "near-async", "near-chain", @@ -3718,6 +3722,7 @@ version = "0.0.0" dependencies = [ "borsh 0.10.2", "chrono", + "itertools", "near-cache", "near-chain-configs", "near-chain-primitives", @@ -3748,6 +3753,7 @@ dependencies = [ "nearcore", "rayon", "tqdm", + "tracing", ] [[package]] @@ -4119,6 +4125,7 @@ version = "0.0.0" dependencies = [ "arbitrary", "assert_matches", + "base64 0.21.0", "bencher", "borsh 0.10.2", "bytesize", @@ -4244,6 +4251,7 @@ dependencies = [ "near-ping", "near-primitives", "once_cell", + "sha2 0.10.6", "tokio", "tracing", ] @@ -4439,6 +4447,7 @@ dependencies = [ "near-vm-compiler", "near-vm-compiler-singlepass", "near-vm-engine", + "near-vm-runner", "near-vm-types", "near-vm-vm", "once_cell", @@ -4450,6 +4459,7 @@ dependencies = [ "ripemd", "serde", "serde_json", + "serde_repr", "serde_with", "sha2 0.10.6", "sha3", @@ -4717,6 +4727,7 @@ dependencies = [ "near-crypto", "near-o11y", "near-primitives", + "near-primitives-core", "near-store", "near-test-contracts", "near-vm-runner", diff --git a/Cargo.toml b/Cargo.toml index 7743ea756b0..64a0fab7290 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ version = "0.0.0" # managed by cargo-workspaces, see below authors = ["Near Inc "] edition = "2021" -rust-version = "1.71.0" +rust-version = "1.72.0" repository = "https://github.com/near/nearcore" license = "MIT OR Apache-2.0" @@ -12,6 +12,7 @@ version = "0.17.0" exclude = ["neard"] [workspace] +resolver = "2" members = [ "chain/chain", "chain/chunks", diff --git a/chain/chain-primitives/src/error.rs b/chain/chain-primitives/src/error.rs index 17ceb077bf4..dbcac2f4ee9 100644 --- a/chain/chain-primitives/src/error.rs +++ b/chain/chain-primitives/src/error.rs @@ -188,6 +188,9 @@ pub enum Error { /// Invalid block merkle root. #[error("Invalid Block Merkle Root")] InvalidBlockMerkleRoot, + /// Invalid split shard ids. + #[error("Invalid Split Shard Ids when resharding. shard_id: {0}, parent_shard_id: {1}")] + InvalidSplitShardsIds(u64, u64), /// Someone is not a validator. Usually happens in signature verification #[error("Not A Validator")] NotAValidator, @@ -271,6 +274,7 @@ impl Error { | Error::InvalidStatePayload | Error::InvalidTransactions | Error::InvalidChallenge + | Error::InvalidSplitShardsIds(_, _) | Error::MaliciousChallenge | Error::IncorrectNumberOfChunkHeaders | Error::InvalidEpochHash diff --git a/chain/chain/Cargo.toml b/chain/chain/Cargo.toml index 4bbc2c47cb7..e008cf85449 100644 --- a/chain/chain/Cargo.toml +++ b/chain/chain/Cargo.toml @@ -10,8 +10,10 @@ actix.workspace = true ansi_term.workspace = true assert_matches.workspace = true borsh.workspace = true +bytesize.workspace = true chrono.workspace = true crossbeam-channel.workspace = true +delay-detector.workspace = true enum-map.workspace = true itertools.workspace = true itoa.workspace = true @@ -25,7 +27,6 @@ strum.workspace = true thiserror.workspace = true tracing.workspace = true -delay-detector.workspace = true near-chain-configs.workspace = true near-chain-primitives.workspace = true near-crypto.workspace = true @@ -48,13 +49,19 @@ expensive_tests = [] test_features = [] delay_detector = ["delay-detector/delay_detector"] no_cache = ["near-store/no_cache"] -protocol_feature_reject_blocks_with_outdated_protocol_version = ["near-primitives/protocol_feature_reject_blocks_with_outdated_protocol_version"] -protocol_feature_block_header_v4 = ["near-primitives/protocol_feature_block_header_v4"] +new_epoch_sync = ["near-store/new_epoch_sync", "near-primitives/new_epoch_sync"] + +protocol_feature_reject_blocks_with_outdated_protocol_version = [ + "near-primitives/protocol_feature_reject_blocks_with_outdated_protocol_version", +] +protocol_feature_simple_nightshade_v2 = [ + "near-primitives/protocol_feature_simple_nightshade_v2", +] nightly = [ "nightly_protocol", - "protocol_feature_block_header_v4", "protocol_feature_reject_blocks_with_outdated_protocol_version", + "protocol_feature_simple_nightshade_v2", "near-chain-configs/nightly", "near-client-primitives/nightly", "near-epoch-manager/nightly", diff --git a/chain/chain/src/chain.rs b/chain/chain/src/chain.rs index a8becf1ecdb..21571e8eb5e 100644 --- a/chain/chain/src/chain.rs +++ b/chain/chain/src/chain.rs @@ -37,6 +37,11 @@ use near_primitives::challenge::{ MaybeEncodedShardChunk, PartialState, SlashedValidator, }; use near_primitives::checked_feature; +#[cfg(feature = "new_epoch_sync")] +use near_primitives::epoch_manager::{ + block_info::BlockInfo, + epoch_sync::{BlockHeaderPair, EpochSyncInfo}, +}; use near_primitives::errors::EpochError; use near_primitives::hash::{hash, CryptoHash}; use near_primitives::merkle::{ @@ -71,18 +76,16 @@ use near_primitives::views::{ FinalExecutionOutcomeView, FinalExecutionOutcomeWithReceiptView, FinalExecutionStatus, LightClientBlockView, SignedTransactionView, }; -use near_store::flat::{ - store_helper, FlatStateChanges, FlatStateDelta, FlatStateDeltaMetadata, FlatStorageError, - FlatStorageReadyStatus, FlatStorageStatus, -}; -use near_store::{get_genesis_state_roots, StorageError}; -use near_store::{DBCol, ShardTries, WrappedTrieChanges}; +use near_store::flat::{store_helper, FlatStorageReadyStatus, FlatStorageStatus}; +use near_store::get_genesis_state_roots; +use near_store::{DBCol, ShardTries}; use once_cell::sync::OnceCell; use rand::seq::SliceRandom; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use std::collections::{HashMap, HashSet}; +use std::fmt::{Debug, Formatter}; use std::sync::Arc; use std::time::{Duration as TimeDuration, Instant}; use tracing::{debug, error, info, warn, Span}; @@ -491,13 +494,13 @@ impl Drop for Chain { } } -/// PreprocessBlockResult is a tuple where -/// the first element is a vector of jobs to apply chunks -/// the second element is BlockPreprocessInfo -type PreprocessBlockResult = ( - Vec Result + Send + 'static>>, - BlockPreprocessInfo, -); +/// ApplyChunkJob is a closure that is responsible for applying of a single chunk. +/// All of the chunk details and other arguments are already captured within. +type ApplyChunkJob = Box Result + Send + 'static>; + +/// PreprocessBlockResult is a tuple where the first element is a vector of jobs +/// to apply chunks the second element is BlockPreprocessInfo +type PreprocessBlockResult = (Vec, BlockPreprocessInfo); // Used only for verify_block_hash_and_signature. See that method. #[derive(Clone, Copy, PartialEq, Eq)] @@ -1467,11 +1470,7 @@ impl Chain { let epoch_protocol_version = self.epoch_manager.get_epoch_protocol_version(&epoch_id)?; // Check that block body hash matches the block body. This makes sure that the block body // content is not tampered - if checked_feature!( - "protocol_feature_block_header_v4", - BlockHeaderV4, - epoch_protocol_version - ) { + if checked_feature!("stable", BlockHeaderV4, epoch_protocol_version) { let block_body_hash = block.compute_block_body_hash(); if block_body_hash.is_none() { tracing::warn!("Block version too old for block: {:?}", block.hash()); @@ -1712,17 +1711,18 @@ impl Chain { let mut receipt_proofs_by_shard_id = HashMap::new(); for chunk_header in block.chunks().iter() { - if chunk_header.height_included() == height { - let partial_encoded_chunk = - self.store.get_partial_chunk(&chunk_header.chunk_hash()).unwrap(); - for receipt in partial_encoded_chunk.receipts().iter() { - let ReceiptProof(_, shard_proof) = receipt; - let ShardProof { from_shard_id: _, to_shard_id, proof: _ } = shard_proof; - receipt_proofs_by_shard_id - .entry(*to_shard_id) - .or_insert_with(Vec::new) - .push(receipt.clone()); - } + if chunk_header.height_included() != height { + continue; + } + let partial_encoded_chunk = + self.store.get_partial_chunk(&chunk_header.chunk_hash()).unwrap(); + for receipt in partial_encoded_chunk.receipts().iter() { + let ReceiptProof(_, shard_proof) = receipt; + let ShardProof { to_shard_id, .. } = shard_proof; + receipt_proofs_by_shard_id + .entry(*to_shard_id) + .or_insert_with(Vec::new) + .push(receipt.clone()); } } // sort the receipts deterministically so the order that they will be processed is deterministic @@ -2259,55 +2259,6 @@ impl Chain { Ok(new_head) } - /// Update flat storage for given processed or caught up block, which includes: - /// - merge deltas from current flat storage head to new one; - /// - update flat storage head to the hash of final block visible from given one; - /// - remove info about unreachable blocks from memory. - fn update_flat_storage_for_block( - &mut self, - block: &Block, - shard_uid: ShardUId, - ) -> Result<(), Error> { - if let Some(flat_storage) = self - .runtime_adapter - .get_flat_storage_manager() - .and_then(|manager| manager.get_flat_storage_for_shard(shard_uid)) - { - let mut new_flat_head = *block.header().last_final_block(); - if new_flat_head == CryptoHash::default() { - new_flat_head = *self.genesis.hash(); - } - // Try to update flat head. - flat_storage.update_flat_head(&new_flat_head).unwrap_or_else(|err| { - match &err { - FlatStorageError::BlockNotSupported(_) => { - // It's possible that new head is not a child of current flat head, e.g. when we have a - // fork: - // - // (flat head) /-------> 6 - // 1 -> 2 -> 3 -> 4 - // \---> 5 - // - // where during postprocessing (5) we call `update_flat_head(3)` and then for (6) we can - // call `update_flat_head(2)` because (2) will be last visible final block from it. - // In such case, just log an error. - debug!(target: "chain", "Cannot update flat head to {:?}: {:?}", new_flat_head, err); - } - _ => { - // All other errors are unexpected, so we panic. - panic!("Cannot update flat head to {:?}: {:?}", new_flat_head, err); - } - } - }); - } else { - // TODO (#8250): come up with correct assertion. Currently it doesn't work because runtime may be - // implemented by KeyValueRuntime which doesn't support flat storage, and flat storage background - // creation may happen. - // debug_assert!(false, "Flat storage state for shard {shard_id} does not exist and its creation was not initiated"); - } - Ok(()) - } - /// Run postprocessing on this block, which stores the block on chain. /// Check that if accepting the block unlocks any orphans in the orphan pool and start /// the processing of those blocks. @@ -2391,7 +2342,9 @@ impl Chain { if need_flat_storage_update { let shard_uid = self.epoch_manager.shard_id_to_uid(shard_id, epoch_id)?; - self.update_flat_storage_for_block(&block, shard_uid)?; + if let Some(manager) = self.runtime_adapter.get_flat_storage_manager() { + manager.update_flat_storage_for_shard(shard_uid, &block)?; + } } } @@ -2759,8 +2712,16 @@ impl Chain { parent_hash: &CryptoHash, shard_id: ShardId, ) -> bool { - let will_shard_layout_change = - epoch_manager.will_shard_layout_change(parent_hash).unwrap_or(false); + 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, + Err(err) => { + // TODO(resharding) This is a problem, if this happens the node + // will not perform resharding and fall behind the network. + tracing::error!(target: "chain", ?err, "failed to check if shard layout will change"); + false + } + }; // if shard layout will change the next epoch, we should catch up the shard regardless // whether we already have the shard's state this epoch, because we need to generate // new states for shards split from the current shard for the next epoch @@ -3389,15 +3350,22 @@ impl Chain { // Flat storage must not exist at this point because leftover keys corrupt its state. assert!(flat_storage_manager.get_flat_storage_for_shard(shard_uid).is_none()); + let flat_head_hash = *chunk.prev_block(); + let flat_head_header = self.get_block_header(&flat_head_hash)?; + let flat_head_prev_hash = *flat_head_header.prev_hash(); + let flat_head_height = flat_head_header.height(); + + 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, shard_uid, FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: near_store::flat::BlockInfo { - hash: *block_hash, - prev_hash: *block_header.prev_hash(), - height: block_header.height(), + hash: flat_head_hash, + prev_hash: flat_head_prev_hash, + height: flat_head_height, }, }), ); @@ -3523,13 +3491,9 @@ impl Chain { results: Vec>, ) -> Result<(), Error> { let block = self.store.get_block(block_hash)?; - let prev_block = self.store.get_block(block.header().prev_hash())?; let mut chain_update = self.chain_update(); - chain_update.apply_chunk_postprocessing( - &block, - &prev_block, - results.into_iter().collect::, Error>>()?, - )?; + let results = results.into_iter().collect::, Error>>()?; + chain_update.apply_chunk_postprocessing(&block, results)?; chain_update.commit()?; let epoch_id = block.header().epoch_id(); @@ -3548,7 +3512,9 @@ impl Chain { true, ) { let shard_uid = self.epoch_manager.shard_id_to_uid(shard_id, epoch_id)?; - self.update_flat_storage_for_block(&block, shard_uid)?; + if let Some(manager) = self.runtime_adapter.get_flat_storage_manager() { + manager.update_flat_storage_for_shard(shard_uid, &block)?; + } } } @@ -3802,7 +3768,10 @@ impl Chain { let next_shard_layout = self.epoch_manager.get_shard_layout(block.header().next_epoch_id())?; let new_shards = next_shard_layout.get_split_shard_uids(shard_id).unwrap_or_else(|| { - panic!("shard layout must contain maps of all shards to its split shards {}", shard_id) + panic!( + "shard layout must contain maps of all shards to its split shards {} {:?}", + shard_id, next_shard_layout, + ); }); new_shards .iter() @@ -3823,346 +3792,417 @@ impl Chain { mode: ApplyChunksMode, mut state_patch: SandboxStatePatch, invalid_chunks: &mut Vec, - ) -> Result< - Vec Result + Send + 'static>>, - Error, - > { + ) -> Result, Error> { let _span = tracing::debug_span!(target: "chain", "apply_chunks_preprocessing").entered(); - #[cfg(not(feature = "mock_node"))] - let protocol_version = - self.epoch_manager.get_epoch_protocol_version(block.header().epoch_id())?; - let prev_hash = block.header().prev_hash(); let will_shard_layout_change = self.epoch_manager.will_shard_layout_change(prev_hash)?; let prev_chunk_headers = Chain::get_prev_chunk_headers(self.epoch_manager.as_ref(), prev_block)?; - let mut process_one_chunk = |shard_id: usize, - chunk_header: &ShardChunkHeader, - prev_chunk_header: &ShardChunkHeader| - -> Result< - Option Result + Send + 'static>>, - Error, - > { - // XXX: This is a bit questionable -- sandbox state patching works - // only for a single shard. This so far has been enough. - let state_patch = state_patch.take(); + block + .chunks() + .iter() + .zip(prev_chunk_headers.iter()) + .enumerate() + .filter_map(|(shard_id, (chunk_header, prev_chunk_header))| { + // XXX: This is a bit questionable -- sandbox state patching works + // only for a single shard. This so far has been enough. + let state_patch = state_patch.take(); - let shard_id = shard_id as ShardId; - let cares_about_shard_this_epoch = - self.shard_tracker.care_about_shard(me.as_ref(), prev_hash, shard_id, true); - let cares_about_shard_next_epoch = - self.shard_tracker.will_care_about_shard(me.as_ref(), prev_hash, shard_id, true); - // We want to guarantee that transactions are only applied once for each shard, even - // though apply_chunks may be called twice, once with ApplyChunksMode::NotCaughtUp - // once with ApplyChunksMode::CatchingUp - // Note that this variable does not guard whether we split states or not, see the comments - // before `need_to_split_state` - let should_apply_transactions = match mode { - // next epoch's shard states are not ready, only update this epoch's shards - ApplyChunksMode::NotCaughtUp => cares_about_shard_this_epoch, - // update both this epoch and next epoch - ApplyChunksMode::IsCaughtUp => { - cares_about_shard_this_epoch || cares_about_shard_next_epoch - } - // catching up next epoch's shard states, do not update this epoch's shard state - // since it has already been updated through ApplyChunksMode::NotCaughtUp - ApplyChunksMode::CatchingUp => { - !cares_about_shard_this_epoch && cares_about_shard_next_epoch - } - }; - let need_to_split_states = will_shard_layout_change && cares_about_shard_next_epoch; - // We can only split states when states are ready, i.e., mode != ApplyChunksMode::NotCaughtUp - // 1) if should_apply_transactions == true && split_state_roots.is_some(), - // that means split states are ready. - // `apply_split_state_changes` will apply updates to split_states - // 2) if should_apply_transactions == true && split_state_roots.is_none(), - // that means split states are not ready yet. - // `apply_split_state_changes` will return `state_changes_for_split_states`, - // which will be stored to the database in `process_apply_chunks` - // 3) if should_apply_transactions == false && split_state_roots.is_some() - // This implies mode == CatchingUp and cares_about_shard_this_epoch == true, - // otherwise should_apply_transactions will be true - // That means transactions have already been applied last time when apply_chunks are - // called with mode NotCaughtUp, therefore `state_changes_for_split_states` have been - // stored in the database. Then we can safely read that and apply that to the split - // states - let split_state_roots = if need_to_split_states { - match mode { - ApplyChunksMode::IsCaughtUp | ApplyChunksMode::CatchingUp => { - Some(self.get_split_state_roots(block, shard_id)?) + let apply_chunk_job = self.get_apply_chunk_job( + me, + block, + prev_block, + chunk_header, + prev_chunk_header, + shard_id, + mode, + will_shard_layout_change, + incoming_receipts, + state_patch, + ); + + match apply_chunk_job { + Ok(Some(processor)) => Some(Ok(processor)), + Ok(None) => None, + Err(err) => { + if err.is_bad_data() { + invalid_chunks.push(chunk_header.clone()); + } + Some(Err(err)) } - ApplyChunksMode::NotCaughtUp => None, } + }) + .collect() + } + + /// This method returns the closure that is responsible for applying of a single chunk. + fn get_apply_chunk_job( + &self, + me: &Option, + block: &Block, + prev_block: &Block, + chunk_header: &ShardChunkHeader, + prev_chunk_header: &ShardChunkHeader, + shard_id: usize, + mode: ApplyChunksMode, + will_shard_layout_change: bool, + incoming_receipts: &HashMap>, + state_patch: SandboxStatePatch, + ) -> Result, Error> { + let shard_id = shard_id as ShardId; + let prev_hash = block.header().prev_hash(); + let cares_about_shard_this_epoch = + self.shard_tracker.care_about_shard(me.as_ref(), prev_hash, shard_id, true); + let cares_about_shard_next_epoch = + self.shard_tracker.will_care_about_shard(me.as_ref(), prev_hash, shard_id, true); + let should_apply_transactions = get_should_apply_transactions( + mode, + cares_about_shard_this_epoch, + cares_about_shard_next_epoch, + ); + let need_to_split_states = will_shard_layout_change && cares_about_shard_next_epoch; + // We can only split states when states are ready, i.e., mode != ApplyChunksMode::NotCaughtUp + // 1) if should_apply_transactions == true && split_state_roots.is_some(), + // that means split states are ready. + // `apply_split_state_changes` will apply updates to split_states + // 2) if should_apply_transactions == true && split_state_roots.is_none(), + // that means split states are not ready yet. + // `apply_split_state_changes` will return `state_changes_for_split_states`, + // which will be stored to the database in `process_apply_chunks` + // 3) if should_apply_transactions == false && split_state_roots.is_some() + // This implies mode == CatchingUp and cares_about_shard_this_epoch == true, + // otherwise should_apply_transactions will be true + // That means transactions have already been applied last time when apply_chunks are + // called with mode NotCaughtUp, therefore `state_changes_for_split_states` have been + // stored in the database. Then we can safely read that and apply that to the split + // states + let split_state_roots = if need_to_split_states && mode != ApplyChunksMode::NotCaughtUp { + Some(self.get_split_state_roots(block, shard_id)?) + } else { + None + }; + + let shard_uid = self.epoch_manager.shard_id_to_uid(shard_id, block.header().epoch_id())?; + let is_new_chunk = chunk_header.height_included() == block.header().height(); + let epoch_manager = self.epoch_manager.clone(); + let runtime = self.runtime_adapter.clone(); + if should_apply_transactions { + if is_new_chunk { + self.get_apply_chunk_job_new_chunk( + block, + prev_block, + chunk_header, + prev_chunk_header, + shard_uid, + will_shard_layout_change, + incoming_receipts, + state_patch, + runtime, + epoch_manager, + split_state_roots, + ) } else { - None + self.get_apply_chunk_job_old_chunk( + block, + prev_block, + shard_uid, + will_shard_layout_change, + state_patch, + runtime, + epoch_manager, + split_state_roots, + ) + } + } else if let Some(split_state_roots) = split_state_roots { + // Case 3), split state are ready. Read the state changes from the + // database and apply them to the split states. + assert!(mode == ApplyChunksMode::CatchingUp && cares_about_shard_this_epoch); + self.get_apply_chunk_job_split_state( + block, + shard_uid, + runtime, + epoch_manager, + split_state_roots, + ) + } else { + Ok(None) + } + } + + /// Returns the apply chunk job when applying a new chunk and applying transactions. + fn get_apply_chunk_job_new_chunk( + &self, + block: &Block, + prev_block: &Block, + chunk_header: &ShardChunkHeader, + prev_chunk_header: &ShardChunkHeader, + shard_uid: ShardUId, + will_shard_layout_change: bool, + incoming_receipts: &HashMap>, + state_patch: SandboxStatePatch, + runtime: Arc, + epoch_manager: Arc, + split_state_roots: Option>, + ) -> Result, Error> { + let prev_hash = block.header().prev_hash(); + let shard_id = shard_uid.shard_id(); + + let prev_chunk_height_included = prev_chunk_header.height_included(); + // Validate state root. + let prev_chunk_extra = self.get_chunk_extra(prev_hash, &shard_uid)?; + + // Validate that all next chunk information matches previous chunk extra. + validate_chunk_with_chunk_extra( + // It's safe here to use ChainStore instead of ChainStoreUpdate + // because we're asking prev_chunk_header for already committed block + self.store(), + self.epoch_manager.as_ref(), + prev_hash, + &prev_chunk_extra, + prev_chunk_height_included, + chunk_header, + ) + .map_err(|err| { + warn!( + target: "chain", + ?err, + prev_block_hash=?prev_hash, + block_hash=?block.header().hash(), + shard_id, + prev_chunk_height_included, + ?prev_chunk_extra, + ?chunk_header, + "Failed to validate chunk extra"); + byzantine_assert!(false); + match self.create_chunk_state_challenge(prev_block, block, chunk_header) { + Ok(chunk_state) => Error::InvalidChunkState(Box::new(chunk_state)), + Err(err) => err, + } + })?; + // we can't use hash from the current block here yet because the incoming receipts + // for this block is not stored yet + let mut receipts = collect_receipts(incoming_receipts.get(&shard_id).unwrap()); + let receipt_proof_response = &self.store().get_incoming_receipts_for_shard( + shard_id, + *prev_hash, + prev_chunk_height_included, + )?; + receipts.extend(collect_receipts_from_response(receipt_proof_response)); + let chunk = self.get_chunk_clone_from_header(&chunk_header.clone())?; + + let transactions = chunk.transactions(); + if !validate_transactions_order(transactions) { + let merkle_paths = Block::compute_chunk_headers_root(block.chunks().iter()).1; + let chunk_proof = ChunkProofs { + block_header: block.header().try_to_vec().expect("Failed to serialize"), + merkle_proof: merkle_paths[shard_id as usize].clone(), + chunk: MaybeEncodedShardChunk::Decoded(chunk), }; - let shard_uid = - self.epoch_manager.shard_id_to_uid(shard_id, block.header().epoch_id())?; - let is_new_chunk = chunk_header.height_included() == block.header().height(); - let epoch_manager = self.epoch_manager.clone(); - let runtime = self.runtime_adapter.clone(); - if should_apply_transactions { - if is_new_chunk { - let prev_chunk_height_included = prev_chunk_header.height_included(); - // Validate state root. - let prev_chunk_extra = self.get_chunk_extra(prev_hash, &shard_uid)?; - - // Validate that all next chunk information matches previous chunk extra. - validate_chunk_with_chunk_extra( - // It's safe here to use ChainStore instead of ChainStoreUpdate - // because we're asking prev_chunk_header for already committed block - self.store(), - self.epoch_manager.as_ref(), - block.header().prev_hash(), - &prev_chunk_extra, - prev_chunk_height_included, - chunk_header, + return Err(Error::InvalidChunkProofs(Box::new(chunk_proof))); + } + + // if we are running mock_node, ignore this check because + // this check may require old block headers, which may not exist in storage + // of the client in the mock network + #[cfg(not(feature = "mock_node"))] + let protocol_version = + self.epoch_manager.get_epoch_protocol_version(block.header().epoch_id())?; + + #[cfg(not(feature = "mock_node"))] + if checked_feature!("stable", AccessKeyNonceRange, protocol_version) { + let transaction_validity_period = self.transaction_validity_period; + for transaction in transactions { + self.store() + .check_transaction_validity_period( + prev_block.header(), + &transaction.transaction.block_hash, + transaction_validity_period, ) - .map_err(|e| { - warn!(target: "chain", "Failed to validate chunk extra: {:?}.\n\ - block prev_hash: {}\n\ - block hash: {}\n\ - shard_id: {}\n\ - prev_chunk_height_included: {}\n\ - prev_chunk_extra: {:#?}\n\ - chunk_header: {:#?}", e,block.header().prev_hash(),block.header().hash(),shard_id,prev_chunk_height_included,prev_chunk_extra,chunk_header); - byzantine_assert!(false); - match self.create_chunk_state_challenge(prev_block, block, chunk_header) { - Ok(chunk_state) => { - Error::InvalidChunkState(Box::new(chunk_state)) - } - Err(err) => err, - } + .map_err(|_| { + tracing::warn!("Invalid Transactions for mock node"); + Error::from(Error::InvalidTransactions) })?; - // we can't use hash from the current block here yet because the incoming receipts - // for this block is not stored yet - let mut receipts = collect_receipts(incoming_receipts.get(&shard_id).unwrap()); - receipts.extend(collect_receipts_from_response( - &self.store().get_incoming_receipts_for_shard( - shard_id, - *prev_hash, - prev_chunk_height_included, - )?, - )); - let chunk = self.get_chunk_clone_from_header(&chunk_header.clone())?; - - let transactions = chunk.transactions(); - if !validate_transactions_order(transactions) { - let merkle_paths = - Block::compute_chunk_headers_root(block.chunks().iter()).1; - let chunk_proof = ChunkProofs { - block_header: block.header().try_to_vec().expect("Failed to serialize"), - merkle_proof: merkle_paths[shard_id as usize].clone(), - chunk: MaybeEncodedShardChunk::Decoded(chunk), - }; - return Err(Error::InvalidChunkProofs(Box::new(chunk_proof))); - } + } + }; - // if we are running mock_node, ignore this check because - // this check may require old block headers, which may not exist in storage - // of the client in the mock network - #[cfg(not(feature = "mock_node"))] - if checked_feature!("stable", AccessKeyNonceRange, protocol_version) { - let transaction_validity_period = self.transaction_validity_period; - for transaction in transactions { - self.store() - .check_transaction_validity_period( - prev_block.header(), - &transaction.transaction.block_hash, - transaction_validity_period, - ) - .map_err(|_| { - tracing::warn!("Invalid Transactions for mock node"); - Error::from(Error::InvalidTransactions) - })?; - } - }; + let chunk_inner = chunk.cloned_header().take_inner(); + let gas_limit = chunk_inner.gas_limit(); - let chunk_inner = chunk.cloned_header().take_inner(); - let gas_limit = chunk_inner.gas_limit(); + // This variable is responsible for checking to which block we can apply receipts previously lost in apply_chunks + // (see https://github.com/near/nearcore/pull/4248/) + // We take the first block with existing chunk in the first epoch in which protocol feature + // RestoreReceiptsAfterFixApplyChunks was enabled, and put the restored receipts there. + let is_first_block_with_chunk_of_version = check_if_block_is_first_with_chunk_of_version( + self.store(), + epoch_manager.as_ref(), + prev_block.hash(), + shard_id, + )?; - // This variable is responsible for checking to which block we can apply receipts previously lost in apply_chunks - // (see https://github.com/near/nearcore/pull/4248/) - // We take the first block with existing chunk in the first epoch in which protocol feature - // RestoreReceiptsAfterFixApplyChunks was enabled, and put the restored receipts there. - let is_first_block_with_chunk_of_version = - check_if_block_is_first_with_chunk_of_version( - self.store(), + let block_hash = *block.hash(); + let challenges_result = block.header().challenges_result().clone(); + let block_timestamp = block.header().raw_timestamp(); + let gas_price = prev_block.header().gas_price(); + let random_seed = *block.header().random_value(); + let height = chunk_header.height_included(); + let prev_block_hash = *chunk_header.prev_block_hash(); + + Ok(Some(Box::new(move |parent_span| -> Result { + let _span = tracing::debug_span!( + target: "chain", + parent: parent_span, + "new_chunk", + shard_id) + .entered(); + let _timer = CryptoHashTimer::new(chunk.chunk_hash().0); + match runtime.apply_transactions( + shard_id, + chunk_inner.prev_state_root(), + height, + block_timestamp, + &prev_block_hash, + &block_hash, + &receipts, + chunk.transactions(), + chunk_inner.validator_proposals(), + gas_price, + gas_limit, + &challenges_result, + random_seed, + true, + is_first_block_with_chunk_of_version, + state_patch, + true, + ) { + Ok(apply_result) => { + let apply_split_result_or_state_changes = if will_shard_layout_change { + Some(ChainUpdate::apply_split_state_changes( epoch_manager.as_ref(), - prev_block.hash(), - shard_id, - )?; - - let block_hash = *block.hash(); - let challenges_result = block.header().challenges_result().clone(); - let block_timestamp = block.header().raw_timestamp(); - let gas_price = prev_block.header().gas_price(); - let random_seed = *block.header().random_value(); - let height = chunk_header.height_included(); - let prev_block_hash = *chunk_header.prev_block_hash(); - - Ok(Some(Box::new(move |parent_span| -> Result { - let _span = tracing::debug_span!( - target: "chain", - parent: parent_span, - "new_chunk", - shard_id) - .entered(); - let _timer = CryptoHashTimer::new(chunk.chunk_hash().0); - match runtime.apply_transactions( - shard_id, - chunk_inner.prev_state_root(), - height, - block_timestamp, - &prev_block_hash, + runtime.as_ref(), &block_hash, - &receipts, - chunk.transactions(), - chunk_inner.validator_proposals(), - gas_price, - gas_limit, - &challenges_result, - random_seed, - true, - is_first_block_with_chunk_of_version, - state_patch, - true, - ) { - Ok(apply_result) => { - let apply_split_result_or_state_changes = - if will_shard_layout_change { - Some(ChainUpdate::apply_split_state_changes( - epoch_manager.as_ref(), - runtime.as_ref(), - &block_hash, - &prev_block_hash, - &apply_result, - split_state_roots, - )?) - } else { - None - }; - Ok(ApplyChunkResult::SameHeight(SameHeightResult { - gas_limit, - shard_uid, - apply_result, - apply_split_result_or_state_changes, - })) - } - Err(err) => Err(err), - } - }))) - } else { - let new_extra = self.get_chunk_extra(prev_block.hash(), &shard_uid)?; - - let block_hash = *block.hash(); - let challenges_result = block.header().challenges_result().clone(); - let block_timestamp = block.header().raw_timestamp(); - let gas_price = block.header().gas_price(); - let random_seed = *block.header().random_value(); - let height = block.header().height(); - let prev_block_hash = *prev_block.hash(); - - Ok(Some(Box::new(move |parent_span| -> Result { - let _span = tracing::debug_span!( - target: "chain", - parent: parent_span, - "existing_chunk", - shard_id) - .entered(); - match runtime.apply_transactions( - shard_id, - new_extra.state_root(), - height, - block_timestamp, &prev_block_hash, - &block_hash, - &[], - &[], - new_extra.validator_proposals(), - gas_price, - new_extra.gas_limit(), - &challenges_result, - random_seed, - false, - false, - state_patch, - true, - ) { - Ok(apply_result) => { - let apply_split_result_or_state_changes = - if will_shard_layout_change { - Some(ChainUpdate::apply_split_state_changes( - epoch_manager.as_ref(), - runtime.as_ref(), - &block_hash, - &prev_block_hash, - &apply_result, - split_state_roots, - )?) - } else { - None - }; - Ok(ApplyChunkResult::DifferentHeight(DifferentHeightResult { - shard_uid, - apply_result, - apply_split_result_or_state_changes, - })) - } - Err(err) => Err(err), - } - }))) - } - } else if let Some(split_state_roots) = split_state_roots { - // case 3) - assert!(mode == ApplyChunksMode::CatchingUp && cares_about_shard_this_epoch); - // Split state are ready. Read the state changes from the database and apply them - // to the split states. - let next_epoch_shard_layout = - epoch_manager.get_shard_layout(block.header().next_epoch_id())?; - let state_changes = - self.store().get_state_changes_for_split_states(block.hash(), shard_id)?; - let block_hash = *block.hash(); - Ok(Some(Box::new(move |parent_span| -> Result { - let _span = tracing::debug_span!( - target: "chain", - parent: parent_span, - "split_state", - shard_id, - ?shard_uid) - .entered(); - Ok(ApplyChunkResult::SplitState(SplitStateResult { + &apply_result, + split_state_roots, + )?) + } else { + None + }; + Ok(ApplyChunkResult::SameHeight(SameHeightResult { + gas_limit, shard_uid, - results: runtime.apply_update_to_split_states( + apply_result, + apply_split_result_or_state_changes, + })) + } + Err(err) => Err(err), + } + }))) + } + + /// Returns the apply chunk job when applying an old chunk and applying transactions. + fn get_apply_chunk_job_old_chunk( + &self, + block: &Block, + prev_block: &Block, + shard_uid: ShardUId, + will_shard_layout_change: bool, + state_patch: SandboxStatePatch, + runtime: Arc, + epoch_manager: Arc, + split_state_roots: Option>, + ) -> Result, Error> { + let shard_id = shard_uid.shard_id(); + let new_extra = self.get_chunk_extra(prev_block.hash(), &shard_uid)?; + + let block_hash = *block.hash(); + let challenges_result = block.header().challenges_result().clone(); + let block_timestamp = block.header().raw_timestamp(); + let gas_price = block.header().gas_price(); + let random_seed = *block.header().random_value(); + let height = block.header().height(); + let prev_block_hash = *prev_block.hash(); + + Ok(Some(Box::new(move |parent_span| -> Result { + let _span = tracing::debug_span!( + target: "chain", + parent: parent_span, + "existing_chunk", + shard_id) + .entered(); + match runtime.apply_transactions( + shard_id, + new_extra.state_root(), + height, + block_timestamp, + &prev_block_hash, + &block_hash, + &[], + &[], + new_extra.validator_proposals(), + gas_price, + new_extra.gas_limit(), + &challenges_result, + random_seed, + false, + false, + state_patch, + true, + ) { + Ok(apply_result) => { + let apply_split_result_or_state_changes = if will_shard_layout_change { + Some(ChainUpdate::apply_split_state_changes( + epoch_manager.as_ref(), + runtime.as_ref(), &block_hash, + &prev_block_hash, + &apply_result, split_state_roots, - &next_epoch_shard_layout, - state_changes, - )?, + )?) + } else { + None + }; + Ok(ApplyChunkResult::DifferentHeight(DifferentHeightResult { + shard_uid, + apply_result, + apply_split_result_or_state_changes, })) - }))) - } else { - Ok(None) - } - }; - block - .chunks() - .iter() - .zip(prev_chunk_headers.iter()) - .enumerate() - .filter_map(|(shard_id, (chunk_header, prev_chunk_header))| { - match process_one_chunk(shard_id, chunk_header, prev_chunk_header) { - Ok(Some(processor)) => Some(Ok(processor)), - Ok(None) => None, - Err(err) => { - if err.is_bad_data() { - invalid_chunks.push(chunk_header.clone()); - } - Some(Err(err)) - } } - }) - .collect() + Err(err) => Err(err), + } + }))) + } + + /// Returns the apply chunk job when just splitting state but not applying transactions. + fn get_apply_chunk_job_split_state( + &self, + block: &Block, + shard_uid: ShardUId, + runtime: Arc, + epoch_manager: Arc, + split_state_roots: HashMap, + ) -> Result, Error> { + let shard_id = shard_uid.shard_id(); + let next_epoch_shard_layout = + epoch_manager.get_shard_layout(block.header().next_epoch_id())?; + let state_changes = + self.store().get_state_changes_for_split_states(block.hash(), shard_id)?; + let block_hash = *block.hash(); + Ok(Some(Box::new(move |parent_span| -> Result { + let _span = tracing::debug_span!( + target: "chain", + parent: parent_span, + "split_state", + shard_id, + ?shard_uid) + .entered(); + let results = runtime.apply_update_to_split_states( + &block_hash, + split_state_roots, + &next_epoch_shard_layout, + state_changes, + )?; + Ok(ApplyChunkResult::SplitState(SplitStateResult { shard_uid, results })) + }))) } /// Checks if the time has come to make a state snapshot for testing purposes. @@ -4192,13 +4232,41 @@ impl Chain { let head = self.head()?; let epoch_id = self.epoch_manager.get_epoch_id(&head.prev_block_hash)?; let shard_layout = self.epoch_manager.get_shard_layout(&epoch_id)?; - (helper.make_snapshot_callback)(head.prev_block_hash, shard_layout.get_shard_uids()) + let last_block = self.get_block(&head.last_block_hash)?; + (helper.make_snapshot_callback)( + head.prev_block_hash, + shard_layout.get_shard_uids(), + last_block, + ) } } Ok(()) } } +/// We want to guarantee that transactions are only applied once for each shard, +/// even though apply_chunks may be called twice, once with +/// ApplyChunksMode::NotCaughtUp once with ApplyChunksMode::CatchingUp. Note +/// that it does not guard whether we split states or not, see the comments +/// before `need_to_split_state` +fn get_should_apply_transactions( + mode: ApplyChunksMode, + cares_about_shard_this_epoch: bool, + cares_about_shard_next_epoch: bool, +) -> bool { + match mode { + // next epoch's shard states are not ready, only update this epoch's shards + ApplyChunksMode::NotCaughtUp => cares_about_shard_this_epoch, + // update both this epoch and next epoch + ApplyChunksMode::IsCaughtUp => cares_about_shard_this_epoch || cares_about_shard_next_epoch, + // catching up next epoch's shard states, do not update this epoch's shard state + // since it has already been updated through ApplyChunksMode::NotCaughtUp + ApplyChunksMode::CatchingUp => { + !cares_about_shard_this_epoch && cares_about_shard_next_epoch + } + } +} + /// Implement block merkle proof retrieval. impl Chain { fn combine_maybe_hashes( @@ -4811,6 +4879,7 @@ pub struct ChainUpdate<'a> { transaction_validity_period: BlockHeightDelta, } +#[derive(Debug)] pub struct SameHeightResult { shard_uid: ShardUId, gas_limit: Gas, @@ -4818,18 +4887,21 @@ pub struct SameHeightResult { apply_split_result_or_state_changes: Option, } +#[derive(Debug)] pub struct DifferentHeightResult { shard_uid: ShardUId, apply_result: ApplyTransactionResult, apply_split_result_or_state_changes: Option, } +#[derive(Debug)] pub struct SplitStateResult { // parent shard of the split states shard_uid: ShardUId, results: Vec, } +#[derive(Debug)] pub enum ApplyChunkResult { SameHeight(SameHeightResult), DifferentHeight(DifferentHeightResult), @@ -4879,13 +4951,14 @@ impl<'a> ChainUpdate<'a> { self.chain_store_update.commit() } - /// For all the outgoing receipts generated in block `hash` at the shards we are tracking - /// in this epoch, - /// save a mapping from receipt ids to the destination shard ids that the receipt will be sent - /// to in the next block. - /// Note that this function should be called after `save_block` is called on this block because - /// it requires that the block info is available in EpochManager, otherwise it will return an - /// error. + /// For all the outgoing receipts generated in block `hash` at the shards we + /// are tracking in this epoch, save a mapping from receipt ids to the + /// destination shard ids that the receipt will be sent to in the next + /// block. + /// + /// Note that this function should be called after `save_block` is called on + /// this block because it requires that the block info is available in + /// EpochManager, otherwise it will return an error. pub fn save_receipt_id_to_shard_id_for_block( &mut self, me: &Option, @@ -4894,55 +4967,54 @@ impl<'a> ChainUpdate<'a> { num_shards: NumShards, ) -> Result<(), Error> { for shard_id in 0..num_shards { - if self.shard_tracker.care_about_shard( + let care_about_shard = self.shard_tracker.care_about_shard( me.as_ref(), &prev_hash, shard_id as ShardId, true, - ) { - let receipt_id_to_shard_id: HashMap<_, _> = { - // it can be empty if there is no new chunk for this shard - if let Ok(outgoing_receipts) = - self.chain_store_update.get_outgoing_receipts(hash, shard_id) - { - let shard_layout = - self.epoch_manager.get_shard_layout_from_prev_block(hash)?; - outgoing_receipts - .iter() - .map(|receipt| { - ( - receipt.receipt_id, - account_id_to_shard_id(&receipt.receiver_id, &shard_layout), - ) - }) - .collect() - } else { - HashMap::new() - } - }; - for (receipt_id, shard_id) in receipt_id_to_shard_id { - self.chain_store_update.save_receipt_id_to_shard_id(receipt_id, shard_id); - } + ); + if !care_about_shard { + continue; + } + let receipt_id_to_shard_id = self.get_receipt_id_to_shard_id(hash, shard_id)?; + for (receipt_id, shard_id) in receipt_id_to_shard_id { + self.chain_store_update.save_receipt_id_to_shard_id(receipt_id, shard_id); } } Ok(()) } + /// Returns a mapping from the receipt id to the destination shard id. + fn get_receipt_id_to_shard_id( + &mut self, + hash: &CryptoHash, + shard_id: u64, + ) -> Result, Error> { + let outgoing_receipts = self.chain_store_update.get_outgoing_receipts(hash, shard_id); + let outgoing_receipts = if let Ok(outgoing_receipts) = outgoing_receipts { + outgoing_receipts + } else { + return Ok(HashMap::new()); + }; + let shard_layout = self.epoch_manager.get_shard_layout_from_prev_block(hash)?; + let outgoing_receipts = outgoing_receipts + .iter() + .map(|receipt| { + (receipt.receipt_id, account_id_to_shard_id(&receipt.receiver_id, &shard_layout)) + }) + .collect(); + Ok(outgoing_receipts) + } + fn apply_chunk_postprocessing( &mut self, block: &Block, - prev_block: &Block, apply_results: Vec, ) -> Result<(), Error> { let _span = tracing::debug_span!(target: "chain", "apply_chunk_postprocessing").entered(); for result in apply_results { - self.process_apply_chunk_result( - result, - *block.hash(), - block.header().height(), - *prev_block.hash(), - )? + self.process_apply_chunk_result(block, result)? } Ok(()) } @@ -4989,13 +5061,22 @@ impl<'a> ChainUpdate<'a> { /// for state changes, store the state changes for splitting states fn process_split_state( &mut self, - block_hash: &CryptoHash, - prev_block_hash: &CryptoHash, + block: &Block, shard_uid: &ShardUId, apply_results_or_state_changes: ApplySplitStateResultOrStateChanges, ) -> Result<(), Error> { + let block_hash = block.hash(); + let prev_hash = block.header().prev_hash(); + let height = block.header().height(); match apply_results_or_state_changes { - ApplySplitStateResultOrStateChanges::ApplySplitStateResults(results) => { + ApplySplitStateResultOrStateChanges::ApplySplitStateResults(mut results) => { + tracing::debug!(target: "resharding", height, ?shard_uid, "process_split_state apply"); + + // Sort the results so that the gas reassignment is deterministic. + results.sort_unstable_by_key(|r| r.shard_uid); + // Drop the mutability as we no longer need it. + let results = results; + // Split validator_proposals, gas_burnt, balance_burnt to each split shard // and store the chunk extra for split shards // Note that here we do not split outcomes by the new shard layout, we simply store @@ -5006,18 +5087,18 @@ impl<'a> ChainUpdate<'a> { let chunk_extra = self.chain_store_update.get_chunk_extra(block_hash, shard_uid)?; let next_epoch_shard_layout = { let epoch_id = - self.epoch_manager.get_next_epoch_id_from_prev_block(prev_block_hash)?; + self.epoch_manager.get_next_epoch_id_from_prev_block(prev_hash)?; self.epoch_manager.get_shard_layout(&epoch_id)? }; let mut validator_proposals_by_shard: HashMap<_, Vec<_>> = HashMap::new(); for validator_proposal in chunk_extra.validator_proposals() { - let shard_id = account_id_to_shard_uid( + let shard_uid = account_id_to_shard_uid( validator_proposal.account_id(), &next_epoch_shard_layout, ); validator_proposals_by_shard - .entry(shard_id) + .entry(shard_uid) .or_default() .push(validator_proposal); } @@ -5026,21 +5107,49 @@ impl<'a> ChainUpdate<'a> { .get_split_shard_uids(shard_uid.shard_id()) .unwrap_or_else(|| panic!("invalid shard layout {:?}", next_epoch_shard_layout)) .len() as NumShards; + let total_gas_used = chunk_extra.gas_used(); let total_balance_burnt = chunk_extra.balance_burnt(); - let gas_res = total_gas_used % num_split_shards; + + // The gas remainder, the split shards will be reassigned one + // unit each until its depleted. + let mut gas_res = total_gas_used % num_split_shards; + // The gas quotient, the split shards will be reassigned the + // full value each. let gas_split = total_gas_used / num_split_shards; - let balance_res = (total_balance_burnt % num_split_shards as u128) as NumShards; + + // The balance remainder, the split shards will be reassigned one + // unit each until its depleted. + let mut balance_res = (total_balance_burnt % num_split_shards as u128) as NumShards; + // The balance quotient, the split shards will be reassigned the + // full value each. let balance_split = total_balance_burnt / (num_split_shards as u128); + let gas_limit = chunk_extra.gas_limit(); let outcome_root = *chunk_extra.outcome_root(); let mut sum_gas_used = 0; let mut sum_balance_burnt = 0; + + // The gas and balance distribution assumes that we have a result for every split shard. + // TODO(resharding) make sure that is the case. + assert_eq!(num_split_shards, results.len() as u64); + for result in results { - let shard_id = result.shard_uid.shard_id(); - let gas_burnt = gas_split + if shard_id < gas_res { 1 } else { 0 }; - let balance_burnt = balance_split + if shard_id < balance_res { 1 } else { 0 }; + let gas_burnt = if gas_res > 0 { + gas_res -= 1; + gas_split + 1 + } else { + gas_split + }; + + let balance_burnt = if balance_res > 0 { + balance_res -= 1; + balance_split + 1 + } else { + balance_split + }; + let new_chunk_extra = ChunkExtra::new( &result.new_root, outcome_root, @@ -5052,6 +5161,20 @@ impl<'a> ChainUpdate<'a> { sum_gas_used += gas_burnt; sum_balance_burnt += balance_burnt; + if let Some(manager) = self.runtime_adapter.get_flat_storage_manager() { + // TODO(#9430): Support manager.save_flat_state_changes and manager.update_flat_storage_for_shard + // functions to be a part of the same chain_store_update + let store_update = manager.save_flat_state_changes( + *block_hash, + *prev_hash, + block.header().height(), + result.shard_uid, + result.trie_changes.state_changes(), + )?; + manager.update_flat_storage_for_shard(*shard_uid, block)?; + self.chain_store_update.merge(store_update); + } + self.chain_store_update.save_chunk_extra( block_hash, &result.shard_uid, @@ -5063,6 +5186,7 @@ impl<'a> ChainUpdate<'a> { assert_eq!(sum_balance_burnt, total_balance_burnt); } ApplySplitStateResultOrStateChanges::StateChangesForSplitStates(state_changes) => { + tracing::debug!(target: "resharding", height, ?shard_uid, "process_split_state store"); self.chain_store_update.add_state_changes_for_split_states( *block_hash, shard_uid.shard_id(), @@ -5073,50 +5197,15 @@ impl<'a> ChainUpdate<'a> { Ok(()) } - fn save_flat_state_changes( - &mut self, - block_hash: CryptoHash, - prev_hash: CryptoHash, - height: BlockHeight, - shard_uid: ShardUId, - trie_changes: &WrappedTrieChanges, - ) -> Result<(), Error> { - let delta = FlatStateDelta { - changes: FlatStateChanges::from_state_changes(&trie_changes.state_changes()), - metadata: FlatStateDeltaMetadata { - block: near_store::flat::BlockInfo { hash: block_hash, height, prev_hash }, - }, - }; - - if let Some(chain_flat_storage) = self - .runtime_adapter - .get_flat_storage_manager() - .and_then(|manager| manager.get_flat_storage_for_shard(shard_uid)) - { - // If flat storage exists, we add a block to it. - let store_update = - chain_flat_storage.add_delta(delta).map_err(|e| StorageError::from(e))?; - self.chain_store_update.merge(store_update); - } else { - let shard_id = shard_uid.shard_id(); - // Otherwise, save delta to disk so it will be used for flat storage creation later. - debug!(target: "chain", %shard_id, "Add delta for flat storage creation"); - let mut store_update = self.chain_store_update.store().store_update(); - store_helper::set_delta(&mut store_update, shard_uid, &delta); - self.chain_store_update.merge(store_update); - } - - Ok(()) - } - /// Processed results of applying chunk fn process_apply_chunk_result( &mut self, + block: &Block, result: ApplyChunkResult, - block_hash: CryptoHash, - height: BlockHeight, - prev_block_hash: CryptoHash, ) -> Result<(), Error> { + let block_hash = block.hash(); + let prev_hash = block.header().prev_hash(); + let height = block.header().height(); match result { ApplyChunkResult::SameHeight(SameHeightResult { gas_limit, @@ -5130,7 +5219,7 @@ impl<'a> ChainUpdate<'a> { // Save state root after applying transactions. self.chain_store_update.save_chunk_extra( - &block_hash, + block_hash, &shard_uid, ChunkExtra::new( &apply_result.new_root, @@ -5141,33 +5230,31 @@ impl<'a> ChainUpdate<'a> { apply_result.total_balance_burnt, ), ); - self.save_flat_state_changes( - block_hash, - prev_block_hash, - height, - shard_uid, - &apply_result.trie_changes, - )?; + if let Some(manager) = self.runtime_adapter.get_flat_storage_manager() { + let store_update = manager.save_flat_state_changes( + *block_hash, + *prev_hash, + height, + shard_uid, + apply_result.trie_changes.state_changes(), + )?; + self.chain_store_update.merge(store_update); + } self.chain_store_update.save_trie_changes(apply_result.trie_changes); self.chain_store_update.save_outgoing_receipt( - &block_hash, + block_hash, shard_id, apply_result.outgoing_receipts, ); // Save receipt and transaction results. self.chain_store_update.save_outcomes_with_proofs( - &block_hash, + block_hash, shard_id, apply_result.outcomes, outcome_paths, ); if let Some(apply_results_or_state_changes) = apply_split_result_or_state_changes { - self.process_split_state( - &block_hash, - &prev_block_hash, - &shard_uid, - apply_results_or_state_changes, - )?; + self.process_split_state(block, &shard_uid, apply_results_or_state_changes)?; } } ApplyChunkResult::DifferentHeight(DifferentHeightResult { @@ -5175,37 +5262,33 @@ impl<'a> ChainUpdate<'a> { apply_result, apply_split_result_or_state_changes, }) => { - let old_extra = - self.chain_store_update.get_chunk_extra(&prev_block_hash, &shard_uid)?; + let old_extra = self.chain_store_update.get_chunk_extra(prev_hash, &shard_uid)?; let mut new_extra = ChunkExtra::clone(&old_extra); *new_extra.state_root_mut() = apply_result.new_root; - self.save_flat_state_changes( - block_hash, - prev_block_hash, - height, - shard_uid, - &apply_result.trie_changes, - )?; - self.chain_store_update.save_chunk_extra(&block_hash, &shard_uid, new_extra); + if let Some(manager) = self.runtime_adapter.get_flat_storage_manager() { + let store_update = manager.save_flat_state_changes( + *block_hash, + *prev_hash, + height, + shard_uid, + apply_result.trie_changes.state_changes(), + )?; + self.chain_store_update.merge(store_update); + } + self.chain_store_update.save_chunk_extra(block_hash, &shard_uid, new_extra); self.chain_store_update.save_trie_changes(apply_result.trie_changes); if let Some(apply_results_or_state_changes) = apply_split_result_or_state_changes { - self.process_split_state( - &block_hash, - &prev_block_hash, - &shard_uid, - apply_results_or_state_changes, - )?; + self.process_split_state(block, &shard_uid, apply_results_or_state_changes)?; } } ApplyChunkResult::SplitState(SplitStateResult { shard_uid, results }) => { self.chain_store_update - .remove_state_changes_for_split_states(block_hash, shard_uid.shard_id()); + .remove_state_changes_for_split_states(*block.hash(), shard_uid.shard_id()); self.process_split_state( - &block_hash, - &prev_block_hash, + block, &shard_uid, ApplySplitStateResultOrStateChanges::ApplySplitStateResults(results), )?; @@ -5224,14 +5307,13 @@ impl<'a> ChainUpdate<'a> { apply_chunks_results: Vec>, ) -> Result, Error> { let prev_hash = block.header().prev_hash(); - let prev_block = self.chain_store_update.get_block(prev_hash)?; let results = apply_chunks_results.into_iter().map(|x| { if let Err(err) = &x { warn!(target:"chain", hash = %block.hash(), error = %err, "Error in applying chunks for block"); } x }).collect::, Error>>()?; - self.apply_chunk_postprocessing(block, &prev_block, results)?; + self.apply_chunk_postprocessing(block, results)?; let BlockPreprocessInfo { is_caught_up, @@ -5279,6 +5361,13 @@ impl<'a> ChainUpdate<'a> { .add_validator_proposals(BlockHeaderInfo::new(block.header(), last_finalized_height))?; self.chain_store_update.merge(epoch_manager_update); + #[cfg(feature = "new_epoch_sync")] + { + // BlockInfo should be already recorded in epoch_manager cache + let block_info = self.epoch_manager.get_block_info(block.hash())?; + self.save_epoch_sync_info(block.header().epoch_id(), block.header(), &block_info)?; + } + // Add validated block to the db, even if it's not the canonical fork. self.chain_store_update.save_block(block.clone()); self.chain_store_update.inc_block_refcount(prev_hash)?; @@ -5548,13 +5637,17 @@ impl<'a> ChainUpdate<'a> { self.chain_store_update.save_chunk(chunk); let shard_uid = self.epoch_manager.shard_id_to_uid(shard_id, block_header.epoch_id())?; - self.save_flat_state_changes( - *block_header.hash(), - *chunk_header.prev_block_hash(), - chunk_header.height_included(), - shard_uid, - &apply_result.trie_changes, - )?; + if let Some(manager) = self.runtime_adapter.get_flat_storage_manager() { + let store_update = manager.save_flat_state_changes( + *block_header.hash(), + *chunk_header.prev_block_hash(), + chunk_header.height_included(), + shard_uid, + apply_result.trie_changes.state_changes(), + )?; + self.chain_store_update.merge(store_update); + } + self.chain_store_update.save_trie_changes(apply_result.trie_changes); let chunk_extra = ChunkExtra::new( &apply_result.new_root, @@ -5633,13 +5726,16 @@ impl<'a> ChainUpdate<'a> { Default::default(), true, )?; - self.save_flat_state_changes( - *block_header.hash(), - *prev_block_header.hash(), - height, - shard_uid, - &apply_result.trie_changes, - )?; + if let Some(manager) = self.runtime_adapter.get_flat_storage_manager() { + let store_update = manager.save_flat_state_changes( + *block_header.hash(), + *prev_block_header.hash(), + height, + shard_uid, + apply_result.trie_changes.state_changes(), + )?; + self.chain_store_update.merge(store_update); + } self.chain_store_update.save_trie_changes(apply_result.trie_changes); let mut new_chunk_extra = ChunkExtra::clone(&chunk_extra); @@ -5648,6 +5744,67 @@ impl<'a> ChainUpdate<'a> { self.chain_store_update.save_chunk_extra(block_header.hash(), &shard_uid, new_chunk_extra); Ok(true) } + + /// If the block is the last one in the epoch + /// construct and record `EpochSyncInfo` to `self.chain_store_update`. + #[cfg(feature = "new_epoch_sync")] + fn save_epoch_sync_info( + &mut self, + epoch_id: &EpochId, + last_block_header: &BlockHeader, + last_block_info: &BlockInfo, + ) -> Result<(), Error> { + if self.epoch_manager.is_next_block_epoch_start(last_block_header.hash())? { + let mut store_update = self.chain_store_update.store().store_update(); + store_update + .set_ser( + DBCol::EpochSyncInfo, + epoch_id.as_ref(), + &self.create_epoch_sync_info(last_block_header, last_block_info)?, + ) + .map_err(EpochError::from)?; + self.chain_store_update.merge(store_update); + } + Ok(()) + } + + /// Create a pair of `BlockHeader`s necessary to create `BlockInfo` for `block_hash` + #[cfg(feature = "new_epoch_sync")] + fn get_header_pair(&self, block_hash: &CryptoHash) -> Result { + let header = self.chain_store_update.get_block_header(block_hash)?; + // `block_hash` can correspond to genesis block, for which there is no last final block recorded, + // because `last_final_block` for genesis is `CryptoHash::default()` + // Here we return just the same genesis block header as last known block header + // TODO(posvyatokum) process this case carefully in epoch sync validation + // TODO(posvyatokum) process this carefully in saving the parts of epoch sync data + let last_finalised_header = { + if *header.last_final_block() == CryptoHash::default() { + header.clone() + } else { + self.chain_store_update.get_block_header(header.last_final_block())? + } + }; + Ok(BlockHeaderPair { header, last_finalised_header }) + } + + /// Data that is necessary to prove Epoch in new Epoch Sync. + #[cfg(feature = "new_epoch_sync")] + fn create_epoch_sync_info( + &self, + last_block_header: &BlockHeader, + last_block_info: &BlockInfo, + ) -> Result { + let last = self.get_header_pair(last_block_header.hash())?; + let prev_last = self.get_header_pair(last_block_header.prev_hash())?; + let first = self.get_header_pair(last_block_info.epoch_first_block())?; + let epoch_info = self.epoch_manager.get_epoch_info(last_block_info.epoch_id())?; + Ok(EpochSyncInfo { + last, + prev_last, + first, + block_producers: epoch_info.validators_iter().collect(), + }) + } } pub fn do_apply_chunks( @@ -5693,7 +5850,22 @@ pub struct ApplyStatePartsRequest { pub sync_hash: CryptoHash, } -#[derive(actix::Message)] +// Skip `runtime_adapter`, because it's a complex object that has complex logic +// and many fields. +impl Debug for ApplyStatePartsRequest { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ApplyStatePartsRequest") + .field("runtime_adapter", &"") + .field("shard_uid", &self.shard_uid) + .field("state_root", &self.state_root) + .field("num_parts", &self.num_parts) + .field("epoch_id", &self.epoch_id) + .field("sync_hash", &self.sync_hash) + .finish() + } +} + +#[derive(actix::Message, Debug)] #[rtype(result = "()")] pub struct ApplyStatePartsResponse { pub apply_result: Result<(), near_chain_primitives::error::Error>, @@ -5710,7 +5882,19 @@ pub struct BlockCatchUpRequest { pub work: Vec Result + Send>>, } -#[derive(actix::Message)] +// Skip `work`, because displaying functions is not possible. +impl Debug for BlockCatchUpRequest { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BlockCatchUpRequest") + .field("sync_hash", &self.sync_hash) + .field("block_hash", &self.block_hash) + .field("block_height", &self.block_height) + .field("work", &format!("", self.work.len())) + .finish() + } +} + +#[derive(actix::Message, Debug)] #[rtype(result = "()")] pub struct BlockCatchUpResponse { pub sync_hash: CryptoHash, diff --git a/chain/chain/src/doomslug.rs b/chain/chain/src/doomslug.rs index 0327ff354d4..55b2a549f63 100644 --- a/chain/chain/src/doomslug.rs +++ b/chain/chain/src/doomslug.rs @@ -2,6 +2,8 @@ use std::collections::{HashMap, VecDeque}; use std::sync::Arc; use std::time::{Duration, Instant}; +use crate::doomslug::trackable::TrackableBlockHeightValue; +use crate::metrics; use near_client_primitives::debug::{ApprovalAtHeightStatus, ApprovalHistoryEntry}; use near_crypto::Signature; use near_primitives::block::{Approval, ApprovalInner}; @@ -72,6 +74,30 @@ struct DoomslugApprovalsTracker { threshold_mode: DoomslugThresholdMode, } +mod trackable { + use near_o11y::metrics::IntGauge; + use near_primitives::types::BlockHeight; + use once_cell::sync::Lazy; + + pub struct TrackableBlockHeightValue(BlockHeight, &'static Lazy); + + impl TrackableBlockHeightValue { + pub fn new(value: BlockHeight, gauge: &'static Lazy) -> Self { + gauge.set(value as i64); + Self(value, gauge) + } + + pub fn get(&self) -> BlockHeight { + self.0 + } + + pub fn set(&mut self, new: BlockHeight) { + self.0 = new; + self.1.set(self.0 as i64); + } + } +} + /// Approvals can arrive before the corresponding blocks, and we need a meaningful way to keep as /// many approvals as possible that can be useful in the future, while not allowing an adversary /// to spam us with invalid approvals. @@ -97,13 +123,13 @@ struct DoomslugApprovalsTrackersAtHeight { pub struct Doomslug { approval_tracking: HashMap, /// Largest target height for which we issued an approval - largest_target_height: BlockHeight, + largest_target_height: TrackableBlockHeightValue, /// Largest height for which we saw a block containing 1/2 endorsements in it - largest_final_height: BlockHeight, + largest_final_height: TrackableBlockHeightValue, /// Largest height for which we saw threshold approvals (and thus can potentially create a block) - largest_threshold_height: BlockHeight, + largest_threshold_height: TrackableBlockHeightValue, /// Largest target height of approvals that we've received - largest_approval_height: BlockHeight, + largest_approval_height: TrackableBlockHeightValue, /// Information Doomslug tracks about the chain tip tip: DoomslugTip, /// Whether an endorsement (or in general an approval) was sent since updating the tip @@ -339,10 +365,19 @@ impl Doomslug { ) -> Self { Doomslug { approval_tracking: HashMap::new(), - largest_target_height, - largest_approval_height: 0, - largest_final_height: 0, - largest_threshold_height: 0, + largest_target_height: TrackableBlockHeightValue::new( + largest_target_height, + &metrics::LARGEST_TARGET_HEIGHT, + ), + largest_approval_height: TrackableBlockHeightValue::new( + 0, + &metrics::LARGEST_APPROVAL_HEIGHT, + ), + largest_final_height: TrackableBlockHeightValue::new(0, &metrics::LARGEST_FINAL_HEIGHT), + largest_threshold_height: TrackableBlockHeightValue::new( + 0, + &metrics::LARGEST_THRESHOLD_HEIGHT, + ), tip: DoomslugTip { block_hash: CryptoHash::default(), height: 0 }, endorsement_pending: false, timer: DoomslugTimer { @@ -374,20 +409,20 @@ impl Doomslug { /// produce a block (in practice a blocks might not be produceable yet if not enough time /// passed since it accumulated enough approvals) pub fn get_largest_height_crossing_threshold(&self) -> BlockHeight { - self.largest_threshold_height + self.largest_threshold_height.get() } /// Returns the largest height for which we've received an approval pub fn get_largest_approval_height(&self) -> BlockHeight { - self.largest_approval_height + self.largest_approval_height.get() } pub fn get_largest_final_height(&self) -> BlockHeight { - self.largest_final_height + self.largest_final_height.get() } pub fn get_largest_target_height(&self) -> BlockHeight { - self.largest_target_height + self.largest_target_height.get() } pub fn get_timer_height(&self) -> BlockHeight { @@ -430,8 +465,9 @@ impl Doomslug { pub fn process_timer(&mut self, cur_time: Instant) -> Vec { let mut ret = vec![]; for _ in 0..MAX_TIMER_ITERS { - let skip_delay = - self.timer.get_delay(self.timer.height.saturating_sub(self.largest_final_height)); + let skip_delay = self + .timer + .get_delay(self.timer.height.saturating_sub(self.largest_final_height.get())); // The `endorsement_delay` is time to send approval to the block producer at `timer.height`, // while the `skip_delay` is the time before sending the approval to BP of `timer_height + 1`, @@ -443,8 +479,8 @@ impl Doomslug { if self.endorsement_pending && cur_time >= self.timer.last_endorsement_sent + self.timer.endorsement_delay { - if tip_height >= self.largest_target_height { - self.largest_target_height = tip_height + 1; + if tip_height >= self.largest_target_height.get() { + self.largest_target_height.set(tip_height + 1); if let Some(approval) = self.create_approval(tip_height + 1) { ret.push(approval); @@ -469,8 +505,8 @@ impl Doomslug { if cur_time >= self.timer.started + skip_delay { debug_assert!(!self.endorsement_pending); - self.largest_target_height = - std::cmp::max(self.timer.height + 1, self.largest_target_height); + self.largest_target_height + .set(std::cmp::max(self.timer.height + 1, self.largest_target_height.get())); if let Some(approval) = self.create_approval(self.timer.height + 1) { ret.push(approval); @@ -511,7 +547,7 @@ impl Doomslug { /// * `stakes` - the vector of validator stakes in the current epoch pub fn can_approved_block_be_produced( mode: DoomslugThresholdMode, - approvals: &[Option], + approvals: &[Option>], stakes: &[(Balance, Balance, bool)], ) -> bool { if mode == DoomslugThresholdMode::NoApprovals { @@ -575,7 +611,7 @@ impl Doomslug { debug_assert!(height > self.tip.height || self.tip.height == 0); self.tip = DoomslugTip { block_hash, height }; - self.largest_final_height = last_final_height; + self.largest_final_height.set(last_final_height); self.timer.height = height + 1; self.timer.started = now; @@ -604,13 +640,13 @@ impl Doomslug { .or_insert_with(|| DoomslugApprovalsTrackersAtHeight::new()) .process_approval(now, approval, stakes, threshold_mode); - if approval.target_height > self.largest_approval_height { - self.largest_approval_height = approval.target_height; + if approval.target_height > self.largest_approval_height.get() { + self.largest_approval_height.set(approval.target_height); } if ret != DoomslugBlockProductionReadiness::NotReady { - if approval.target_height > self.largest_threshold_height { - self.largest_threshold_height = approval.target_height; + if approval.target_height > self.largest_threshold_height.get() { + self.largest_threshold_height.set(approval.target_height); } } @@ -681,7 +717,7 @@ impl Doomslug { true } else { let delay = self.timer.get_delay( - self.timer.height.saturating_sub(self.largest_final_height), + self.timer.height.saturating_sub(self.largest_final_height.get()), ) / 6; let ready = now > when + delay; diff --git a/chain/chain/src/flat_storage_creator.rs b/chain/chain/src/flat_storage_creator.rs index e85616c6962..9e968f3c605 100644 --- a/chain/chain/src/flat_storage_creator.rs +++ b/chain/chain/src/flat_storage_creator.rs @@ -110,7 +110,7 @@ impl FlatStorageShardCreator { trie_iter.visit_nodes_interval(&path_begin, &path_end).unwrap() { if let Some(key) = key { - let value = trie.storage.retrieve_raw_bytes(&hash).unwrap(); + let value = trie.retrieve_value(&hash).unwrap(); store_helper::set_flat_state_value( &mut store_update, shard_uid, @@ -446,7 +446,15 @@ impl FlatStorageCreator { return Ok(None); }; for shard_id in 0..num_shards { - if shard_tracker.care_about_shard(me, &chain_head.prev_block_hash, shard_id, true) { + // The node applies transactions from the shards it cares about this and the next epoch. + if shard_tracker.care_about_shard(me, &chain_head.prev_block_hash, shard_id, true) + || shard_tracker.will_care_about_shard( + me, + &chain_head.prev_block_hash, + shard_id, + true, + ) + { let shard_uid = epoch_manager.shard_id_to_uid(shard_id, &chain_head.epoch_id)?; let status = flat_storage_manager.get_flat_storage_status(shard_uid); diff --git a/chain/chain/src/metrics.rs b/chain/chain/src/metrics.rs index d6ddce5867b..10be472e5dd 100644 --- a/chain/chain/src/metrics.rs +++ b/chain/chain/src/metrics.rs @@ -117,3 +117,31 @@ pub(crate) static SCHEDULED_CATCHUP_BLOCK: Lazy = Lazy::new(|| { ) .unwrap() }); +pub(crate) static LARGEST_TARGET_HEIGHT: Lazy = Lazy::new(|| { + try_create_int_gauge( + "near_largest_target_height", + "The largest height for which we sent an approval (or skip)", + ) + .unwrap() +}); +pub(crate) static LARGEST_THRESHOLD_HEIGHT: Lazy = Lazy::new(|| { + try_create_int_gauge( + "near_largest_threshold_height", + "The largest height where we got enough approvals", + ) + .unwrap() +}); +pub(crate) static LARGEST_APPROVAL_HEIGHT: Lazy = Lazy::new(|| { + try_create_int_gauge( + "near_largest_approval_height", + "The largest height for which we've got at least one approval", + ) + .unwrap() +}); +pub(crate) static LARGEST_FINAL_HEIGHT: Lazy = Lazy::new(|| { + try_create_int_gauge( + "near_largest_final_height", + "Largest height for which we saw a block containing 1/2 endorsements in it", + ) + .unwrap() +}); diff --git a/chain/chain/src/resharding.rs b/chain/chain/src/resharding.rs index c2164f18e91..964bd2a7369 100644 --- a/chain/chain/src/resharding.rs +++ b/chain/chain/src/resharding.rs @@ -3,35 +3,58 @@ /// build_state_for_split_shards_preprocessing and build_state_for_split_shards_postprocessing are handled /// by the client_actor while the heavy resharding build_state_for_split_shards is done by SyncJobsActor /// so as to not affect client. -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; - +use crate::Chain; +use itertools::Itertools; use near_chain_primitives::error::Error; use near_primitives::hash::CryptoHash; use near_primitives::shard_layout::{account_id_to_shard_uid, ShardLayout}; +use near_primitives::state::FlatStateValue; use near_primitives::state_part::PartId; -use near_primitives::syncing::{get_num_state_parts, STATE_PART_MEMORY_LIMIT}; +use near_primitives::syncing::get_num_state_parts; use near_primitives::types::chunk_extra::ChunkExtra; use near_primitives::types::{AccountId, ShardId, StateRoot}; +use near_store::flat::{ + store_helper, BlockInfo, FlatStorageManager, FlatStorageReadyStatus, FlatStorageStatus, +}; use near_store::split_state::get_delayed_receipts; -use near_store::{ShardTries, ShardUId, Trie}; - +use near_store::{ShardTries, ShardUId, Store, Trie, TrieDBStorage, TrieStorage}; +use std::collections::{HashMap, HashSet}; +use std::fmt::{Debug, Formatter}; +use std::sync::Arc; use tracing::debug; -use crate::types::RuntimeAdapter; -use crate::Chain; +// This is the approx batch size of the trie key, value pair entries that are written to the child shard trie. +const RESHARDING_BATCH_MEMORY_LIMIT: bytesize::ByteSize = bytesize::ByteSize(300 * bytesize::MIB); +/// StateSplitRequest has all the information needed to start a resharding job. This message is sent +/// from ClientActor to SyncJobsActor. We do not want to stall the ClientActor with a long running +/// resharding job. The SyncJobsActor is helpful for handling such long running jobs. #[derive(actix::Message)] #[rtype(result = "()")] pub struct StateSplitRequest { - pub runtime_adapter: Arc, + pub tries: Arc, pub sync_hash: CryptoHash, pub shard_uid: ShardUId, pub state_root: StateRoot, pub next_epoch_shard_layout: ShardLayout, } -#[derive(actix::Message)] +// Skip `runtime_adapter`, because it's a complex object that has complex logic +// and many fields. +impl Debug for StateSplitRequest { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("StateSplitRequest") + .field("tries", &"") + .field("sync_hash", &self.sync_hash) + .field("shard_uid", &self.shard_uid) + .field("state_root", &self.state_root) + .field("next_epoch_shard_layout", &self.next_epoch_shard_layout) + .finish() + } +} + +// StateSplitResponse is the response sent from SyncJobsActor to ClientActor once resharding is completed. +#[derive(actix::Message, Debug)] #[rtype(result = "()")] pub struct StateSplitResponse { pub sync_hash: CryptoHash, @@ -39,24 +62,92 @@ pub struct StateSplitResponse { pub new_state_roots: Result, Error>, } +fn get_checked_account_id_to_shard_uid_fn( + shard_uid: ShardUId, + new_shards: Vec, + next_epoch_shard_layout: ShardLayout, +) -> impl Fn(&AccountId) -> ShardUId { + let split_shard_ids: HashSet<_> = new_shards.into_iter().collect(); + move |account_id: &AccountId| { + let new_shard_uid = account_id_to_shard_uid(account_id, &next_epoch_shard_layout); + // check that all accounts in the shard are mapped the shards that this shard will split + // to according to shard layout + assert!( + split_shard_ids.contains(&new_shard_uid), + "Inconsistent shard_layout specs. Account {:?} in shard {:?} and in shard {:?}, but the former is not parent shard for the latter", + account_id, + shard_uid, + new_shard_uid, + ); + new_shard_uid + } +} + +// Return iterate over flat storage to get key, value. Used later in the get_trie_update_batch function. +// TODO(#9436): This isn't completely correct. We need to get the flat storage iterator based off a +// particular block, specifically, the last block of the previous epoch. +fn get_flat_storage_iter<'a>( + store: &'a Store, + shard_uid: ShardUId, +) -> impl Iterator, Vec)> + 'a { + let trie_storage = TrieDBStorage::new(store.clone(), shard_uid); + store_helper::iter_flat_state_entries(shard_uid, &store, None, None).map( + move |entry| -> (Vec, Vec) { + let (key, value) = entry.unwrap(); + let value = match value { + FlatStateValue::Ref(ref_value) => { + trie_storage.retrieve_raw_bytes(&ref_value.hash).unwrap().to_vec() + } + FlatStateValue::Inlined(inline_value) => inline_value, + }; + (key, value) + }, + ) +} + +// Format of the trie key, value pair that is used in tries.add_values_to_split_states() function +type TrieEntry = (Vec, Option>); + +// Function to return batches of trie key, value pairs from flat storage iter. We return None at the end of iter. +// The batch size is roughly RESHARDING_BATCH_MEMORY_LIMIT (300 MB) +// TODO(#9434) Add metrics for resharding progress +fn get_trie_update_batch( + iter: &mut impl Iterator, Vec)>, +) -> Option> { + let mut size: u64 = 0; + let mut entries = Vec::new(); + while let Some((key, value)) = iter.next() { + size += key.len() as u64 + value.len() as u64; + entries.push((key, Some(value))); + if size > RESHARDING_BATCH_MEMORY_LIMIT.as_u64() { + break; + } + } + if entries.is_empty() { + None + } else { + Some(entries) + } +} + fn apply_delayed_receipts<'a>( tries: &ShardTries, orig_shard_uid: ShardUId, orig_state_root: StateRoot, state_roots: HashMap, - account_id_to_shard_id: &(dyn Fn(&AccountId) -> ShardUId + 'a), + account_id_to_shard_uid: &(dyn Fn(&AccountId) -> ShardUId + 'a), ) -> Result, Error> { let orig_trie_update = tries.new_trie_update_view(orig_shard_uid, orig_state_root); let mut start_index = None; let mut new_state_roots = state_roots; while let Some((next_index, receipts)) = - get_delayed_receipts(&orig_trie_update, start_index, STATE_PART_MEMORY_LIMIT)? + get_delayed_receipts(&orig_trie_update, start_index, RESHARDING_BATCH_MEMORY_LIMIT)? { let (store_update, updated_state_roots) = tries.apply_delayed_receipts_to_split_states( &new_state_roots, &receipts, - account_id_to_shard_id, + account_id_to_shard_uid, )?; new_state_roots = updated_state_roots; start_index = Some(next_index); @@ -66,6 +157,25 @@ fn apply_delayed_receipts<'a>( Ok(new_state_roots) } +// function to set up flat storage status to Ready after a resharding event +// TODO(resharding) : Consolidate this with setting up flat storage during state sync logic +fn set_flat_storage_state( + store: Store, + flat_storage_manager: &FlatStorageManager, + shard_uid: ShardUId, + block_info: BlockInfo, +) -> Result<(), Error> { + let mut store_update = store.store_update(); + store_helper::set_flat_storage_status( + &mut store_update, + shard_uid, + FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: block_info }), + ); + store_update.commit()?; + flat_storage_manager.create_flat_storage_for_shard(shard_uid)?; + Ok(()) +} + impl Chain { pub fn build_state_for_split_shards_preprocessing( &self, @@ -73,19 +183,18 @@ impl Chain { shard_id: ShardId, state_split_scheduler: &dyn Fn(StateSplitRequest), ) -> Result<(), Error> { - let (epoch_id, next_epoch_id) = { - let block_header = self.get_block_header(sync_hash)?; - (block_header.epoch_id().clone(), block_header.next_epoch_id().clone()) - }; - let shard_layout = self.epoch_manager.get_shard_layout(&epoch_id)?; - let next_epoch_shard_layout = self.epoch_manager.get_shard_layout(&next_epoch_id)?; + let block_header = self.get_block_header(sync_hash)?; + let shard_layout = self.epoch_manager.get_shard_layout(block_header.epoch_id())?; + let next_epoch_shard_layout = + self.epoch_manager.get_shard_layout(block_header.next_epoch_id())?; + assert_ne!(shard_layout, next_epoch_shard_layout); + let shard_uid = ShardUId::from_shard_id_and_layout(shard_id, &shard_layout); - let prev_hash = *self.get_block_header(sync_hash)?.prev_hash(); + let prev_hash = block_header.prev_hash(); let state_root = *self.get_chunk_extra(&prev_hash, &shard_uid)?.state_root(); - assert_ne!(shard_layout, next_epoch_shard_layout); state_split_scheduler(StateSplitRequest { - runtime_adapter: self.runtime_adapter.clone(), + tries: Arc::new(self.runtime_adapter.get_tries()), sync_hash: *sync_hash, shard_uid, state_root, @@ -104,18 +213,12 @@ impl Chain { StateSplitResponse { shard_id, sync_hash, new_state_roots } } + // TODO(#9446) remove function when shifting to flat storage iteration for resharding fn build_state_for_split_shards_impl( state_split_request: StateSplitRequest, ) -> Result, Error> { - let StateSplitRequest { - runtime_adapter, - shard_uid, - state_root, - next_epoch_shard_layout, - .. - } = state_split_request; - // TODO(resharding) use flat storage to split the trie here - let tries = runtime_adapter.get_tries(); + let StateSplitRequest { tries, shard_uid, state_root, next_epoch_shard_layout, .. } = + state_split_request; let trie = tries.get_view_trie_for_shard(shard_uid, state_root); let shard_id = shard_uid.shard_id(); let new_shards = next_epoch_shard_layout @@ -123,30 +226,18 @@ impl Chain { .ok_or(Error::InvalidShardId(shard_id))?; let mut state_roots: HashMap<_, _> = new_shards.iter().map(|shard_uid| (*shard_uid, Trie::EMPTY_ROOT)).collect(); - let split_shard_ids: HashSet<_> = new_shards.into_iter().collect(); - let checked_account_id_to_shard_id = |account_id: &AccountId| { - let new_shard_uid = account_id_to_shard_uid(account_id, &next_epoch_shard_layout); - // check that all accounts in the shard are mapped the shards that this shard will split - // to according to shard layout - assert!( - split_shard_ids.contains(&new_shard_uid), - "Inconsistent shard_layout specs. Account {:?} in shard {:?} and in shard {:?}, but the former is not parent shard for the latter", - account_id, - shard_uid, - new_shard_uid, - ); - new_shard_uid - }; + let checked_account_id_to_shard_uid = + get_checked_account_id_to_shard_uid_fn(shard_uid, new_shards, next_epoch_shard_layout); let state_root_node = trie.retrieve_root_node()?; let num_parts = get_num_state_parts(state_root_node.memory_usage); - debug!(target: "runtime", "splitting state for shard {} to {} parts to build new states", shard_id, num_parts); + debug!(target: "resharding", "splitting state for shard {} to {} parts to build new states", shard_id, num_parts); for part_id in 0..num_parts { let trie_items = trie.get_trie_items_for_part(PartId::new(part_id, num_parts))?; let (store_update, new_state_roots) = tries.add_values_to_split_states( &state_roots, trie_items.into_iter().map(|(key, value)| (key, Some(value))).collect(), - &checked_account_id_to_shard_id, + &checked_account_id_to_shard_uid, )?; state_roots = new_state_roots; store_update.commit()?; @@ -156,39 +247,118 @@ impl Chain { shard_uid, state_root, state_roots, - &checked_account_id_to_shard_id, + &checked_account_id_to_shard_uid, )?; + + Ok(state_roots) + } + + // TODO(#9446) After implementing iterator at specific head, shift to build_state_for_split_shards_impl_v2 + #[allow(dead_code)] + fn build_state_for_split_shards_impl_v2( + state_split_request: StateSplitRequest, + ) -> Result, Error> { + let StateSplitRequest { tries, shard_uid, state_root, next_epoch_shard_layout, .. } = + state_split_request; + let store = tries.get_store(); + + let shard_id = shard_uid.shard_id(); + let new_shards = next_epoch_shard_layout + .get_split_shard_uids(shard_id) + .ok_or(Error::InvalidShardId(shard_id))?; + let mut state_roots: HashMap<_, _> = + new_shards.iter().map(|shard_uid| (*shard_uid, Trie::EMPTY_ROOT)).collect(); + + // function to map account id to shard uid in range of child shards + let checked_account_id_to_shard_uid = + get_checked_account_id_to_shard_uid_fn(shard_uid, new_shards, next_epoch_shard_layout); + + let mut iter = get_flat_storage_iter(&store, shard_uid); + while let Some(batch) = get_trie_update_batch(&mut iter) { + // TODO(#9435): This is highly inefficient as for each key in the batch, we are parsing the account_id + // A better way would be to use the boundary account to construct the from and to key range for flat storage iterator + let (store_update, new_state_roots) = tries.add_values_to_split_states( + &state_roots, + batch, + &checked_account_id_to_shard_uid, + )?; + state_roots = new_state_roots; + store_update.commit()?; + } + + state_roots = apply_delayed_receipts( + &tries, + shard_uid, + state_root, + state_roots, + &checked_account_id_to_shard_uid, + )?; + Ok(state_roots) } pub fn build_state_for_split_shards_postprocessing( &mut self, sync_hash: &CryptoHash, - state_roots: Result, Error>, + state_roots: HashMap, ) -> Result<(), Error> { - let prev_hash = *self.get_block_header(sync_hash)?.prev_hash(); + let block_header = self.get_block_header(sync_hash)?; + let prev_hash = block_header.prev_hash(); + + let child_shard_uids = state_roots.keys().collect_vec(); + self.initialize_flat_storage(&prev_hash, &child_shard_uids)?; + let mut chain_store_update = self.mut_store().store_update(); - for (shard_uid, state_root) in state_roots? { + for (shard_uid, state_root) in state_roots { // here we store the state roots in chunk_extra in the database for later use let chunk_extra = ChunkExtra::new_with_only_state_root(&state_root); chain_store_update.save_chunk_extra(&prev_hash, &shard_uid, chunk_extra); - debug!(target:"chain", "Finish building split state for shard {:?} {:?} {:?} ", shard_uid, prev_hash, state_root); + debug!(target:"resharding", "Finish building split state for shard {:?} {:?} {:?} ", shard_uid, prev_hash, state_root); } - chain_store_update.commit() + chain_store_update.commit()?; + + Ok(()) + } + + // Here we iterate over all the child shards and initialize flat storage for them by calling set_flat_storage_state + // Note that this function is called on the current_block which is the first block the next epoch. + // We set the flat_head as the prev_block as after resharding, the state written to flat storage corresponds to the + // state as of prev_block, and that's the convention that we follow. + fn initialize_flat_storage( + &self, + prev_hash: &CryptoHash, + child_shard_uids: &[&ShardUId], + ) -> Result<(), Error> { + let prev_block_header = self.get_block_header(prev_hash)?; + let prev_block_info = BlockInfo { + hash: *prev_block_header.hash(), + prev_hash: *prev_block_header.prev_hash(), + height: prev_block_header.height(), + }; + + // create flat storage for child shards + if let Some(flat_storage_manager) = self.runtime_adapter.get_flat_storage_manager() { + for shard_uid in child_shard_uids { + let store = self.runtime_adapter.store().clone(); + set_flat_storage_state(store, &flat_storage_manager, **shard_uid, prev_block_info)?; + } + } + Ok(()) } } #[cfg(test)] mod tests { + use super::StateSplitRequest; + use crate::Chain; use near_primitives::account::Account; - use near_primitives::hash::{hash, CryptoHash}; + use near_primitives::hash::CryptoHash; use near_primitives::receipt::{DelayedReceiptIndices, Receipt}; - use near_primitives::state_part::PartId; + use near_primitives::shard_layout::{account_id_to_shard_uid, ShardLayout}; use near_primitives::trie_key::trie_key_parsers::parse_account_id_from_raw_key; use near_primitives::trie_key::TrieKey; - use near_primitives::types::{ - AccountId, NumShards, StateChangeCause, StateChangesForSplitStates, StateRoot, - }; + use near_primitives::types::{StateChangeCause, StateChangesForSplitStates, StateRoot}; + use near_store::flat::FlatStateChanges; use near_store::test_utils::{ create_tries, gen_receipts, gen_unique_accounts, get_all_delayed_receipts, }; @@ -198,6 +368,7 @@ mod tests { use rand::seq::SliceRandom; use rand::Rng; use std::collections::HashMap; + use std::sync::Arc; #[test] fn test_split_and_update_states() { @@ -209,10 +380,11 @@ mod tests { } fn test_split_and_update_state_impl(rng: &mut impl Rng) { - let tries = create_tries(); + let shard_uid = ShardUId::single_shard(); + let tries = Arc::new(create_tries()); // add accounts and receipts to state let mut account_ids = gen_unique_accounts(rng, 1, 100); - let mut trie_update = tries.new_trie_update(ShardUId::single_shard(), Trie::EMPTY_ROOT); + let mut trie_update = tries.new_trie_update(shard_uid, Trie::EMPTY_ROOT); for account_id in account_ids.iter() { set_account( &mut trie_update, @@ -235,61 +407,38 @@ mod tests { }, ); trie_update.commit(StateChangeCause::Resharding); - let (_, trie_changes, _) = trie_update.finalize().unwrap(); + let (_, trie_changes, state_changes) = trie_update.finalize().unwrap(); let mut store_update = tries.store_update(); - let state_root = - tries.apply_all(&trie_changes, ShardUId::single_shard(), &mut store_update); + let state_root = tries.apply_all(&trie_changes, shard_uid, &mut store_update); + let flat_state_changes = FlatStateChanges::from_state_changes(&state_changes); + flat_state_changes.apply_to_flat_state(&mut store_update, shard_uid); store_update.commit().unwrap(); state_root }; - let num_shards = 4; - let account_id_to_shard_id = &|account_id: &AccountId| ShardUId { - shard_id: (hash(account_id.as_ref().as_bytes()).0[0] as NumShards % num_shards) as u32, - version: 1, - }; - // add accounts and receipts to the split shards - let mut split_state_roots = { - let trie_items = tries - .get_view_trie_for_shard(ShardUId::single_shard(), state_root) - .get_trie_items_for_part(PartId::new(0, 1)) - .unwrap(); - let split_state_roots: HashMap<_, _> = (0..num_shards) - .map(|shard_id| { - (ShardUId { version: 1, shard_id: shard_id as u32 }, Trie::EMPTY_ROOT) - }) - .collect(); - let (store_update, split_state_roots) = tries - .add_values_to_split_states( - &split_state_roots, - trie_items.into_iter().map(|(key, value)| (key, Some(value))).collect(), - account_id_to_shard_id, - ) - .unwrap(); - store_update.commit().unwrap(); - let (store_update, split_state_roots) = tries - .apply_delayed_receipts_to_split_states( - &split_state_roots, - &get_all_delayed_receipts(&tries, &ShardUId::single_shard(), &state_root), - account_id_to_shard_id, - ) - .unwrap(); - store_update.commit().unwrap(); - split_state_roots - }; + let next_epoch_shard_layout = ShardLayout::v1_test(); + let response = Chain::build_state_for_split_shards(StateSplitRequest { + tries: tries.clone(), + sync_hash: state_root, + shard_uid, + state_root, + next_epoch_shard_layout: next_epoch_shard_layout.clone(), + }); + let mut split_state_roots = response.new_state_roots.unwrap(); + compare_state_and_split_states( &tries, &state_root, &split_state_roots, - account_id_to_shard_id, + &next_epoch_shard_layout, ); // update the original shard for _ in 0..10 { // add accounts let new_accounts = gen_unique_accounts(rng, 1, 10); - let mut trie_update = tries.new_trie_update(ShardUId::single_shard(), state_root); + let mut trie_update = tries.new_trie_update(shard_uid, state_root); for account_id in new_accounts.iter() { set_account( &mut trie_update, @@ -341,29 +490,29 @@ mod tests { trie_update.commit(StateChangeCause::Resharding); let (_, trie_changes, state_changes) = trie_update.finalize().unwrap(); let mut store_update = tries.store_update(); - let new_state_root = - tries.apply_all(&trie_changes, ShardUId::single_shard(), &mut store_update); + let new_state_root = tries.apply_all(&trie_changes, shard_uid, &mut store_update); store_update.commit().unwrap(); state_root = new_state_root; // update split states - let trie_changes = tries + let mut trie_updates = tries .apply_state_changes_to_split_states( &split_state_roots, StateChangesForSplitStates::from_raw_state_changes( &state_changes, removed_receipts, ), - account_id_to_shard_id, + &|account_id| account_id_to_shard_uid(account_id, &next_epoch_shard_layout), ) .unwrap(); - split_state_roots = trie_changes - .iter() - .map(|(shard_uid, trie_changes)| { + split_state_roots = trie_updates + .drain() + .map(|(shard_uid, trie_update)| { let mut state_update = tries.store_update(); - let state_root = tries.apply_all(trie_changes, *shard_uid, &mut state_update); + let (_, trie_changes, _) = trie_update.finalize().unwrap(); + let state_root = tries.apply_all(&trie_changes, shard_uid, &mut state_update); state_update.commit().unwrap(); - (*shard_uid, state_root) + (shard_uid, state_root) }) .collect(); @@ -371,7 +520,7 @@ mod tests { &tries, &state_root, &split_state_roots, - account_id_to_shard_id, + &next_epoch_shard_layout, ); } } @@ -380,27 +529,29 @@ mod tests { tries: &ShardTries, state_root: &StateRoot, state_roots: &HashMap, - account_id_to_shard_id: &dyn Fn(&AccountId) -> ShardUId, + next_epoch_shard_layout: &ShardLayout, ) { - // check that the 4 tries combined to the orig trie - let trie_items = - get_trie_nodes_except_delayed_receipts(tries, &ShardUId::single_shard(), state_root); - let trie_items_by_shard: HashMap<_, _> = state_roots + // Get trie items before resharding and split them by account shard + let trie_items_before_resharding = + get_trie_items_except_delayed_receipts(tries, &ShardUId::single_shard(), state_root); + + let trie_items_after_resharding: HashMap<_, _> = state_roots .iter() .map(|(&shard_uid, state_root)| { - (shard_uid, get_trie_nodes_except_delayed_receipts(tries, &shard_uid, state_root)) + (shard_uid, get_trie_items_except_delayed_receipts(tries, &shard_uid, state_root)) }) .collect(); let mut expected_trie_items_by_shard: HashMap<_, _> = state_roots.iter().map(|(&shard_uid, _)| (shard_uid, vec![])).collect(); - for item in trie_items { + for item in trie_items_before_resharding { let account_id = parse_account_id_from_raw_key(&item.0).unwrap().unwrap(); - let shard_uid: ShardUId = account_id_to_shard_id(&account_id); + let shard_uid: ShardUId = account_id_to_shard_uid(&account_id, next_epoch_shard_layout); expected_trie_items_by_shard.get_mut(&shard_uid).unwrap().push(item); } - assert_eq!(trie_items_by_shard, expected_trie_items_by_shard); + assert_eq!(expected_trie_items_by_shard, trie_items_after_resharding); + // check that the new tries combined to the orig trie for delayed receipts let receipts_from_split_states: HashMap<_, _> = state_roots .iter() .map(|(&shard_uid, state_root)| { @@ -412,13 +563,13 @@ mod tests { let mut expected_receipts_by_shard: HashMap<_, Vec<_>> = state_roots.iter().map(|(&shard_uid, _)| (shard_uid, vec![])).collect(); for receipt in get_all_delayed_receipts(tries, &ShardUId::single_shard(), state_root) { - let shard_uid = account_id_to_shard_id(&receipt.receiver_id); + let shard_uid = account_id_to_shard_uid(&receipt.receiver_id, next_epoch_shard_layout); expected_receipts_by_shard.get_mut(&shard_uid).unwrap().push(receipt.clone()); } assert_eq!(expected_receipts_by_shard, receipts_from_split_states); } - fn get_trie_nodes_except_delayed_receipts( + fn get_trie_items_except_delayed_receipts( tries: &ShardTries, shard_uid: &ShardUId, state_root: &StateRoot, diff --git a/chain/chain/src/state_snapshot_actor.rs b/chain/chain/src/state_snapshot_actor.rs index 6035b77c43b..424be8b2302 100644 --- a/chain/chain/src/state_snapshot_actor.rs +++ b/chain/chain/src/state_snapshot_actor.rs @@ -1,5 +1,6 @@ use actix::AsyncContext; use near_o11y::{handler_debug_span, OpenTelemetrySpanExt, WithSpanContext, WithSpanContextExt}; +use near_primitives::block::Block; use near_primitives::hash::CryptoHash; use near_primitives::shard_layout::ShardUId; use near_store::flat::FlatStorageManager; @@ -29,6 +30,8 @@ struct MakeSnapshotRequest { prev_block_hash: CryptoHash, /// Shards that need to be present in the snapshot. shard_uids: Vec, + /// Last block of the prev epoch. + block: Block, /// Whether to perform compaction. compaction_enabled: bool, } @@ -47,9 +50,10 @@ impl actix::Handler> for StateSnapshotActor _ctx: &mut actix::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "state_snapshot", msg); - let MakeSnapshotRequest { prev_block_hash, shard_uids, compaction_enabled } = msg; + tracing::debug!(target: "state_snapshot", ?msg); + let MakeSnapshotRequest { prev_block_hash, shard_uids, block, compaction_enabled } = msg; - let res = self.tries.make_state_snapshot(&prev_block_hash, &shard_uids); + let res = self.tries.make_state_snapshot(&prev_block_hash, &shard_uids, &block); if !self.flat_storage_manager.set_flat_state_updates_mode(true) { tracing::error!(target: "state_snapshot", ?prev_block_hash, ?shard_uids, "Failed to unlock flat state updates"); } @@ -77,7 +81,8 @@ impl actix::Handler> for StateSnapshotAc msg: WithSpanContext, _ctx: &mut actix::Context, ) -> Self::Result { - let (_span, _msg) = handler_debug_span!(target: "state_snapshot", msg); + let (_span, msg) = handler_debug_span!(target: "state_snapshot", msg); + tracing::debug!(target: "state_snapshot", ?msg); if let Err(err) = self.tries.compact_state_snapshot() { tracing::error!(target: "state_snapshot", ?err, "State snapshot compaction failed"); @@ -88,7 +93,7 @@ impl actix::Handler> for StateSnapshotAc } pub type MakeSnapshotCallback = - Arc) -> () + Send + Sync + 'static>; + Arc, Block) -> () + Send + Sync + 'static>; /// Sends a request to make a state snapshot. pub fn get_make_snapshot_callback( @@ -96,11 +101,15 @@ pub fn get_make_snapshot_callback( flat_storage_manager: FlatStorageManager, compaction_enabled: bool, ) -> MakeSnapshotCallback { - Arc::new(move |prev_block_hash, shard_uids| { - tracing::info!(target: "state_snapshot", ?prev_block_hash, ?shard_uids, "start_snapshot_callback sends `MakeSnapshotCallback` to state_snapshot_addr"); + Arc::new(move |prev_block_hash, shard_uids, block| { + tracing::info!( + target: "state_snapshot", + ?prev_block_hash, + ?shard_uids, + "start_snapshot_callback sends `MakeSnapshotCallback` to state_snapshot_addr"); if flat_storage_manager.set_flat_state_updates_mode(false) { state_snapshot_addr.do_send( - MakeSnapshotRequest { prev_block_hash, shard_uids, compaction_enabled } + MakeSnapshotRequest { prev_block_hash, shard_uids, block, compaction_enabled } .with_span_context(), ); } diff --git a/chain/chain/src/store.rs b/chain/chain/src/store.rs index 48ca877888f..123e4a4a78a 100644 --- a/chain/chain/src/store.rs +++ b/chain/chain/src/store.rs @@ -9,11 +9,16 @@ use near_cache::CellLruCache; use near_chain_primitives::error::Error; use near_epoch_manager::EpochManagerAdapter; use near_primitives::block::Tip; +#[cfg(feature = "protocol_feature_simple_nightshade_v2")] +use near_primitives::checked_feature; +#[cfg(feature = "new_epoch_sync")] +use near_primitives::epoch_manager::epoch_sync::EpochSyncInfo; use near_primitives::errors::InvalidTxError; use near_primitives::hash::CryptoHash; use near_primitives::merkle::{MerklePath, PartialMerkleTree}; use near_primitives::receipt::Receipt; -use near_primitives::shard_layout::{account_id_to_shard_id, get_block_shard_uid, ShardUId}; +use near_primitives::shard_layout::account_id_to_shard_id; +use near_primitives::shard_layout::{get_block_shard_uid, ShardLayout, ShardUId}; use near_primitives::sharding::{ ChunkHash, EncodedShardChunk, PartialEncodedChunk, ReceiptProof, ShardChunk, ShardChunkHeader, StateSyncInfo, @@ -37,6 +42,7 @@ use near_primitives::utils::{ get_block_shard_id, get_outcome_id_block_hash, get_outcome_id_block_hash_rev, index_to_bytes, to_timestamp, }; +use near_primitives::version::ProtocolVersion; use near_primitives::views::LightClientBlockView; use near_store::{ DBCol, KeyForStateChanges, ShardTries, Store, StoreUpdate, WrappedTrieChanges, CHUNK_TAIL_KEY, @@ -197,11 +203,13 @@ pub trait ChainStoreAccess { hash: &CryptoHash, shard_id: ShardId, ) -> Result>, Error>; + fn get_incoming_receipts( &self, hash: &CryptoHash, shard_id: ShardId, ) -> Result>, Error>; + /// Collect incoming receipts for shard `shard_id` from /// the block at height `last_chunk_height_included` (non-inclusive) to the block `block_hash` (inclusive) /// This is because the chunks for the shard are empty for the blocks in between, @@ -233,6 +241,11 @@ pub trait ChainStoreAccess { ret.push(ReceiptProofResponse(block_hash, Arc::new(vec![]))); } + // TODO(resharding) + // when crossing the epoch boundary we should check if the shard + // layout is different and handle that + // one idea would be to do shard_id := parent(shard_id) but remember to + // deduplicate the receipts as well block_hash = prev_hash; } @@ -480,35 +493,132 @@ impl ChainStore { loop { let block_header = self.get_block_header(&receipts_block_hash)?; - if block_header.height() == last_included_height { - let receipts_shard_layout = - epoch_manager.get_shard_layout(block_header.epoch_id())?; - - // get the shard from which the outgoing receipt were generated - let receipts_shard_id = if shard_layout != receipts_shard_layout { - shard_layout.get_parent_shard_id(shard_id)? - } else { - shard_id - }; - let mut receipts = self - .get_outgoing_receipts(&receipts_block_hash, receipts_shard_id) - .map(|v| v.to_vec()) - .unwrap_or_default(); - - // filter to receipts that belong to `shard_id` in the current shard layout - if shard_layout != receipts_shard_layout { - receipts.retain(|receipt| { - account_id_to_shard_id(&receipt.receiver_id, &shard_layout) == shard_id - }); - } + if block_header.height() != last_included_height { + receipts_block_hash = *block_header.prev_hash(); + continue; + } + let receipts_shard_layout = epoch_manager.get_shard_layout(block_header.epoch_id())?; - return Ok(receipts); + // get the shard from which the outgoing receipt were generated + let receipts_shard_id = if shard_layout != receipts_shard_layout { + shard_layout.get_parent_shard_id(shard_id)? } else { - receipts_block_hash = *block_header.prev_hash(); + shard_id + }; + + let mut receipts = self + .get_outgoing_receipts(&receipts_block_hash, receipts_shard_id) + .map(|v| v.to_vec()) + .unwrap_or_default(); + + if shard_layout != receipts_shard_layout { + // the shard layout has changed so we need to reassign the outgoing receipts + let epoch_id = epoch_manager.get_epoch_id_from_prev_block(&prev_block_hash)?; + let protocol_version = epoch_manager.get_epoch_protocol_version(&epoch_id)?; + Self::reassign_outgoing_receipts_for_resharding( + &mut receipts, + protocol_version, + &shard_layout, + shard_id, + receipts_shard_id, + )?; } + + return Ok(receipts); } } + fn reassign_outgoing_receipts_for_resharding( + receipts: &mut Vec, + protocol_version: ProtocolVersion, + shard_layout: &ShardLayout, + shard_id: u64, + receipts_shard_id: u64, + ) -> Result<(), Error> { + tracing::trace!(target: "resharding", ?protocol_version, shard_id, receipts_shard_id, "reassign_outgoing_receipts_for_resharding"); + // If simple nightshade v2 is enabled and stable use that. + #[cfg(feature = "protocol_feature_simple_nightshade_v2")] + if checked_feature!("stable", SimpleNightshadeV2, protocol_version) { + Self::reassign_outgoing_receipts_for_resharding_v2( + receipts, + shard_layout, + shard_id, + receipts_shard_id, + )?; + return Ok(()); + } + + // Otherwise use the old reassignment. Keep in mind it only works for + // 1 shard -> n shards reshardings, otherwise receipts get lost. + Self::reassign_outgoing_receipts_for_resharding_v1(receipts, shard_layout, shard_id)?; + Ok(()) + } + + /// Reassign the outgoing receipts from the parent shard to the children + /// shards. + /// + /// This method does it based on the "lowest child index" approach where it + /// assigns all the receipts from parent to the child shard with the lowest + /// index. It's meant to be used for the resharding from simple nightshade + /// with 4 shards to simple nightshade v2 with 5 shards and subsequent + /// reshardings. + /// + /// e.g. in the following resharding + /// 0->0', 1->1', 2->2', 3->3',4' + /// 0' will get all outgoing receipts from its parent 0 + /// 1' will get all outgoing receipts from its parent 1 + /// 2' will get all outgoing receipts from its parent 2 + /// 3' will get all outgoing receipts from its parent 3 + /// 4' will get no outgoing receipts from its parent 3 + /// All receipts are distributed to children, each exactly once. + #[cfg(feature = "protocol_feature_simple_nightshade_v2")] + fn reassign_outgoing_receipts_for_resharding_v2( + receipts: &mut Vec, + shard_layout: &ShardLayout, + shard_id: ShardId, + receipts_shard_id: ShardId, + ) -> Result<(), Error> { + let split_shard_ids = shard_layout.get_split_shard_ids(receipts_shard_id); + let split_shard_ids = + split_shard_ids.ok_or(Error::InvalidSplitShardsIds(shard_id, receipts_shard_id))?; + + // The target shard id is the split shard with the lowest shard id. + let target_shard_id = split_shard_ids.iter().min(); + let target_shard_id = + *target_shard_id.ok_or(Error::InvalidSplitShardsIds(shard_id, receipts_shard_id))?; + + if shard_id == target_shard_id { + // This shard_id is the lowest index child, it gets all the receipts. + Ok(()) + } else { + // This shard_id is not the lowest index child, it gets no receipts. + receipts.clear(); + Ok(()) + } + } + + /// Reassign the outgoing receipts from the parent shard to the children + /// shards. + /// + /// This method does it based on the "receipt receiver" approach where the + /// receipt is assigned to the shard of the receiver. + /// + /// This approach worked well for the 1->4 shards resharding but it doesn't + /// work for following reshardings. The reason is that it's only the child + /// shards that look at parents shard's outgoing receipts. If the receipt + /// receiver happens to not fall within one of the children shards then the + /// receipt is lost. + fn reassign_outgoing_receipts_for_resharding_v1( + receipts: &mut Vec, + shard_layout: &ShardLayout, + shard_id: ShardId, + ) -> Result<(), Error> { + receipts.retain(|receipt| { + account_id_to_shard_id(&receipt.receiver_id, &shard_layout) == shard_id + }); + Ok(()) + } + /// For a given transaction, it expires if the block that the chunk points to is more than `validity_period` /// ahead of the block that has `base_block_hash`. pub fn check_transaction_validity_period( @@ -721,6 +831,15 @@ impl ChainStore { store_update.commit().map_err(|err| err.into()) } + /// Save epoch sync info + #[cfg(feature = "new_epoch_sync")] + pub fn get_epoch_sync_info(&self, epoch_id: &EpochId) -> Result { + option_to_not_found( + self.store.get_ser(DBCol::EpochSyncInfo, epoch_id.as_ref()), + "EpochSyncInfo", + ) + } + /// Retrieve the kinds of state changes occurred in a given block. /// /// We store different types of data, so we prefer to only expose minimal information about the @@ -2605,9 +2724,10 @@ impl<'a> ChainStoreUpdate<'a> { | DBCol::FlatStateChanges | DBCol::FlatStateDeltaMetadata | DBCol::FlatStorageStatus - | DBCol::Misc => { - unreachable!(); - } + | DBCol::Misc + => unreachable!(), + #[cfg(feature = "new_epoch_sync")] + DBCol::EpochSyncInfo => unreachable!(), } self.merge(store_update); } @@ -3315,9 +3435,9 @@ mod tests { let transaction_validity_period = 5; let mut chain = get_chain(); let genesis = chain.get_block_by_height(0).unwrap(); + let genesis_hash = *genesis.hash(); let signer = Arc::new(create_test_signer("test1")); let mut short_fork = vec![]; - #[allow(clippy::redundant_clone)] let mut prev_block = genesis.clone(); for i in 1..(transaction_validity_period + 2) { let mut store_update = chain.mut_store().store_update(); @@ -3332,14 +3452,13 @@ mod tests { assert_eq!( chain.mut_store().check_transaction_validity_period( &short_fork_head, - genesis.hash(), + &genesis_hash, transaction_validity_period ), Err(InvalidTxError::Expired) ); let mut long_fork = vec![]; - #[allow(clippy::redundant_clone)] - let mut prev_block = genesis.clone(); + let mut prev_block = genesis; for i in 1..(transaction_validity_period * 5) { let mut store_update = chain.mut_store().store_update(); let block = TestBlockBuilder::new(&prev_block, signer.clone()).height(i).build(); @@ -3352,7 +3471,7 @@ mod tests { assert_eq!( chain.mut_store().check_transaction_validity_period( long_fork_head, - genesis.hash(), + &genesis_hash, transaction_validity_period ), Err(InvalidTxError::Expired) @@ -3563,14 +3682,13 @@ mod tests { let mut chain = get_chain_with_epoch_length(1); let genesis = chain.get_block_by_height(0).unwrap(); let signer = Arc::new(create_test_signer("test1")); - #[allow(clippy::redundant_clone)] - let mut prev_block = genesis.clone(); + let mut prev_block = genesis; let mut blocks = vec![prev_block.clone()]; { let mut store_update = chain.store().store().store_update(); let block_info = BlockInfo::default(); store_update - .insert_ser(DBCol::BlockInfo, genesis.hash().as_ref(), &block_info) + .insert_ser(DBCol::BlockInfo, prev_block.hash().as_ref(), &block_info) .unwrap(); store_update.commit().unwrap(); } diff --git a/chain/chain/src/test_utils/kv_runtime.rs b/chain/chain/src/test_utils/kv_runtime.rs index 9105e21ba47..9d199482c19 100644 --- a/chain/chain/src/test_utils/kv_runtime.rs +++ b/chain/chain/src/test_utils/kv_runtime.rs @@ -145,7 +145,6 @@ impl MockEpochManager { let map_with_default_hash3 = HashMap::from([(EpochId::default(), 0)]); let mut validators = HashMap::new(); - #[allow(unused_mut)] let mut validators_by_valset: Vec = vs .block_producers .iter() @@ -853,7 +852,7 @@ impl EpochManagerAdapter for MockEpochManager { _prev_block_hash: &CryptoHash, _prev_block_height: BlockHeight, _block_height: BlockHeight, - _approvals: &[Option], + _approvals: &[Option>], ) -> Result { Ok(true) } @@ -862,13 +861,13 @@ impl EpochManagerAdapter for MockEpochManager { &self, epoch_id: &EpochId, can_approved_block_be_produced: &dyn Fn( - &[Option], + &[Option>], &[(Balance, Balance, bool)], ) -> bool, prev_block_hash: &CryptoHash, prev_block_height: BlockHeight, block_height: BlockHeight, - approvals: &[Option], + approvals: &[Option>], ) -> Result<(), Error> { let validators = self.get_block_producers(self.get_valset_for_epoch(epoch_id)?); let message_to_sign = Approval::get_data_for_sig( diff --git a/chain/chain/src/tests/simple_chain.rs b/chain/chain/src/tests/simple_chain.rs index f864bfff441..a5e51a7dbaa 100644 --- a/chain/chain/src/tests/simple_chain.rs +++ b/chain/chain/src/tests/simple_chain.rs @@ -50,7 +50,7 @@ fn build_chain() { if cfg!(feature = "nightly") { insta::assert_display_snapshot!(hash, @"GargNTMFiuET32KH5uPLFwMSU8xXtvrk6aGqgkPbRZg8"); } else { - insta::assert_display_snapshot!(hash, @"8GP6PcFavb4pqeofMFjDyKUQnfVZtwPWsVA4V47WNbRn"); + insta::assert_display_snapshot!(hash, @"712T4sPbJhNWWN3bWweccECGYWbnUmGpqpKW2SJpb2k5"); } for i in 1..5 { @@ -80,7 +80,7 @@ fn build_chain() { if cfg!(feature = "nightly") { insta::assert_display_snapshot!(hash, @"2aurKZqRfPkZ3woNjA7Kf79wq5MYz98AohTYWoBFiG7o"); } else { - insta::assert_display_snapshot!(hash, @"319JoVaUej5iXmrZMeaZBPMeBLePQzJofA5Y1ztdyPw9"); + insta::assert_display_snapshot!(hash, @"GUAPgvPQQmhumyuFzPusg3BKtRkVLpCw4asTAWgdTLq6"); } } diff --git a/chain/chain/src/types.rs b/chain/chain/src/types.rs index 00c43fef52d..3c4f9af8316 100644 --- a/chain/chain/src/types.rs +++ b/chain/chain/src/types.rs @@ -75,6 +75,7 @@ pub struct AcceptedBlock { pub provenance: Provenance, } +#[derive(Debug)] pub struct ApplySplitStateResult { pub shard_uid: ShardUId, pub trie_changes: WrappedTrieChanges, @@ -86,11 +87,17 @@ pub struct ApplySplitStateResult { // if it's ready, apply transactions also apply updates to split states and this enum will be // ApplySplitStateResults // otherwise, it simply returns the state changes needed to be applied to split states +#[derive(Debug)] pub enum ApplySplitStateResultOrStateChanges { + /// Immediately apply the split state result. + /// Happens during IsCaughtUp and CatchingUp ApplySplitStateResults(Vec), + /// Store the split state results so that they can be applied later. + /// Happens during NotCaughtUp. StateChangesForSplitStates(StateChangesForSplitStates), } +#[derive(Debug)] pub struct ApplyTransactionResult { pub trie_changes: WrappedTrieChanges, pub new_root: StateRoot, @@ -505,7 +512,8 @@ mod tests { let b1 = TestBlockBuilder::new(&genesis, signer.clone()).build(); assert!(b1.header().verify_block_producer(&signer.public_key())); let other_signer = create_test_signer("other2"); - let approvals = vec![Some(Approval::new(*b1.hash(), 1, 2, &other_signer).signature)]; + let approvals = + vec![Some(Box::new(Approval::new(*b1.hash(), 1, 2, &other_signer).signature))]; let b2 = TestBlockBuilder::new(&b1, signer.clone()).approvals(approvals).build(); b2.header().verify_block_producer(&signer.public_key()); } diff --git a/chain/chunks/Cargo.toml b/chain/chunks/Cargo.toml index ade8f3c146d..d2468844e45 100644 --- a/chain/chunks/Cargo.toml +++ b/chain/chunks/Cargo.toml @@ -21,6 +21,8 @@ rand.workspace = true reed-solomon-erasure.workspace = true time.workspace = true tracing.workspace = true +# itertools has collect_vec which is useful in quick debugging prints +itertools.workspace = true near-async.workspace = true near-chain-configs.workspace = true diff --git a/chain/chunks/README.md b/chain/chunks/README.md index cd3137b0975..3515991f9a7 100644 --- a/chain/chunks/README.md +++ b/chain/chunks/README.md @@ -5,12 +5,12 @@ This crate cotains functions to handle chunks. In NEAR - the block consists of m When a chunk is created, the creator encodes its contents using Reed Solomon encoding (ErasureCoding) and adds cross-shard receipts - creating PartialEncodedChunks that are later sent to all the validators (each validator gets a subset of them). This is done for data availability reasons (so that we need only a part of the validators to reconstruct the whole chunk). You can read more about it in the Nightshade paper (https://near.org/nightshade/) -A honest validator will only approve a block if it receives its assigned parts for all chunks in the block - which means that for each chunk, it has `has_all_parts()` returning true. +A honest validator will only approve a block if it receives its assigned parts for all chunks in the block - which means that for each chunk, it has `has_all_parts()` returning true. For all nodes (validators and non-validators), they will only accept/process a block if the following requirements are satisfied: * for every shard it tracks, a node has to have the full chunk, -* for every shard it doesn't track, a node has have the receipts from this shard to all shards +* for every shard it doesn't track, a node has have the receipts from this shard to all shards If node tracks given shard (that is - it wants to have a whole content of the shard present in the local memory) - it will want to receive the necessary amount of PartialChunks to be able to reconstruct the whole chunk. As we use ReedSolomon, this means that they need `data_shard_count` PartialChunks (which is lower than `total_shard_count`). Afterwards, it will reconstruct the chunk and persist it in the local storage (via chain/client). @@ -21,7 +21,7 @@ When the PartialEncodedChunk is received (in chain/client) - it will try to vali Shard Manager is responsible for: * **fetching partial chunks** - it can ask other nodes for the partial chunks that it is missing. It keeps the track of the requests via RequestPool and can be asked to resend them when someone calls `resend_chunk_requests` (this is done periodically by the client actor). -* **storing partial chunks** - it stores partial chunks within local cache before they can be used to reconstruct for the full chunk. +* **storing partial chunks** - it stores partial chunks within local cache before they can be used to reconstruct for the full chunk. `stored_partial_encoded_chunks` stores non-validated partial chunks while `ChunkCache` stores validated partial chunks. (we need the previous block in order to validate partial chunks). This data is also used when other nodes are requesting a partial encoded chunk (see below). * **handling partial chunks requests** - when request for the partial chunk arrives, it handles reading it from store and returning to the requesting node * **validating chunks** - once it receives correct set of partial chunks for the chunk, it can 'accept the chunk' (which means that validator can sign the block if all chunks are accepted) diff --git a/chain/chunks/src/lib.rs b/chain/chunks/src/lib.rs index 710d5c13b52..864e5d0c0e0 100644 --- a/chain/chunks/src/lib.rs +++ b/chain/chunks/src/lib.rs @@ -606,17 +606,21 @@ impl ShardsManager { }, ); - if !mark_only { - let fetch_from_archival = chunk_needs_to_be_fetched_from_archival( + if mark_only { + debug!(target: "chunks", height, shard_id, ?chunk_hash, "Marked the chunk as being requested but did not send the request yet."); + return; + } + + let fetch_from_archival = chunk_needs_to_be_fetched_from_archival( &ancestor_hash, &self.chain_header_head.last_block_hash, self.epoch_manager.as_ref()).unwrap_or_else(|err| { error!(target: "chunks", "Error during requesting partial encoded chunk. Cannot determine whether to request from an archival node, defaulting to not: {}", err); false }); - let old_block = self.chain_header_head.last_block_hash != prev_block_hash - && self.chain_header_head.prev_block_hash != prev_block_hash; + let old_block = self.chain_header_head.last_block_hash != prev_block_hash + && self.chain_header_head.prev_block_hash != prev_block_hash; - let should_wait_for_chunk_forwarding = + let should_wait_for_chunk_forwarding = self.should_wait_for_chunk_forwarding(&ancestor_hash, chunk_header.shard_id(), chunk_header.height_created()+1).unwrap_or_else(|_| { // ancestor_hash must be accepted because we don't request missing chunks through this // this function for orphans @@ -625,30 +629,27 @@ impl ShardsManager { false }); - // If chunks forwarding is enabled, - // we purposely do not send chunk request messages right away for new blocks. Such requests - // will eventually be sent because of the `resend_chunk_requests` loop. However, - // we want to give some time for any `PartialEncodedChunkForward` messages to arrive - // before we send requests. - if !should_wait_for_chunk_forwarding || fetch_from_archival || old_block { - debug!(target: "chunks", height, shard_id, ?chunk_hash, "Requesting."); - let request_result = self.request_partial_encoded_chunk( - height, - &ancestor_hash, - shard_id, - &chunk_hash, - false, - old_block, - fetch_from_archival, - ); - if let Err(err) = request_result { - error!(target: "chunks", "Error during requesting partial encoded chunk: {}", err); - } - } else { - debug!(target: "chunks",should_wait_for_chunk_forwarding, fetch_from_archival, old_block, "Delaying the chunk request."); + // If chunks forwarding is enabled, + // we purposely do not send chunk request messages right away for new blocks. Such requests + // will eventually be sent because of the `resend_chunk_requests` loop. However, + // we want to give some time for any `PartialEncodedChunkForward` messages to arrive + // before we send requests. + if !should_wait_for_chunk_forwarding || fetch_from_archival || old_block { + debug!(target: "chunks", height, shard_id, ?chunk_hash, "Requesting."); + let request_result = self.request_partial_encoded_chunk( + height, + &ancestor_hash, + shard_id, + &chunk_hash, + false, + old_block, + fetch_from_archival, + ); + if let Err(err) = request_result { + error!(target: "chunks", "Error during requesting partial encoded chunk: {}", err); } } else { - debug!(target: "chunks", height, shard_id, ?chunk_hash, "Marked the chunk as being requested but did not send the request yet."); + debug!(target: "chunks",should_wait_for_chunk_forwarding, fetch_from_archival, old_block, "Delaying the chunk request."); } } @@ -792,9 +793,21 @@ impl ShardsManager { /// Finds the parts and receipt proofs asked for in the request, and returns a response /// containing whatever was found. See comment for PartialEncodedChunkResponseSource for /// an explanation of that part of the return value. + /// Ensures the receipts in the response are in a deterministic order. fn prepare_partial_encoded_chunk_response( &mut self, request: PartialEncodedChunkRequestMsg, + ) -> (PartialEncodedChunkResponseSource, PartialEncodedChunkResponseMsg) { + let (src, mut response_msg) = self.prepare_partial_encoded_chunk_response_unsorted(request); + // Note that the PartialChunks column is a write-once column, and needs + // the values to be deterministic. + response_msg.receipts.sort(); + (src, response_msg) + } + + fn prepare_partial_encoded_chunk_response_unsorted( + &mut self, + request: PartialEncodedChunkRequestMsg, ) -> (PartialEncodedChunkResponseSource, PartialEncodedChunkResponseMsg) { let PartialEncodedChunkRequestMsg { chunk_hash, part_ords, mut tracking_shards } = request; let mut response = PartialEncodedChunkResponseMsg { @@ -1422,7 +1435,7 @@ impl ShardsManager { } } - // 2. Consider it valid; mergeparts and receipts included in the partial encoded chunk + // 2. Consider it valid; merge parts and receipts included in the partial encoded chunk // into chunk cache let new_part_ords = self.encoded_chunks.merge_in_partial_encoded_chunk(partial_encoded_chunk); diff --git a/chain/chunks/src/logic.rs b/chain/chunks/src/logic.rs index ee42c120e2e..b0a4f823170 100644 --- a/chain/chunks/src/logic.rs +++ b/chain/chunks/src/logic.rs @@ -118,13 +118,15 @@ pub fn make_partial_encoded_chunk_from_owned_parts_and_needed_receipts<'a>( }) .cloned() .collect(); - let receipts = receipts + let mut receipts: Vec<_> = receipts .filter(|receipt| { cares_about_shard || need_receipt(prev_block_hash, receipt.1.to_shard_id, me, shard_tracker) }) .cloned() .collect(); + // Make sure the receipts are in a deterministic order. + receipts.sort(); match header.clone() { ShardChunkHeader::V1(header) => { PartialEncodedChunk::V1(PartialEncodedChunkV1 { header, parts, receipts }) @@ -153,7 +155,6 @@ pub fn decode_encoded_chunk( }) { debug!(target: "chunks", "Reconstructed and decoded chunk {}, encoded length was {}, num txs: {}, I'm {:?}", chunk_hash.0, encoded_chunk.encoded_length(), shard_chunk.transactions().len(), me); - let partial_chunk = create_partial_chunk( encoded_chunk, merkle_paths, diff --git a/chain/client-primitives/src/debug.rs b/chain/client-primitives/src/debug.rs index 9e1118da9aa..2a5618944e8 100644 --- a/chain/client-primitives/src/debug.rs +++ b/chain/client-primitives/src/debug.rs @@ -167,6 +167,7 @@ pub struct ValidatorStatus { } // Different debug requests that can be sent by HTML pages, via GET. +#[derive(Debug)] pub enum DebugStatus { // Request for the current sync status SyncStatus, diff --git a/chain/client-primitives/src/types.rs b/chain/client-primitives/src/types.rs index 7ffe8b781ab..46a0ba160b4 100644 --- a/chain/client-primitives/src/types.rs +++ b/chain/client-primitives/src/types.rs @@ -1,5 +1,5 @@ use actix::Message; -use ansi_term::Color::{Purple, Yellow}; +use ansi_term::Color::Purple; use ansi_term::Style; use chrono::DateTime; use chrono::Utc; @@ -231,31 +231,16 @@ pub fn format_shard_sync_phase( ShardSyncStatus::StateDownloadParts => { let mut num_parts_done = 0; let mut num_parts_not_done = 0; - let mut text = "".to_string(); - for (i, download) in shard_sync_download.downloads.iter().enumerate() { + for download in shard_sync_download.downloads.iter() { if download.done { num_parts_done += 1; - continue; + } else { + num_parts_not_done += 1; } - num_parts_not_done += 1; - text.push_str(&format!( - "[{}: {}, {}, {:?}] ", - paint(&i.to_string(), Yellow.bold(), use_colour), - download.done, - download.state_requests_count, - download.last_target - )); } - format!( - "{} [{}: is_done, requests sent, last target] {} num_parts_done={} num_parts_not_done={}", - paint("PARTS", Purple.bold(), use_colour), - paint("part_id", Yellow.bold(), use_colour), - text, - num_parts_done, - num_parts_not_done - ) + format!("num_parts_done={num_parts_done} num_parts_not_done={num_parts_not_done}") } - status => format!("{:?}", status), + status => format!("{status:?}"), } } @@ -372,6 +357,7 @@ impl From for SyncStatusView { } /// Actor message requesting block by id, hash or sync state. +#[derive(Debug)] pub struct GetBlock(pub BlockReference); #[derive(thiserror::Error, Debug)] @@ -415,6 +401,7 @@ impl Message for GetBlock { } /// Get block with the block merkle tree. Used for testing +#[derive(Debug)] pub struct GetBlockWithMerkleTree(pub BlockReference); impl GetBlockWithMerkleTree { @@ -428,6 +415,7 @@ impl Message for GetBlockWithMerkleTree { } /// Actor message requesting a chunk by chunk hash and block hash + shard id. +#[derive(Debug)] pub enum GetChunk { Height(BlockHeight, ShardId), BlockHash(CryptoHash, ShardId), @@ -558,6 +546,7 @@ pub enum QueryError { Unreachable { error_message: String }, } +#[derive(Debug)] pub struct Status { pub is_health_check: bool, // If true - return more detailed information about the current status (recent blocks etc). @@ -604,6 +593,7 @@ impl Message for Status { type Result = Result; } +#[derive(Debug)] pub struct GetNextLightClientBlock { pub last_block_hash: CryptoHash, } @@ -645,12 +635,14 @@ impl Message for GetNextLightClientBlock { type Result = Result>, GetNextLightClientBlockError>; } +#[derive(Debug)] pub struct GetNetworkInfo {} impl Message for GetNetworkInfo { type Result = Result; } +#[derive(Debug)] pub struct GetGasPrice { pub block_id: MaybeBlockId, } @@ -739,6 +731,7 @@ impl Message for TxStatus { type Result = Result, TxStatusError>; } +#[derive(Debug)] pub struct GetValidatorInfo { pub epoch_reference: EpochReference, } @@ -774,6 +767,7 @@ impl From for GetValidatorInfoError { } } +#[derive(Debug)] pub struct GetValidatorOrdered { pub block_id: MaybeBlockId, } @@ -782,6 +776,7 @@ impl Message for GetValidatorOrdered { type Result = Result, GetValidatorInfoError>; } +#[derive(Debug)] pub struct GetStateChanges { pub block_hash: CryptoHash, pub state_changes_request: StateChangesRequestView, @@ -821,6 +816,7 @@ impl Message for GetStateChanges { type Result = Result; } +#[derive(Debug)] pub struct GetStateChangesInBlock { pub block_hash: CryptoHash, } @@ -829,6 +825,7 @@ impl Message for GetStateChangesInBlock { type Result = Result; } +#[derive(Debug)] pub struct GetStateChangesWithCauseInBlock { pub block_hash: CryptoHash, } @@ -837,6 +834,7 @@ impl Message for GetStateChangesWithCauseInBlock { type Result = Result; } +#[derive(Debug)] pub struct GetStateChangesWithCauseInBlockForTrackedShards { pub block_hash: CryptoHash, pub epoch_id: EpochId, @@ -846,6 +844,7 @@ impl Message for GetStateChangesWithCauseInBlockForTrackedShards { type Result = Result, GetStateChangesError>; } +#[derive(Debug)] pub struct GetExecutionOutcome { pub id: TransactionOrReceiptId, } @@ -912,6 +911,7 @@ impl Message for GetExecutionOutcome { type Result = Result; } +#[derive(Debug)] pub struct GetExecutionOutcomesForBlock { pub block_hash: CryptoHash, } @@ -920,6 +920,7 @@ impl Message for GetExecutionOutcomesForBlock { type Result = Result>, String>; } +#[derive(Debug)] pub struct GetBlockProof { pub block_hash: CryptoHash, pub head_block_hash: CryptoHash, @@ -962,6 +963,7 @@ impl Message for GetBlockProof { type Result = Result; } +#[derive(Debug)] pub struct GetReceipt { pub receipt_id: CryptoHash, } @@ -993,6 +995,7 @@ impl Message for GetReceipt { type Result = Result, GetReceiptError>; } +#[derive(Debug)] pub struct GetProtocolConfig(pub BlockReference); impl Message for GetProtocolConfig { @@ -1023,6 +1026,7 @@ impl From for GetProtocolConfigError { } } +#[derive(Debug)] pub struct GetMaintenanceWindows { pub account_id: AccountId, } @@ -1048,6 +1052,7 @@ impl From for GetMaintenanceWindowsError { } } +#[derive(Debug)] pub struct GetClientConfig {} impl Message for GetClientConfig { @@ -1075,6 +1080,7 @@ impl From for GetClientConfigError { } } +#[derive(Debug)] pub struct GetSplitStorageInfo {} impl Message for GetSplitStorageInfo { diff --git a/chain/client/Cargo.toml b/chain/client/Cargo.toml index eaef75f9843..e34f3f317a9 100644 --- a/chain/client/Cargo.toml +++ b/chain/client/Cargo.toml @@ -74,7 +74,6 @@ delay_detector = [ "near-network/delay_detector", "delay-detector/delay_detector", ] -protocol_feature_block_header_v4 = ["near-primitives/protocol_feature_block_header_v4"] nightly_protocol = [ "near-async/nightly_protocol", "near-chain-configs/nightly_protocol", @@ -92,7 +91,6 @@ nightly_protocol = [ ] nightly = [ "nightly_protocol", - "protocol_feature_block_header_v4", "near-async/nightly", "near-chain-configs/nightly", "near-chain/nightly", @@ -111,3 +109,6 @@ sandbox = [ "near-client-primitives/sandbox", "near-chain/sandbox", ] +new_epoch_sync = [ + "near-chain/new_epoch_sync" +] diff --git a/chain/client/src/adapter.rs b/chain/client/src/adapter.rs index 3446d5f0095..1e0cbcd8a32 100644 --- a/chain/client/src/adapter.rs +++ b/chain/client/src/adapter.rs @@ -16,7 +16,7 @@ use near_primitives::types::{AccountId, EpochId, ShardId}; use near_primitives::views::FinalExecutionOutcomeView; /// Transaction status query -#[derive(actix::Message)] +#[derive(actix::Message, Debug)] #[rtype(result = "Option>")] pub(crate) struct TxStatusRequest { pub tx_hash: CryptoHash, @@ -24,12 +24,12 @@ pub(crate) struct TxStatusRequest { } /// Transaction status response -#[derive(actix::Message)] +#[derive(actix::Message, Debug)] #[rtype(result = "()")] pub(crate) struct TxStatusResponse(pub Box); /// Request a block. -#[derive(actix::Message)] +#[derive(actix::Message, Debug)] #[rtype(result = "Option>")] pub(crate) struct BlockRequest(pub CryptoHash); @@ -47,7 +47,7 @@ pub struct BlockResponse { pub struct BlockApproval(pub Approval, pub PeerId); /// Request headers. -#[derive(actix::Message)] +#[derive(actix::Message, Debug)] #[rtype(result = "Option>")] pub(crate) struct BlockHeadersRequest(pub Vec); @@ -57,7 +57,7 @@ pub(crate) struct BlockHeadersRequest(pub Vec); pub(crate) struct BlockHeadersResponse(pub Vec, pub PeerId); /// State request header. -#[derive(actix::Message)] +#[derive(actix::Message, Debug)] #[rtype(result = "Option")] pub(crate) struct StateRequestHeader { pub shard_id: ShardId, @@ -81,7 +81,7 @@ pub(crate) struct StateResponse(pub Box); /// Account announcements that needs to be validated before being processed. /// They are paired with last epoch id known to this announcement, in order to accept only /// newer announcements. -#[derive(actix::Message)] +#[derive(actix::Message, Debug)] #[rtype(result = "Result,ReasonForBan>")] pub(crate) struct AnnounceAccountRequest(pub Vec<(AnnounceAccount, Option)>); diff --git a/chain/client/src/client.rs b/chain/client/src/client.rs index af8f852b03f..c8a3dd6f5c1 100644 --- a/chain/client/src/client.rs +++ b/chain/client/src/client.rs @@ -615,7 +615,7 @@ impl Client { if is_slashed { None } else { - approvals_map.remove(&account_id).map(|x| x.0.signature) + approvals_map.remove(&account_id).map(|x| x.0.signature.into()) } }) .collect(); @@ -1506,124 +1506,128 @@ impl Client { } if let Some(validator_signer) = self.validator_signer.clone() { - // Reconcile the txpool against the new block *after* we have broadcast it too our peers. - // This may be slow and we do not want to delay block propagation. let validator_id = validator_signer.validator_id().clone(); - match status { - BlockStatus::Next => { - // If this block immediately follows the current tip, remove transactions - // from the txpool - self.remove_transactions_for_block(validator_id.clone(), &block); - } - BlockStatus::Fork => { - // If it's a fork, no need to reconcile transactions or produce chunks - return; - } - BlockStatus::Reorg(prev_head) => { - // If a reorg happened, reintroduce transactions from the previous chain and - // remove transactions from the new chain - let mut reintroduce_head = self.chain.get_block_header(&prev_head).unwrap(); - let mut remove_head = block.header().clone(); - assert_ne!(remove_head.hash(), reintroduce_head.hash()); - - let mut to_remove = vec![]; - let mut to_reintroduce = vec![]; - - while remove_head.hash() != reintroduce_head.hash() { - while remove_head.height() > reintroduce_head.height() { - to_remove.push(*remove_head.hash()); - remove_head = self - .chain - .get_block_header(remove_head.prev_hash()) - .unwrap() - .clone(); - } - while reintroduce_head.height() > remove_head.height() - || reintroduce_head.height() == remove_head.height() - && reintroduce_head.hash() != remove_head.hash() - { - to_reintroduce.push(*reintroduce_head.hash()); - reintroduce_head = self - .chain - .get_block_header(reintroduce_head.prev_hash()) - .unwrap() - .clone(); - } - } - - for to_reintroduce_hash in to_reintroduce { - if let Ok(block) = self.chain.get_block(&to_reintroduce_hash) { - let block = block.clone(); - self.reintroduce_transactions_for_block(validator_id.clone(), &block); - } - } - for to_remove_hash in to_remove { - if let Ok(block) = self.chain.get_block(&to_remove_hash) { - let block = block.clone(); - self.remove_transactions_for_block(validator_id.clone(), &block); - } - } - } - }; + if !self.reconcile_transaction_pool(validator_id.clone(), status, &block) { + return; + } if provenance != Provenance::SYNC && !self.sync_status.is_syncing() && !skip_produce_chunk { - // Produce new chunks - let epoch_id = - self.epoch_manager.get_epoch_id_from_prev_block(block.header().hash()).unwrap(); - for shard_id in 0..self.epoch_manager.num_shards(&epoch_id).unwrap() { - let chunk_proposer = self - .epoch_manager - .get_chunk_producer(&epoch_id, block.header().height() + 1, shard_id) - .unwrap(); - - if &chunk_proposer == &validator_id { - let _span = tracing::debug_span!( - target: "client", - "on_block_accepted", - prev_block_hash = ?*block.hash(), - ?shard_id) - .entered(); - let _timer = metrics::PRODUCE_AND_DISTRIBUTE_CHUNK_TIME - .with_label_values(&[&shard_id.to_string()]) - .start_timer(); - match self.produce_chunk( - *block.hash(), - &epoch_id, - Chain::get_prev_chunk_header( - self.epoch_manager.as_ref(), - &block, - shard_id, - ) - .unwrap(), - block.header().height() + 1, - shard_id, - ) { - Ok(Some((encoded_chunk, merkle_paths, receipts))) => { - self.persist_and_distribute_encoded_chunk( - encoded_chunk, - merkle_paths, - receipts, - validator_id.clone(), - ) - .expect("Failed to process produced chunk"); - } - Ok(None) => {} - Err(err) => { - error!(target: "client", "Error producing chunk {:?}", err); - } - } - } - } + self.produce_chunks(&block, validator_id); } } + self.shards_manager_adapter .send(ShardsManagerRequestFromClient::CheckIncompleteChunks(*block.hash())); } + /// Reconcile the transaction pool after processing a block. + /// returns true if it's ok to proceed to produce chunks + /// returns false when handling a fork and there is no need to produce chunks + fn reconcile_transaction_pool( + &mut self, + validator_id: AccountId, + status: BlockStatus, + block: &Block, + ) -> bool { + match status { + BlockStatus::Next => { + // If this block immediately follows the current tip, remove + // transactions from the txpool. + self.remove_transactions_for_block(validator_id, block); + } + BlockStatus::Fork => { + // If it's a fork, no need to reconcile transactions or produce chunks. + return false; + } + BlockStatus::Reorg(prev_head) => { + // If a reorg happened, reintroduce transactions from the + // previous chain and remove transactions from the new chain. + let mut reintroduce_head = self.chain.get_block_header(&prev_head).unwrap(); + let mut remove_head = block.header().clone(); + assert_ne!(remove_head.hash(), reintroduce_head.hash()); + + let mut to_remove = vec![]; + let mut to_reintroduce = vec![]; + + while remove_head.hash() != reintroduce_head.hash() { + while remove_head.height() > reintroduce_head.height() { + to_remove.push(*remove_head.hash()); + remove_head = + self.chain.get_block_header(remove_head.prev_hash()).unwrap().clone(); + } + while reintroduce_head.height() > remove_head.height() + || reintroduce_head.height() == remove_head.height() + && reintroduce_head.hash() != remove_head.hash() + { + to_reintroduce.push(*reintroduce_head.hash()); + reintroduce_head = self + .chain + .get_block_header(reintroduce_head.prev_hash()) + .unwrap() + .clone(); + } + } + + for to_reintroduce_hash in to_reintroduce { + if let Ok(block) = self.chain.get_block(&to_reintroduce_hash) { + let block = block.clone(); + self.reintroduce_transactions_for_block(validator_id.clone(), &block); + } + } + + for to_remove_hash in to_remove { + if let Ok(block) = self.chain.get_block(&to_remove_hash) { + let block = block.clone(); + self.remove_transactions_for_block(validator_id.clone(), &block); + } + } + } + }; + true + } + + // Produce new chunks + fn produce_chunks(&mut self, block: &Block, validator_id: AccountId) { + let epoch_id = + self.epoch_manager.get_epoch_id_from_prev_block(block.header().hash()).unwrap(); + for shard_id in 0..self.epoch_manager.num_shards(&epoch_id).unwrap() { + let next_height = block.header().height() + 1; + let epoch_manager = self.epoch_manager.as_ref(); + let chunk_proposer = + epoch_manager.get_chunk_producer(&epoch_id, next_height, shard_id).unwrap(); + if &chunk_proposer != &validator_id { + continue; + } + + let _span = tracing::debug_span!( + target: "client", "on_block_accepted", prev_block_hash = ?*block.hash(), ?shard_id) + .entered(); + let _timer = metrics::PRODUCE_AND_DISTRIBUTE_CHUNK_TIME + .with_label_values(&[&shard_id.to_string()]) + .start_timer(); + let last_header = Chain::get_prev_chunk_header(epoch_manager, block, shard_id).unwrap(); + match self.produce_chunk(*block.hash(), &epoch_id, last_header, next_height, shard_id) { + Ok(Some((encoded_chunk, merkle_paths, receipts))) => { + self.persist_and_distribute_encoded_chunk( + encoded_chunk, + merkle_paths, + receipts, + validator_id.clone(), + ) + .expect("Failed to process produced chunk"); + } + Ok(None) => {} + Err(err) => { + error!(target: "client", "Error producing chunk {:?}", err); + } + } + } + } + pub fn persist_and_distribute_encoded_chunk( &mut self, encoded_chunk: EncodedShardChunk, @@ -2016,9 +2020,11 @@ impl Client { let shard_id = self.epoch_manager.account_id_to_shard_id(&tx.transaction.signer_id, &epoch_id)?; - if self.shard_tracker.care_about_shard(me, &head.last_block_hash, shard_id, true) - || self.shard_tracker.will_care_about_shard(me, &head.last_block_hash, shard_id, true) - { + let care_about_shard = + self.shard_tracker.care_about_shard(me, &head.last_block_hash, shard_id, true); + let will_care_about_shard = + self.shard_tracker.will_care_about_shard(me, &head.last_block_hash, shard_id, true); + if care_about_shard || will_care_about_shard { let shard_uid = self.epoch_manager.shard_id_to_uid(shard_id, &epoch_id)?; let state_root = match self.chain.get_chunk_extra(&head.last_block_hash, &shard_uid) { Ok(chunk_extra) => *chunk_extra.state_root(), @@ -2089,14 +2095,12 @@ impl Client { } } else if check_only { Ok(ProcessTxResponse::DoesNotTrackShard) + } else if is_forwarded { + // Received forwarded transaction but we are not tracking the shard + debug!(target: "client", "Received forwarded transaction but no tracking shard {}, I'm {:?}", shard_id, me); + Ok(ProcessTxResponse::NoResponse) } else { - if is_forwarded { - // received forwarded transaction but we are not tracking the shard - debug!(target: "client", "Received forwarded transaction but no tracking shard {}, I'm {:?}", shard_id, me); - return Ok(ProcessTxResponse::NoResponse); - } // We are not tracking this shard, so there is no way to validate this tx. Just rerouting. - self.forward_tx(&epoch_id, tx)?; Ok(ProcessTxResponse::RequestRouted) } diff --git a/chain/client/src/client_actor.rs b/chain/client/src/client_actor.rs index 49fef8c3850..6977f8778dd 100644 --- a/chain/client/src/client_actor.rs +++ b/chain/client/src/client_actor.rs @@ -411,7 +411,7 @@ impl Handler> for ClientActor { type Result = (); fn handle(&mut self, msg: WithSpanContext, ctx: &mut Context) { - self.wrap(msg,ctx,"BlockResponse",|this:&mut Self,msg|{ + self.wrap(msg, ctx, "BlockResponse", |this: &mut Self, msg|{ let BlockResponse{ block, peer_id, was_requested } = msg; let blocks_at_height = this .client @@ -499,7 +499,7 @@ impl Handler> for ClientActor { type Result = (); fn handle(&mut self, msg: WithSpanContext, ctx: &mut Context) { - self.wrap(msg,ctx,"StateResponse",|this,msg| { + self.wrap(msg, ctx, "StateResponse", |this, msg| { let StateResponse(state_response_info) = msg; let shard_id = state_response_info.shard_id(); let hash = state_response_info.sync_hash(); @@ -557,6 +557,7 @@ impl Handler> for ClientActor { type Result = (); fn handle(&mut self, msg: WithSpanContext, ctx: &mut Context) { + // SetNetworkInfo is a large message. Avoid printing it at the `debug` verbosity. self.wrap(msg, ctx, "SetNetworkInfo", |this, msg| { let SetNetworkInfo(network_info) = msg; this.network_info = network_info; @@ -610,6 +611,7 @@ impl Handler> for ClientActor { #[perf] fn handle(&mut self, msg: WithSpanContext, ctx: &mut Context) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _d = delay_detector::DelayDetector::new(|| "client status".into()); self.check_triggers(ctx); @@ -787,7 +789,7 @@ impl Handler> for ClientActor { /// `ApplyChunksDoneMessage` is a message that signals the finishing of applying chunks of a block. /// Upon receiving this message, ClientActors knows that it's time to finish processing the blocks that /// just finished applying chunks. -#[derive(actix::Message)] +#[derive(actix::Message, Debug)] #[rtype(result = "()")] pub struct ApplyChunksDoneMessage; @@ -1465,30 +1467,21 @@ impl ClientActor { /// /// The selected block will always be the first block on a new epoch: /// . - /// - /// To prevent syncing from a fork, we move `state_fetch_horizon` steps backwards and use that epoch. - /// Usually `state_fetch_horizon` is much less than the expected number of produced blocks on an epoch, - /// so this is only relevant on epoch boundaries. fn find_sync_hash(&mut self) -> Result { let header_head = self.client.chain.header_head()?; - let mut sync_hash = header_head.prev_block_hash; - for _ in 0..self.client.config.state_fetch_horizon { - sync_hash = *self.client.chain.get_block_header(&sync_hash)?.prev_hash(); - } - let mut epoch_start_sync_hash = + let sync_hash = header_head.last_block_hash; + let epoch_start_sync_hash = StateSync::get_epoch_start_sync_hash(&mut self.client.chain, &sync_hash)?; - if &epoch_start_sync_hash == self.client.chain.genesis().hash() { - // If we are within `state_fetch_horizon` blocks of the second epoch, the sync hash will - // be the first block of the first epoch (or, the genesis block). Due to implementation - // details of the state sync, we can't state sync to the genesis block, so redo the - // search without going back `state_fetch_horizon` blocks. - epoch_start_sync_hash = StateSync::get_epoch_start_sync_hash( - &mut self.client.chain, - &header_head.last_block_hash, - )?; - assert_ne!(&epoch_start_sync_hash, self.client.chain.genesis().hash()); - } + let genesis_hash = self.client.chain.genesis().hash(); + tracing::debug!( + target: "sync", + ?header_head, + ?sync_hash, + ?epoch_start_sync_hash, + ?genesis_hash, + "find_sync_hash"); + assert_ne!(&epoch_start_sync_hash, genesis_hash); Ok(epoch_start_sync_hash) } @@ -1754,6 +1747,7 @@ impl Handler> for ClientActor { _: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); if let Some((sync, _, _)) = self.client.catchup_state_syncs.get_mut(&msg.sync_hash) { // We are doing catchup sync.set_apply_result(msg.shard_id, msg.apply_result); @@ -1772,6 +1766,7 @@ impl Handler> for ClientActor { _: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); if let Some((_, _, blocks_catch_up_state)) = self.client.catchup_state_syncs.get_mut(&msg.sync_hash) { @@ -1792,6 +1787,7 @@ impl Handler> for ClientActor { _: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); if let Some((sync, _, _)) = self.client.catchup_state_syncs.get_mut(&msg.sync_hash) { // We are doing catchup sync.set_split_result(msg.shard_id, msg.new_state_roots); @@ -1839,7 +1835,8 @@ impl Handler> for ClientActor { msg: WithSpanContext, _: &mut Context, ) -> Self::Result { - let (_span, _msg) = handler_debug_span!(target: "client", msg); + let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _d = delay_detector::DelayDetector::new(|| "client get client config".into()); Ok(self.client.config.clone()) diff --git a/chain/client/src/info.rs b/chain/client/src/info.rs index bc67c19694b..e442f1b3bbe 100644 --- a/chain/client/src/info.rs +++ b/chain/client/src/info.rs @@ -641,17 +641,23 @@ pub fn display_sync_status( for (shard_id, shard_status) in shard_statuses { write!(res, "[{}: {}]", shard_id, shard_status.status.to_string(),).unwrap(); } - if matches!(state_sync_config, SyncConfig::Peers) { - // TODO #8719 - tracing::warn!( - target: "stats", - "The node is syncing its State. The current implementation of this mechanism is known to be unreliable. It may never complete, or fail randomly and corrupt the DB.\n\ - Suggestions:\n\ - * Download a recent data snapshot and restart the node.\n\ - * Disable state sync in the config. Add `\"state_sync_enabled\": false` to `config.json`.\n\ - \n\ - A better implementation of State Sync is work in progress."); - } + match state_sync_config { + SyncConfig::Peers => { + tracing::warn!( + target: "stats", + "The node is trying to sync its State from its peers. The current implementation of this mechanism is known to be unreliable. It may never complete, or fail randomly and corrupt the DB.\n\ + Suggestions:\n\ + * Try to state sync from GCS. See `\"state_sync\"` and `\"state_sync_enabled\"` options in the reference `config.json` file. + or + * Disable state sync in the config. Add `\"state_sync_enabled\": false` to `config.json`, then download a recent data snapshot and restart the node."); + } + SyncConfig::ExternalStorage(_) => { + tracing::info!( + target: "stats", + "The node is trying to sync its State from external storage. The current implementation is experimental. If it fails, consider disabling state sync and restarting from a recent snapshot:\n\ + - Add `\"state_sync_enabled\": false` to `config.json`, then download a recent data snapshot and restart the node."); + } + }; res } SyncStatus::StateSyncDone => "State sync done".to_string(), diff --git a/chain/client/src/sync/block.rs b/chain/client/src/sync/block.rs index 85bf04868c9..5189dbd1a22 100644 --- a/chain/client/src/sync/block.rs +++ b/chain/client/src/sync/block.rs @@ -96,6 +96,18 @@ impl BlockSync { && !self.archive && self.state_sync_enabled { + tracing::debug!( + target: "sync", + head_epoch_id = ?head.epoch_id, + header_head_epoch_id = ?header_head.epoch_id, + head_next_epoch_id = ?head.next_epoch_id, + head_height = head.height, + header_head_height = header_head.height, + header_head_height_sub = header_head.height.saturating_sub(self.block_fetch_horizon), + archive = self.archive, + state_sync_enabled = self.state_sync_enabled, + block_fetch_horizon = self.block_fetch_horizon, + "Switched from block sync to state sync"); // Epochs are different and we are too far from horizon, State Sync is needed return Ok(true); } diff --git a/chain/client/src/sync/header.rs b/chain/client/src/sync/header.rs index b3632e0afe0..70d7784f35a 100644 --- a/chain/client/src/sync/header.rs +++ b/chain/client/src/sync/header.rs @@ -676,7 +676,7 @@ mod test { signers2 .iter() .map(|signer| { - Some( + Some(Box::new( Approval::new( *last_block.hash(), last_block.header().height(), @@ -684,7 +684,7 @@ mod test { signer.as_ref(), ) .signature, - ) + )) }) .collect(), Ratio::new(0, 1), diff --git a/chain/client/src/sync/state.rs b/chain/client/src/sync/state.rs index 85f10e04b30..af05a0e85f2 100644 --- a/chain/client/src/sync/state.rs +++ b/chain/client/src/sync/state.rs @@ -33,6 +33,7 @@ use near_chain::near_chain_primitives; use near_chain::resharding::StateSplitRequest; use near_chain::Chain; use near_chain_configs::{ExternalStorageConfig, ExternalStorageLocation, SyncConfig}; +use near_client_primitives::types::format_shard_sync_phase_per_shard; use near_client_primitives::types::{ format_shard_sync_phase, DownloadStatus, ShardSyncDownload, ShardSyncStatus, }; @@ -297,9 +298,6 @@ impl StateSync { let old_status = shard_sync_download.status.clone(); let mut shard_sync_done = false; - metrics::STATE_SYNC_STAGE - .with_label_values(&[&shard_id.to_string()]) - .set(shard_sync_download.status.repr() as i64); match &shard_sync_download.status { ShardSyncStatus::StateDownloadHeader => { (download_timeout, run_shard_state_download) = self @@ -312,12 +310,8 @@ impl StateSync { )?; } ShardSyncStatus::StateDownloadParts => { - let res = self.sync_shards_download_parts_status( - shard_id, - shard_sync_download, - sync_hash, - now, - ); + let res = + self.sync_shards_download_parts_status(shard_id, shard_sync_download, now); download_timeout = res.0; run_shard_state_download = res.1; update_sync_status |= res.2; @@ -342,13 +336,8 @@ impl StateSync { )?; } ShardSyncStatus::StateDownloadComplete => { - shard_sync_done = self.sync_shards_download_complete_status( - split_states, - shard_id, - shard_sync_download, - sync_hash, - chain, - )?; + shard_sync_done = self + .sync_shards_download_complete_status(split_states, shard_sync_download); } ShardSyncStatus::StateSplitScheduling => { debug_assert!(split_states); @@ -374,6 +363,14 @@ impl StateSync { shard_sync_done = true; } } + let stage = if shard_sync_done { + // Update the state sync stage metric, because maybe we'll not + // enter this function again. + ShardSyncStatus::StateSyncDone.repr() + } else { + shard_sync_download.status.repr() + }; + metrics::STATE_SYNC_STAGE.with_label_values(&[&shard_id.to_string()]).set(stage as i64); all_done &= shard_sync_done; if download_timeout { @@ -407,6 +404,13 @@ impl StateSync { } update_sync_status |= shard_sync_download.status != old_status; } + if update_sync_status { + // Print debug messages only if something changed. + // Otherwise it spams the debug logs. + tracing::debug!( + target: "sync", + progress_per_shard = ?format_shard_sync_phase_per_shard(new_shard_sync, false)); + } Ok((update_sync_status, all_done)) } @@ -951,7 +955,6 @@ impl StateSync { &mut self, shard_id: ShardId, shard_sync_download: &mut ShardSyncDownload, - sync_hash: CryptoHash, now: DateTime, ) -> (bool, bool, bool) { // Step 2 - download all the parts (each part is usually around 1MB). @@ -991,7 +994,6 @@ impl StateSync { num_parts_done += 1; } } - tracing::debug!(target: "sync", %shard_id, %sync_hash, num_parts_done, parts_done); metrics::STATE_SYNC_PARTS_DONE .with_label_values(&[&shard_id.to_string()]) .set(num_parts_done); @@ -1058,8 +1060,7 @@ impl StateSync { ) -> Result<(), near_chain::Error> { // Keep waiting until our shard is on the list of results // (these are set via callback from ClientActor - both for sync and catchup). - let result = self.state_parts_apply_results.remove(&shard_id); - if let Some(result) = result { + if let Some(result) = self.state_parts_apply_results.remove(&shard_id) { match chain.set_state_finalize(shard_id, sync_hash, result) { Ok(()) => { *shard_sync_download = ShardSyncDownload { @@ -1088,30 +1089,21 @@ impl StateSync { fn sync_shards_download_complete_status( &mut self, split_states: bool, - shard_id: ShardId, shard_sync_download: &mut ShardSyncDownload, - sync_hash: CryptoHash, - chain: &mut Chain, - ) -> Result { - let shard_state_header = chain.get_state_header(shard_id, sync_hash)?; - let state_num_parts = - get_num_state_parts(shard_state_header.state_root_node().memory_usage); - chain.clear_downloaded_parts(shard_id, sync_hash, state_num_parts)?; - - let mut shard_sync_done = false; + ) -> bool { // If the shard layout is changing in this epoch - we have to apply it right now. if split_states { *shard_sync_download = ShardSyncDownload { downloads: vec![], status: ShardSyncStatus::StateSplitScheduling, - } + }; + false } else { // If there is no layout change - we're done. *shard_sync_download = ShardSyncDownload { downloads: vec![], status: ShardSyncStatus::StateSyncDone }; - shard_sync_done = true; + true } - Ok(shard_sync_done) } fn sync_shards_state_split_scheduling_status( @@ -1145,7 +1137,7 @@ impl StateSync { let result = self.split_state_roots.remove(&shard_id); let mut shard_sync_done = false; if let Some(state_roots) = result { - chain.build_state_for_split_shards_postprocessing(&sync_hash, state_roots)?; + chain.build_state_for_split_shards_postprocessing(&sync_hash, state_roots?)?; *shard_sync_download = ShardSyncDownload { downloads: vec![], status: ShardSyncStatus::StateSyncDone }; shard_sync_done = true; diff --git a/chain/client/src/sync/state_sync_actor.rs b/chain/client/src/sync/state_sync_actor.rs deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/chain/client/src/sync_jobs_actor.rs b/chain/client/src/sync_jobs_actor.rs index 8e0745ad6cd..fa9e3cbba9a 100644 --- a/chain/client/src/sync_jobs_actor.rs +++ b/chain/client/src/sync_jobs_actor.rs @@ -119,6 +119,7 @@ impl actix::Handler> for SyncJobsActor { _: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let results = do_apply_chunks(msg.block_hash, msg.block_height, msg.work); self.client_addr.do_send( @@ -137,6 +138,7 @@ impl actix::Handler> for SyncJobsActor { _: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let response = Chain::build_state_for_split_shards(msg); self.client_addr.do_send(response.with_span_context()); } diff --git a/chain/client/src/test_utils.rs b/chain/client/src/test_utils.rs index 2fe2960828c..f5133870e18 100644 --- a/chain/client/src/test_utils.rs +++ b/chain/client/src/test_utils.rs @@ -1,3 +1,7 @@ +// FIXME(nagisa): Is there a good reason we're triggering this? Luckily though this is just test +// code so we're in the clear. +#![allow(clippy::arc_with_non_send_sync)] + use std::cmp::max; use std::collections::{HashMap, HashSet}; use std::mem::swap; @@ -55,8 +59,8 @@ use near_network::types::{ }; use near_o11y::testonly::TracingCapture; use near_o11y::WithSpanContextExt; +use near_primitives::action::delegate::{DelegateAction, NonDelegateAction, SignedDelegateAction}; use near_primitives::block::{ApprovalInner, Block, GenesisId}; -use near_primitives::delegate_action::{DelegateAction, NonDelegateAction, SignedDelegateAction}; use near_primitives::epoch_manager::RngSeed; use near_primitives::hash::{hash, CryptoHash}; use near_primitives::merkle::{merklize, MerklePath, PartialMerkleTree}; @@ -85,7 +89,6 @@ use near_telemetry::TelemetryActor; use crate::adapter::{ AnnounceAccountRequest, BlockApproval, BlockHeadersRequest, BlockHeadersResponse, BlockRequest, BlockResponse, ProcessTxResponse, SetNetworkInfo, StateRequestHeader, StateRequestPart, - StateResponse, }; pub struct PeerManagerMock { @@ -976,16 +979,6 @@ pub fn setup_mock_all_validators( } } } - NetworkRequests::StateResponse { route_back, response } => { - for (i, address) in addresses.iter().enumerate() { - if route_back == address { - connectors1[i].client_actor.do_send( - StateResponse(Box::new(response.clone())) - .with_span_context(), - ); - } - } - } NetworkRequests::AnnounceAccount(announce_account) => { let mut aa = announced_accounts1.write().unwrap(); let key = ( @@ -1765,9 +1758,9 @@ impl TestEnvBuilder { }; let make_state_snapshot_callback : Option = if self.add_state_snapshots { let runtime = runtime.clone(); - let snapshot : MakeSnapshotCallback = Arc::new(move |prev_block_hash, shard_uids| { + let snapshot : MakeSnapshotCallback = Arc::new(move |prev_block_hash, shard_uids, block| { tracing::info!(target: "state_snapshot", ?prev_block_hash, "make_snapshot_callback"); - runtime.get_tries().make_state_snapshot(&prev_block_hash, &shard_uids).unwrap(); + runtime.get_tries().make_state_snapshot(&prev_block_hash, &shard_uids, &block).unwrap(); }); Some(snapshot) } else { @@ -1885,21 +1878,48 @@ impl TestEnv { pub fn process_partial_encoded_chunks(&mut self) { let network_adapters = self.network_adapters.clone(); - for network_adapter in network_adapters { - // process partial encoded chunks - while let Some(request) = network_adapter.pop() { - if let PeerManagerMessageRequest::NetworkRequests( - NetworkRequests::PartialEncodedChunkMessage { - account_id, - partial_encoded_chunk, - }, - ) = request - { - self.shards_manager(&account_id).send( - ShardsManagerRequestFromNetwork::ProcessPartialEncodedChunk( - PartialEncodedChunk::from(partial_encoded_chunk), - ), - ); + + let mut keep_going = true; + while keep_going { + for network_adapter in network_adapters.iter() { + keep_going = false; + // process partial encoded chunks + while let Some(request) = network_adapter.pop() { + // if there are any requests in any of the adapters reset + // keep going to true as processing of any message may + // trigger more messages to be processed in other clients + // it's a bit sad and it would be much nicer if all messages + // were forwarded to a single queue + // TODO would be nicer to first handle all PECs and then all PECFs + keep_going = true; + match request { + PeerManagerMessageRequest::NetworkRequests( + NetworkRequests::PartialEncodedChunkMessage { + account_id, + partial_encoded_chunk, + }, + ) => { + let partial_encoded_chunk = + PartialEncodedChunk::from(partial_encoded_chunk); + let message = + ShardsManagerRequestFromNetwork::ProcessPartialEncodedChunk( + partial_encoded_chunk, + ); + self.shards_manager(&account_id).send(message); + } + PeerManagerMessageRequest::NetworkRequests( + NetworkRequests::PartialEncodedChunkForward { account_id, forward }, + ) => { + let message = + ShardsManagerRequestFromNetwork::ProcessPartialEncodedChunkForward( + forward, + ); + self.shards_manager(&account_id).send(message); + } + _ => { + tracing::debug!(target: "test", ?request, "skipping unsupported request type"); + } + } } } } @@ -1994,6 +2014,9 @@ impl TestEnv { } pub fn process_shards_manager_responses_and_finish_processing_blocks(&mut self, idx: usize) { + let _span = + tracing::debug_span!(target: "test", "process_shards_manager", client=idx).entered(); + loop { self.process_shards_manager_responses(idx); if self.clients[idx].finish_blocks_in_processing().is_empty() { @@ -2188,7 +2211,7 @@ impl TestEnv { relayer, sender, &relayer_signer, - vec![Action::Delegate(signed_delegate_action)], + vec![Action::Delegate(Box::new(signed_delegate_action))], tip.last_block_hash, ) } @@ -2227,12 +2250,12 @@ impl TestEnv { /// deployed already. pub fn call_main(&mut self, account: &AccountId) -> FinalExecutionOutcomeView { let signer = InMemorySigner::from_seed(account.clone(), KeyType::ED25519, account.as_str()); - let actions = vec![Action::FunctionCall(FunctionCallAction { + let actions = vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "main".to_string(), args: vec![], gas: 3 * 10u64.pow(14), deposit: 0, - })]; + }))]; let tx = self.tx_from_actions(actions, &signer, signer.account_id.clone()); self.execute_tx(tx).unwrap() } diff --git a/chain/client/src/tests/process_blocks.rs b/chain/client/src/tests/process_blocks.rs index 0eb9ce1b16f..1cef4949002 100644 --- a/chain/client/src/tests/process_blocks.rs +++ b/chain/client/src/tests/process_blocks.rs @@ -1,7 +1,6 @@ use crate::test_utils::TestEnv; use assert_matches::assert_matches; use near_chain::{test_utils, ChainGenesis, Provenance}; -#[cfg(feature = "protocol_feature_block_header_v4")] use near_crypto::vrf::Value; use near_crypto::{KeyType, PublicKey, Signature}; use near_primitives::block::Block; @@ -80,11 +79,8 @@ fn test_bad_shard_id() { block.mut_header().get_mut().inner_rest.chunk_receipts_root = Block::compute_chunk_receipts_root(&chunks); block.set_chunks(chunks); - #[cfg(feature = "protocol_feature_block_header_v4")] - { - block.mut_header().get_mut().inner_rest.block_body_hash = - block.compute_block_body_hash().unwrap(); - } + block.mut_header().get_mut().inner_rest.block_body_hash = + block.compute_block_body_hash().unwrap(); block.mut_header().resign(&validator_signer); let err = env.clients[0] @@ -95,7 +91,6 @@ fn test_bad_shard_id() { /// Test that if a block's content (vrf_value) is corrupted, the invalid block will not affect the node's block processing #[test] -#[cfg(feature = "protocol_feature_block_header_v4")] fn test_bad_block_content_vrf() { let mut env = TestEnv::builder(ChainGenesis::test()).num_shards(4).build(); let prev_block = env.clients[0].produce_block(1).unwrap().unwrap(); diff --git a/chain/client/src/view_client.rs b/chain/client/src/view_client.rs index f549edab51f..7d623842c6e 100644 --- a/chain/client/src/view_client.rs +++ b/chain/client/src/view_client.rs @@ -540,6 +540,7 @@ impl Handler> for ViewClientActor { #[perf] fn handle(&mut self, msg: WithSpanContext, _: &mut Self::Context) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME.with_label_values(&["Query"]).start_timer(); self.handle_query(msg) } @@ -552,6 +553,7 @@ impl Handler> for ViewClientActor { #[perf] fn handle(&mut self, msg: WithSpanContext, _: &mut Self::Context) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME.with_label_values(&["GetBlock"]).start_timer(); let block = self.get_block_by_reference(&msg.0)?.ok_or(GetBlockError::NotSyncedYet)?; @@ -573,6 +575,7 @@ impl Handler> for ViewClientActor { ctx: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME .with_label_values(&["GetBlockWithMerkleTree"]) .start_timer(); @@ -591,6 +594,7 @@ impl Handler> for ViewClientActor { #[perf] fn handle(&mut self, msg: WithSpanContext, _: &mut Self::Context) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME.with_label_values(&["GetChunk"]).start_timer(); let get_chunk_from_block = |block: Block, @@ -648,6 +652,7 @@ impl Handler> for ViewClientActor { #[perf] fn handle(&mut self, msg: WithSpanContext, _: &mut Self::Context) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME.with_label_values(&["TxStatus"]).start_timer(); self.get_tx_status(msg.tx_hash, msg.signer_account_id, msg.fetch_receipt) @@ -664,6 +669,7 @@ impl Handler> for ViewClientActor { _: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME .with_label_values(&["GetValidatorInfo"]) .start_timer(); @@ -714,6 +720,7 @@ impl Handler> for ViewClientActor { _: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME .with_label_values(&["GetValidatorOrdered"]) .start_timer(); @@ -737,6 +744,7 @@ impl Handler> for ViewClientActor { _: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME .with_label_values(&["GetStateChangesInBlock"]) .start_timer(); @@ -761,6 +769,7 @@ impl Handler> for ViewClientActor { _: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME.with_label_values(&["GetStateChanges"]).start_timer(); Ok(self @@ -784,6 +793,7 @@ impl Handler> for ViewClientAct _: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME .with_label_values(&["GetStateChangesWithCauseInBlock"]) .start_timer(); @@ -809,6 +819,7 @@ impl Handler> f _: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME .with_label_values(&["GetStateChangesWithCauseInBlockForTrackedShards"]) .start_timer(); @@ -856,6 +867,7 @@ impl Handler> for ViewClientActor { _: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME .with_label_values(&["GetNextLightClientBlock"]) .start_timer(); @@ -903,6 +915,7 @@ impl Handler> for ViewClientActor { _: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME .with_label_values(&["GetExecutionOutcome"]) .start_timer(); @@ -992,6 +1005,7 @@ impl Handler> for ViewClientActor _: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME .with_label_values(&["GetExecutionOutcomesForBlock"]) .start_timer(); @@ -1012,6 +1026,7 @@ impl Handler> for ViewClientActor { #[perf] fn handle(&mut self, msg: WithSpanContext, _: &mut Self::Context) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME.with_label_values(&["GetReceipt"]).start_timer(); Ok(self @@ -1032,6 +1047,7 @@ impl Handler> for ViewClientActor { _: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME.with_label_values(&["GetBlockProof"]).start_timer(); let block_header = self.chain.get_block_header(&msg.block_hash)?; @@ -1053,6 +1069,7 @@ impl Handler> for ViewClientActor { _: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME .with_label_values(&["GetProtocolConfig"]) .start_timer(); @@ -1081,6 +1098,7 @@ impl Handler> for ViewClientActor { _ctx: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME .with_label_values(&["NetworkAdversarialMessage"]) .start_timer(); @@ -1118,6 +1136,7 @@ impl Handler> for ViewClientActor { _ctx: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME.with_label_values(&["TxStatusRequest"]).start_timer(); let TxStatusRequest { tx_hash, signer_account_id } = msg; @@ -1139,6 +1158,7 @@ impl Handler> for ViewClientActor { _ctx: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME .with_label_values(&["TxStatusResponse"]) .start_timer(); @@ -1161,6 +1181,7 @@ impl Handler> for ViewClientActor { _ctx: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME.with_label_values(&["BlockRequest"]).start_timer(); let BlockRequest(hash) = msg; @@ -1182,6 +1203,7 @@ impl Handler> for ViewClientActor { _ctx: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME .with_label_values(&["BlockHeadersRequest"]) .start_timer(); @@ -1207,6 +1229,7 @@ impl Handler> for ViewClientActor { _ctx: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME .with_label_values(&["StateRequestHeader"]) .start_timer(); @@ -1296,6 +1319,7 @@ impl Handler> for ViewClientActor { _ctx: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME .with_label_values(&["StateRequestPart"]) .start_timer(); @@ -1350,6 +1374,7 @@ impl Handler> for ViewClientActor { _ctx: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME .with_label_values(&["AnnounceAccountRequest"]) .start_timer(); @@ -1408,6 +1433,7 @@ impl Handler> for ViewClientActor { _ctx: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _timer = metrics::VIEW_CLIENT_MESSAGE_TIME.with_label_values(&["GetGasPrice"]).start_timer(); let header = self.maybe_block_id_to_block_header(msg.block_id); @@ -1425,6 +1451,7 @@ impl Handler> for ViewClientActor { _ctx: &mut Self::Context, ) -> Self::Result { let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); Ok(self.get_maintenance_windows(msg.account_id)?) } } @@ -1437,7 +1464,8 @@ impl Handler> for ViewClientActor { msg: WithSpanContext, _: &mut Self::Context, ) -> Self::Result { - let (_span, _msg) = handler_debug_span!(target: "client", msg); + let (_span, msg) = handler_debug_span!(target: "client", msg); + tracing::debug!(target: "client", ?msg); let _d = delay_detector::DelayDetector::new(|| "client get split storage info".into()); let store = self.chain.store().store(); diff --git a/chain/epoch-manager/Cargo.toml b/chain/epoch-manager/Cargo.toml index 5b48ff2748e..181abc8a44f 100644 --- a/chain/epoch-manager/Cargo.toml +++ b/chain/epoch-manager/Cargo.toml @@ -18,6 +18,8 @@ rand_hc.workspace = true serde_json.workspace = true smart-default.workspace = true tracing.workspace = true +# itertools has collect_vec which is useful in quick debugging prints +itertools.workspace = true near-crypto.workspace = true near-primitives.workspace = true @@ -28,7 +30,9 @@ near-cache.workspace = true [features] expensive_tests = [] -protocol_feature_fix_staking_threshold = ["near-primitives/protocol_feature_fix_staking_threshold"] +protocol_feature_fix_staking_threshold = [ + "near-primitives/protocol_feature_fix_staking_threshold", +] nightly = [ "nightly_protocol", "protocol_feature_fix_staking_threshold", @@ -43,3 +47,4 @@ nightly_protocol = [ "near-store/nightly_protocol", ] no_cache = [] +new_epoch_sync = ["near-store/new_epoch_sync", "near-primitives/new_epoch_sync"] diff --git a/chain/epoch-manager/src/adapter.rs b/chain/epoch-manager/src/adapter.rs index a059ff11c74..5df2deddf8d 100644 --- a/chain/epoch-manager/src/adapter.rs +++ b/chain/epoch-manager/src/adapter.rs @@ -343,7 +343,7 @@ pub trait EpochManagerAdapter: Send + Sync { prev_block_hash: &CryptoHash, prev_block_height: BlockHeight, block_height: BlockHeight, - approvals: &[Option], + approvals: &[Option>], ) -> Result; /// Verify approvals and check threshold, but ignore next epoch approvals and slashing @@ -351,14 +351,14 @@ pub trait EpochManagerAdapter: Send + Sync { &self, epoch_id: &EpochId, can_approved_block_be_produced: &dyn Fn( - &[Option], + &[Option>], // (stake this in epoch, stake in next epoch, is_slashed) &[(Balance, Balance, bool)], ) -> bool, prev_block_hash: &CryptoHash, prev_block_height: BlockHeight, block_height: BlockHeight, - approvals: &[Option], + approvals: &[Option>], ) -> Result<(), Error>; fn cares_about_shard_from_prev_block( @@ -838,7 +838,7 @@ impl EpochManagerAdapter for EpochManagerHandle { prev_block_hash: &CryptoHash, prev_block_height: BlockHeight, block_height: BlockHeight, - approvals: &[Option], + approvals: &[Option>], ) -> Result { let info = { let epoch_manager = self.read(); @@ -874,13 +874,13 @@ impl EpochManagerAdapter for EpochManagerHandle { &self, epoch_id: &EpochId, can_approved_block_be_produced: &dyn Fn( - &[Option], + &[Option>], &[(Balance, Balance, bool)], ) -> bool, prev_block_hash: &CryptoHash, prev_block_height: BlockHeight, block_height: BlockHeight, - approvals: &[Option], + approvals: &[Option>], ) -> Result<(), Error> { let info = { let epoch_manager = self.read(); diff --git a/chain/epoch-manager/src/lib.rs b/chain/epoch-manager/src/lib.rs index ebec225b0d0..710933d84b5 100644 --- a/chain/epoch-manager/src/lib.rs +++ b/chain/epoch-manager/src/lib.rs @@ -325,7 +325,6 @@ impl EpochManager { /// we don't kick out too many validators in case of network instability. /// We also make sure that these exempted validators were not kicked out in the last epoch, /// so it is guaranteed that they will stay as validators after this epoch. - #[allow(unused_variables)] fn compute_exempted_kickout( epoch_info: &EpochInfo, validator_block_chunk_stats: &HashMap, @@ -340,7 +339,6 @@ impl EpochManager { // until the total excepted stake exceeds the ratio of total stake that we need to keep. // Later when we perform the check to kick out validators, we don't kick out validators in // exempted_validators. - #[allow(unused_mut)] let mut exempted_validators = HashSet::new(); if checked_feature!("stable", MaxKickoutStake, epoch_info.protocol_version()) { let min_keep_stake = total_stake * (exempt_perc as u128) / 100; @@ -420,7 +418,6 @@ impl EpochManager { let block_producer_kickout_threshold = config.block_producer_kickout_threshold; let chunk_producer_kickout_threshold = config.chunk_producer_kickout_threshold; let mut validator_block_chunk_stats = HashMap::new(); - #[allow(unused)] let mut total_stake: Balance = 0; let mut maximum_block_prod = 0; let mut max_validator = None; @@ -558,11 +555,10 @@ impl EpochManager { let next_version = if let Some((version, stake)) = versions.into_iter().max_by_key(|&(_version, stake)| stake) { - if stake - > (total_block_producer_stake - * *config.protocol_upgrade_stake_threshold.numer() as u128) - / *config.protocol_upgrade_stake_threshold.denom() as u128 - { + let numer = *config.protocol_upgrade_stake_threshold.numer() as u128; + let denom = *config.protocol_upgrade_stake_threshold.denom() as u128; + let threshold = total_block_producer_stake * numer / denom; + if stake > threshold { version } else { protocol_version @@ -570,7 +566,6 @@ impl EpochManager { } else { protocol_version }; - // Gather slashed validators and add them to kick out first. let slashed_validators = last_block_info.slashed(); for (account_id, _) in slashed_validators.iter() { diff --git a/chain/epoch-manager/src/shard_tracker.rs b/chain/epoch-manager/src/shard_tracker.rs index eb6dea893c9..2cbf5a71caf 100644 --- a/chain/epoch-manager/src/shard_tracker.rs +++ b/chain/epoch-manager/src/shard_tracker.rs @@ -250,7 +250,6 @@ mod tests { .into_handle() } - #[allow(unused)] pub fn record_block( epoch_manager: &mut EpochManager, prev_h: CryptoHash, diff --git a/chain/epoch-manager/src/tests/mod.rs b/chain/epoch-manager/src/tests/mod.rs index 94e4efbba3b..0e1faec9eb1 100644 --- a/chain/epoch-manager/src/tests/mod.rs +++ b/chain/epoch-manager/src/tests/mod.rs @@ -2507,7 +2507,6 @@ fn test_validator_kickout_sanity() { #[test] /// Test that the stake of validators kicked out in an epoch doesn't exceed the max_kickout_stake_ratio fn test_max_kickout_stake_ratio() { - #[allow(unused_mut)] let mut epoch_config = epoch_config(5, 2, 4, 0, 90, 80, 0).for_protocol_version(PROTOCOL_VERSION); let accounts = vec![ diff --git a/chain/indexer/src/streamer/mod.rs b/chain/indexer/src/streamer/mod.rs index 4e624d578e1..a823d54669b 100644 --- a/chain/indexer/src/streamer/mod.rs +++ b/chain/indexer/src/streamer/mod.rs @@ -2,7 +2,6 @@ use std::time::Duration; use actix::Addr; use async_recursion::async_recursion; -use near_indexer_primitives::types::ProtocolVersion; use node_runtime::config::RuntimeConfig; use rocksdb::DB; use tokio::sync::mpsc; @@ -128,7 +127,6 @@ async fn build_streamer_message( let chunk_local_receipts = convert_transactions_sir_into_local_receipts( &client, &runtime_config, - protocol_config_view.protocol_version, indexer_transactions .iter() .filter(|tx| tx.transaction.signer_id == tx.transaction.receiver_id) @@ -175,7 +173,6 @@ async fn build_streamer_message( if let Some(receipt) = find_local_receipt_by_id_in_block( &client, &runtime_config, - protocol_config_view.protocol_version, prev_block, execution_outcome.id, ) @@ -188,7 +185,7 @@ async fn build_streamer_message( } }; receipt_execution_outcomes - .push(IndexerExecutionOutcomeWithReceipt { execution_outcome, receipt: receipt }); + .push(IndexerExecutionOutcomeWithReceipt { execution_outcome, receipt }); } // Blocks #47317863 and #47317864 @@ -246,7 +243,6 @@ async fn build_streamer_message( async fn find_local_receipt_by_id_in_block( client: &Addr, runtime_config: &RuntimeConfig, - protocol_version: ProtocolVersion, block: views::BlockView, receipt_id: near_primitives::hash::CryptoHash, ) -> Result, FailedToFetchData> { @@ -276,7 +272,6 @@ async fn find_local_receipt_by_id_in_block( let local_receipts = convert_transactions_sir_into_local_receipts( &client, &runtime_config, - protocol_version, vec![&indexer_transaction], &block, ) diff --git a/chain/indexer/src/streamer/utils.rs b/chain/indexer/src/streamer/utils.rs index 5a1e92c6c25..6d3bbcd8d22 100644 --- a/chain/indexer/src/streamer/utils.rs +++ b/chain/indexer/src/streamer/utils.rs @@ -1,6 +1,5 @@ use actix::Addr; -use near_indexer_primitives::types::ProtocolVersion; use near_indexer_primitives::IndexerTransactionWithOutcome; use near_primitives::views; use node_runtime::config::{tx_cost, RuntimeConfig}; @@ -11,7 +10,6 @@ use super::fetchers::fetch_block; pub(crate) async fn convert_transactions_sir_into_local_receipts( client: &Addr, runtime_config: &RuntimeConfig, - protocol_version: ProtocolVersion, txs: Vec<&IndexerTransactionWithOutcome>, block: &views::BlockView, ) -> Result, FailedToFetchData> { @@ -25,7 +23,7 @@ pub(crate) async fn convert_transactions_sir_into_local_receipts( txs.into_iter() .map(|tx| { let cost = tx_cost( - &runtime_config.fees, + &runtime_config, &near_primitives::transaction::Transaction { signer_id: tx.transaction.signer_id.clone(), public_key: tx.transaction.public_key.clone(), @@ -44,7 +42,6 @@ pub(crate) async fn convert_transactions_sir_into_local_receipts( }, prev_block_gas_price, true, - protocol_version, ); views::ReceiptView { predecessor_id: tx.transaction.signer_id.clone(), diff --git a/chain/jsonrpc/jsonrpc-tests/res/genesis_config.json b/chain/jsonrpc/jsonrpc-tests/res/genesis_config.json index abee9f6545a..e9c7e32d6cd 100644 --- a/chain/jsonrpc/jsonrpc-tests/res/genesis_config.json +++ b/chain/jsonrpc/jsonrpc-tests/res/genesis_config.json @@ -1,5 +1,5 @@ { - "protocol_version": 62, + "protocol_version": 63, "genesis_time": "1970-01-01T00:00:00.000000000Z", "chain_id": "sample", "genesis_height": 0, @@ -69,4 +69,4 @@ ], "use_production_config": false, "records": [] -} +} \ No newline at end of file diff --git a/chain/jsonrpc/jsonrpc-tests/src/lib.rs b/chain/jsonrpc/jsonrpc-tests/src/lib.rs index fa060c029b1..40ced21b0d3 100644 --- a/chain/jsonrpc/jsonrpc-tests/src/lib.rs +++ b/chain/jsonrpc/jsonrpc-tests/src/lib.rs @@ -57,7 +57,6 @@ pub fn start_all_with_validity_period_and_no_epoch_sync( } #[macro_export] -#[allow(unused_macros)] // Suppress Rustc warnings even though this macro is used. macro_rules! test_with_client { ($node_type:expr, $client:ident, $block:expr) => { init_test_logger(); diff --git a/chain/jsonrpc/res/chain_n_chunk_info.css b/chain/jsonrpc/res/chain_n_chunk_info.css new file mode 100644 index 00000000000..eff1995f34c --- /dev/null +++ b/chain/jsonrpc/res/chain_n_chunk_info.css @@ -0,0 +1,27 @@ +table { + width: 100%; + border-collapse: collapse; +} + +table, +th, +td { + border: 1px solid black; +} + +td { + text-align: left; + vertical-align: top; + padding: 8px; +} + +th { + text-align: center; + vertical-align: center; + padding: 8px; + background-color: lightgrey; +} + +tr.active { + background-color: #eff8bf; +} \ No newline at end of file diff --git a/chain/jsonrpc/res/chain_n_chunk_info.html b/chain/jsonrpc/res/chain_n_chunk_info.html index 2c422a22184..22f58fb788e 100644 --- a/chain/jsonrpc/res/chain_n_chunk_info.html +++ b/chain/jsonrpc/res/chain_n_chunk_info.html @@ -1,34 +1,6 @@ - + diff --git a/chain/jsonrpc/res/sync.css b/chain/jsonrpc/res/sync.css new file mode 100644 index 00000000000..eff1995f34c --- /dev/null +++ b/chain/jsonrpc/res/sync.css @@ -0,0 +1,27 @@ +table { + width: 100%; + border-collapse: collapse; +} + +table, +th, +td { + border: 1px solid black; +} + +td { + text-align: left; + vertical-align: top; + padding: 8px; +} + +th { + text-align: center; + vertical-align: center; + padding: 8px; + background-color: lightgrey; +} + +tr.active { + background-color: #eff8bf; +} \ No newline at end of file diff --git a/chain/jsonrpc/res/sync.html b/chain/jsonrpc/res/sync.html index d07b1bf9dd5..5a81028d349 100644 --- a/chain/jsonrpc/res/sync.html +++ b/chain/jsonrpc/res/sync.html @@ -1,35 +1,7 @@ - + diff --git a/chain/jsonrpc/src/lib.rs b/chain/jsonrpc/src/lib.rs index a29e48f8034..4aedfcfb71e 100644 --- a/chain/jsonrpc/src/lib.rs +++ b/chain/jsonrpc/src/lib.rs @@ -297,9 +297,6 @@ impl JsonRpcHandler { process_method_call(request, |params| self.next_light_client_block(params)).await } "network_info" => process_method_call(request, |_params: ()| self.network_info()).await, - "tier1_network_info" => { - process_method_call(request, |_params: ()| self.tier1_network_info()).await - } "query" => { let params = RpcRequest::parse(request.params)?; let query_response = self.query(params).await; @@ -1024,16 +1021,6 @@ impl JsonRpcHandler { Ok(network_info.rpc_into()) } - async fn tier1_network_info( - &self, - ) -> Result< - near_jsonrpc_primitives::types::network_info::RpcNetworkInfoResponse, - near_jsonrpc_primitives::types::network_info::RpcNetworkInfoError, - > { - let network_info = self.client_send(GetNetworkInfo {}).await?; - Ok(network_info.rpc_into()) - } - async fn gas_price( &self, request_data: near_jsonrpc_primitives::types::gas_price::RpcGasPriceRequest, @@ -1423,18 +1410,6 @@ fn network_info_handler( response.boxed() } -fn tier1_network_info_handler( - handler: web::Data, -) -> impl Future> { - let response = async move { - match handler.tier1_network_info().await { - Ok(value) => Ok(HttpResponse::Ok().json(&value)), - Err(_) => Ok(HttpResponse::ServiceUnavailable().finish()), - } - }; - response.boxed() -} - pub async fn prometheus_handler() -> Result { metrics::PROMETHEUS_REQUEST_COUNT.inc(); @@ -1576,10 +1551,6 @@ pub fn start_http( .route(web::head().to(health_handler)), ) .service(web::resource("/network_info").route(web::get().to(network_info_handler))) - .service( - web::resource("/tier1_network_info") - .route(web::get().to(tier1_network_info_handler)), - ) .service(web::resource("/metrics").route(web::get().to(prometheus_handler))) .service(web::resource("/debug/api/entity").route(web::post().to(handle_entity_debug))) .service(web::resource("/debug/api/{api}").route(web::get().to(debug_handler))) diff --git a/chain/network/src/config_json.rs b/chain/network/src/config_json.rs index d225018525c..4eeb4b8d117 100644 --- a/chain/network/src/config_json.rs +++ b/chain/network/src/config_json.rs @@ -197,10 +197,8 @@ pub struct Config { fn default_tier1_enable_inbound() -> bool { true } -/// This default will be changed over the next releases. -/// It allows us to gradually roll out the TIER1 feature. fn default_tier1_enable_outbound() -> bool { - false + true } fn default_tier1_connect_interval() -> Duration { diff --git a/chain/network/src/network_protocol/borsh.rs b/chain/network/src/network_protocol/borsh.rs index 9e7d3a4c5e6..32d3f8d1cff 100644 --- a/chain/network/src/network_protocol/borsh.rs +++ b/chain/network/src/network_protocol/borsh.rs @@ -4,13 +4,14 @@ //! WARNING WARNING WARNING //! We need to maintain backwards compatibility, all changes to this file needs to be reviews. use crate::network_protocol::edge::{Edge, PartialEdgeInfo}; -use crate::network_protocol::{PeerChainInfoV2, PeerInfo, RoutedMessage}; +use crate::network_protocol::{PeerChainInfoV2, PeerInfo, RoutedMessage, StateResponseInfo}; use borsh::{BorshDeserialize, BorshSerialize}; use near_primitives::block::{Block, BlockHeader, GenesisId}; use near_primitives::challenge::Challenge; use near_primitives::hash::CryptoHash; use near_primitives::network::{AnnounceAccount, PeerId}; use near_primitives::transaction::SignedTransaction; +use near_primitives::types::ShardId; use std::fmt; use std::fmt::Formatter; @@ -119,7 +120,6 @@ impl std::error::Error for HandshakeFailureReason {} /// If need to remove old items - replace with `None`. #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Clone, Debug, strum::AsRefStr)] // TODO(#1313): Use Box -#[allow(clippy::large_enum_variant)] pub(super) enum PeerMessage { Handshake(Handshake), HandshakeFailure(PeerInfo, HandshakeFailureReason), @@ -154,6 +154,10 @@ pub(super) enum PeerMessage { _RoutingTableSyncV2, DistanceVector(DistanceVector), + + StateRequestHeader(ShardId, CryptoHash), + StateRequestPart(ShardId, CryptoHash, u64), + VersionedStateResponse(StateResponseInfo), } #[cfg(target_arch = "x86_64")] // Non-x86_64 doesn't match this requirement yet but it's not bad as it's not production-ready const _: () = assert!(std::mem::size_of::() <= 1144, "PeerMessage > 1144 bytes"); diff --git a/chain/network/src/network_protocol/borsh_conv.rs b/chain/network/src/network_protocol/borsh_conv.rs index 4eca252bb0e..0d37a661ab9 100644 --- a/chain/network/src/network_protocol/borsh_conv.rs +++ b/chain/network/src/network_protocol/borsh_conv.rs @@ -193,6 +193,15 @@ impl TryFrom<&net::PeerMessage> for mem::PeerMessage { return Err(Self::Error::DeprecatedRoutingTableSyncV2) } net::PeerMessage::DistanceVector(dv) => mem::PeerMessage::DistanceVector(dv.into()), + net::PeerMessage::StateRequestHeader(shard_id, sync_hash) => { + mem::PeerMessage::StateRequestHeader(shard_id, sync_hash) + } + net::PeerMessage::StateRequestPart(shard_id, sync_hash, part_id) => { + mem::PeerMessage::StateRequestPart(shard_id, sync_hash, part_id) + } + net::PeerMessage::VersionedStateResponse(sri) => { + mem::PeerMessage::VersionedStateResponse(sri) + } }) } } @@ -233,6 +242,15 @@ impl From<&mem::PeerMessage> for net::PeerMessage { mem::PeerMessage::Routed(r) => net::PeerMessage::Routed(Box::new(r.msg.clone())), mem::PeerMessage::Disconnect(_) => net::PeerMessage::Disconnect, mem::PeerMessage::Challenge(c) => net::PeerMessage::Challenge(c), + mem::PeerMessage::StateRequestHeader(shard_id, sync_hash) => { + net::PeerMessage::StateRequestHeader(shard_id, sync_hash) + } + mem::PeerMessage::StateRequestPart(shard_id, sync_hash, part_id) => { + net::PeerMessage::StateRequestPart(shard_id, sync_hash, part_id) + } + mem::PeerMessage::VersionedStateResponse(sri) => { + net::PeerMessage::VersionedStateResponse(sri) + } } } } diff --git a/chain/network/src/network_protocol/mod.rs b/chain/network/src/network_protocol/mod.rs index a9ec99db71b..03a46de6af7 100644 --- a/chain/network/src/network_protocol/mod.rs +++ b/chain/network/src/network_protocol/mod.rs @@ -407,6 +407,10 @@ pub enum PeerMessage { /// Gracefully disconnect from other peer. Disconnect(Disconnect), Challenge(Challenge), + + StateRequestHeader(ShardId, CryptoHash), + StateRequestPart(ShardId, CryptoHash, u64), + VersionedStateResponse(StateResponseInfo), } impl fmt::Display for PeerMessage { @@ -504,8 +508,8 @@ pub enum RoutedMessageBody { /// Not used, but needed to borsh backward compatibility. _UnusedReceiptOutcomeResponse, - StateRequestHeader(ShardId, CryptoHash), - StateRequestPart(ShardId, CryptoHash, u64), + _UnusedStateRequestHeader, + _UnusedStateRequestPart, /// StateResponse in not produced since protocol version 58. /// We can remove the support for it in protocol version 60. /// It has been obsoleted by VersionedStateResponse which @@ -518,7 +522,7 @@ pub enum RoutedMessageBody { Ping(Ping), Pong(Pong), VersionedPartialEncodedChunk(PartialEncodedChunk), - VersionedStateResponse(StateResponseInfo), + _UnusedVersionedStateResponse, PartialEncodedChunkForward(PartialEncodedChunkForwardMsg), } @@ -557,12 +561,8 @@ impl fmt::Debug for RoutedMessageBody { RoutedMessageBody::_UnusedQueryResponse => write!(f, "QueryResponse"), RoutedMessageBody::ReceiptOutcomeRequest(hash) => write!(f, "ReceiptRequest({})", hash), RoutedMessageBody::_UnusedReceiptOutcomeResponse => write!(f, "ReceiptResponse"), - RoutedMessageBody::StateRequestHeader(shard_id, sync_hash) => { - write!(f, "StateRequestHeader({}, {})", shard_id, sync_hash) - } - RoutedMessageBody::StateRequestPart(shard_id, sync_hash, part_id) => { - write!(f, "StateRequestPart({}, {}, {})", shard_id, sync_hash, part_id) - } + RoutedMessageBody::_UnusedStateRequestHeader => write!(f, "StateRequestHeader"), + RoutedMessageBody::_UnusedStateRequestPart => write!(f, "StateRequestPart"), RoutedMessageBody::StateResponse(response) => { write!(f, "StateResponse({}, {})", response.shard_id, response.sync_hash) } @@ -579,12 +579,6 @@ impl fmt::Debug for RoutedMessageBody { RoutedMessageBody::VersionedPartialEncodedChunk(_) => { write!(f, "VersionedPartialEncodedChunk(?)") } - RoutedMessageBody::VersionedStateResponse(response) => write!( - f, - "VersionedStateResponse({}, {})", - response.shard_id(), - response.sync_hash() - ), RoutedMessageBody::PartialEncodedChunkForward(forward) => write!( f, "PartialChunkForward({:?}, {:?})", @@ -593,6 +587,7 @@ impl fmt::Debug for RoutedMessageBody { ), RoutedMessageBody::Ping(_) => write!(f, "Ping"), RoutedMessageBody::Pong(_) => write!(f, "Pong"), + RoutedMessageBody::_UnusedVersionedStateResponse => write!(f, "VersionedStateResponse"), } } } @@ -675,8 +670,6 @@ impl RoutedMessage { self.body, RoutedMessageBody::Ping(_) | RoutedMessageBody::TxStatusRequest(_, _) - | RoutedMessageBody::StateRequestHeader(_, _) - | RoutedMessageBody::StateRequestPart(_, _, _) | RoutedMessageBody::PartialEncodedChunkRequest(_) | RoutedMessageBody::ReceiptOutcomeRequest(_) ) diff --git a/chain/network/src/network_protocol/network.proto b/chain/network/src/network_protocol/network.proto index 84f135c176c..08af7c2b7d4 100644 --- a/chain/network/src/network_protocol/network.proto +++ b/chain/network/src/network_protocol/network.proto @@ -106,6 +106,11 @@ message BlockHeader { bytes borsh = 1; } +// Wrapper of the borsh-encoded StateResponseInfo. +message StateResponseInfo { + bytes borsh = 1; +} + // Unique identifier of the NEAR chain. message GenesisId { // Name of the chain (for example "mainnet"). @@ -409,6 +414,21 @@ message TraceContext { SamplingPriority sampling_priority = 3; } +message StateRequestHeader { + uint64 shard_id = 1; + CryptoHash sync_hash = 2; +} + +message StateRequestPart { + uint64 shard_id = 1; + CryptoHash sync_hash = 2; + uint64 part_id = 3; +} + +message StateResponse { + StateResponseInfo state_response_info = 1; +} + // PeerMessage is a wrapper of all message types exchanged between NEAR nodes. // The wire format of a single message M consists of len(M)+4 bytes: // : 4 bytes : little endian uint32 @@ -460,5 +480,9 @@ message PeerMessage { RoutedMessage routed = 17; Disconnect disconnect = 18; Challenge challenge = 19; + + StateRequestHeader state_request_header = 29; + StateRequestPart state_request_part = 30; + StateResponse state_response = 31; } } 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 3af21a855ce..e82a7f8c38b 100644 --- a/chain/network/src/network_protocol/proto_conv/peer_message.rs +++ b/chain/network/src/network_protocol/proto_conv/peer_message.rs @@ -1,13 +1,14 @@ /// Conversion functions for PeerMessage - the top-level message for the NEAR P2P protocol format. use super::*; -use crate::network_protocol::proto; use crate::network_protocol::proto::peer_message::Message_type as ProtoMT; +use crate::network_protocol::proto::{self}; use crate::network_protocol::{ AdvertisedPeerDistance, Disconnect, DistanceVector, PeerMessage, PeersRequest, PeersResponse, RoutingTableUpdate, SyncAccountsData, }; use crate::network_protocol::{RoutedMessage, RoutedMessageV2}; +use crate::types::StateResponseInfo; use borsh::{BorshDeserialize as _, BorshSerialize as _}; use near_async::time::error::ComponentRange; use near_primitives::block::{Block, BlockHeader}; @@ -144,6 +145,23 @@ impl TryFrom<&proto::Block> for Block { ////////////////////////////////////////// +impl From<&StateResponseInfo> for proto::StateResponseInfo { + fn from(x: &StateResponseInfo) -> Self { + Self { borsh: x.try_to_vec().unwrap(), ..Default::default() } + } +} + +pub type ParseStateInfoError = borsh::maybestd::io::Error; + +impl TryFrom<&proto::StateResponseInfo> for StateResponseInfo { + type Error = ParseStateInfoError; + fn try_from(x: &proto::StateResponseInfo) -> Result { + Self::try_from_slice(&x.borsh) + } +} + +////////////////////////////////////////// + impl From<&PeerMessage> for proto::PeerMessage { fn from(x: &PeerMessage) -> Self { Self { @@ -225,6 +243,27 @@ impl From<&PeerMessage> for proto::PeerMessage { borsh: r.try_to_vec().unwrap(), ..Default::default() }), + PeerMessage::StateRequestHeader(shard_id, sync_hash) => { + ProtoMT::StateRequestHeader(proto::StateRequestHeader { + shard_id: *shard_id, + sync_hash: MF::some(sync_hash.into()), + ..Default::default() + }) + } + PeerMessage::StateRequestPart(shard_id, sync_hash, part_id) => { + ProtoMT::StateRequestPart(proto::StateRequestPart { + shard_id: *shard_id, + sync_hash: MF::some(sync_hash.into()), + part_id: *part_id, + ..Default::default() + }) + } + PeerMessage::VersionedStateResponse(sri) => { + ProtoMT::StateResponse(proto::StateResponse { + state_response_info: MF::some(sri.into()), + ..Default::default() + }) + } }), ..Default::default() } @@ -276,6 +315,8 @@ pub enum ParsePeerMessageError { RoutedCreatedAtTimestamp(ComponentRange), #[error("sync_accounts_data: {0}")] SyncAccountsData(ParseVecError), + #[error("state_response: {0}")] + StateResponse(ParseRequiredError), } impl TryFrom<&proto::PeerMessage> for PeerMessage { @@ -362,6 +403,18 @@ impl TryFrom<&proto::PeerMessage> for PeerMessage { ProtoMT::Challenge(c) => PeerMessage::Challenge( Challenge::try_from_slice(&c.borsh).map_err(Self::Error::Challenge)?, ), + ProtoMT::StateRequestHeader(srh) => PeerMessage::StateRequestHeader( + srh.shard_id, + try_from_required(&srh.sync_hash).map_err(Self::Error::BlockRequest)?, + ), + ProtoMT::StateRequestPart(srp) => PeerMessage::StateRequestPart( + srp.shard_id, + try_from_required(&srp.sync_hash).map_err(Self::Error::BlockRequest)?, + srp.part_id, + ), + ProtoMT::StateResponse(t) => PeerMessage::VersionedStateResponse( + try_from_required(&t.state_response_info).map_err(Self::Error::StateResponse)?, + ), }) } } diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index 24de627b1de..f6df7880631 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -917,20 +917,6 @@ impl PeerActor { network_state.client.tx_status_response(tx_result).await; None } - RoutedMessageBody::StateRequestHeader(shard_id, sync_hash) => network_state - .client - .state_request_header(shard_id, sync_hash) - .await? - .map(RoutedMessageBody::VersionedStateResponse), - RoutedMessageBody::StateRequestPart(shard_id, sync_hash, part_id) => network_state - .client - .state_request_part(shard_id, sync_hash, part_id) - .await? - .map(RoutedMessageBody::VersionedStateResponse), - RoutedMessageBody::VersionedStateResponse(info) => { - network_state.client.state_response(info).await; - None - } RoutedMessageBody::StateResponse(info) => { network_state.client.state_response(StateResponseInfo::V1(info)).await; None @@ -1057,6 +1043,21 @@ impl PeerActor { network_state.client.challenge(challenge).await; None } + PeerMessage::StateRequestHeader(shard_id, sync_hash) => network_state + .client + .state_request_header(shard_id, sync_hash) + .await? + .map(PeerMessage::VersionedStateResponse), + PeerMessage::StateRequestPart(shard_id, sync_hash, part_id) => network_state + .client + .state_request_part(shard_id, sync_hash, part_id) + .await? + .map(PeerMessage::VersionedStateResponse), + PeerMessage::VersionedStateResponse(info) => { + //TODO: Route to state sync actor. + network_state.client.state_response(info).await; + None + } msg => { tracing::error!(target: "network", "Peer received unexpected type: {:?}", msg); None diff --git a/chain/network/src/peer_manager/peer_manager_actor.rs b/chain/network/src/peer_manager/peer_manager_actor.rs index 13b05ccd16f..1b3cc7cea2d 100644 --- a/chain/network/src/peer_manager/peer_manager_actor.rs +++ b/chain/network/src/peer_manager/peer_manager_actor.rs @@ -646,27 +646,6 @@ impl PeerManagerActor { } } - /// Return whether the message is sent or not. - fn send_message_to_account_or_peer_or_hash( - &mut self, - target: &AccountOrPeerIdOrHash, - msg: RoutedMessageBody, - ) -> bool { - let target = match target { - AccountOrPeerIdOrHash::AccountId(account_id) => { - return self.state.send_message_to_account(&self.clock, account_id, msg); - } - AccountOrPeerIdOrHash::PeerId(it) => PeerIdOrHash::PeerId(it.clone()), - AccountOrPeerIdOrHash::Hash(it) => PeerIdOrHash::Hash(*it), - }; - - self.state.send_message_to_peer( - &self.clock, - tcp::Tier::T2, - self.state.sign_message(&self.clock, RawRoutedMessage { target, body: msg }), - ) - } - pub(crate) fn get_network_info(&self) -> NetworkInfo { let tier1 = self.state.tier1.load(); let tier2 = self.state.tier2.load(); @@ -789,9 +768,14 @@ impl PeerManagerActor { } } NetworkRequests::StateRequestHeader { shard_id, sync_hash, target } => { - if self.send_message_to_account_or_peer_or_hash( - &target, - RoutedMessageBody::StateRequestHeader(shard_id, sync_hash), + let peer_id = match target { + AccountOrPeerIdOrHash::AccountId(_) => return NetworkResponses::RouteNotFound, + AccountOrPeerIdOrHash::PeerId(peer_id) => peer_id, + AccountOrPeerIdOrHash::Hash(_) => return NetworkResponses::RouteNotFound, + }; + if self.state.tier2.send_message( + peer_id, + Arc::new(PeerMessage::StateRequestHeader(shard_id, sync_hash)), ) { NetworkResponses::NoResponse } else { @@ -799,24 +783,14 @@ impl PeerManagerActor { } } NetworkRequests::StateRequestPart { shard_id, sync_hash, part_id, target } => { - if self.send_message_to_account_or_peer_or_hash( - &target, - RoutedMessageBody::StateRequestPart(shard_id, sync_hash, part_id), - ) { - NetworkResponses::NoResponse - } else { - NetworkResponses::RouteNotFound - } - } - NetworkRequests::StateResponse { route_back, response } => { - let body = RoutedMessageBody::VersionedStateResponse(response); - if self.state.send_message_to_peer( - &self.clock, - tcp::Tier::T2, - self.state.sign_message( - &self.clock, - RawRoutedMessage { target: PeerIdOrHash::Hash(route_back), body }, - ), + let peer_id = match target { + AccountOrPeerIdOrHash::AccountId(_) => return NetworkResponses::RouteNotFound, + AccountOrPeerIdOrHash::PeerId(peer_id) => peer_id, + AccountOrPeerIdOrHash::Hash(_) => return NetworkResponses::RouteNotFound, + }; + if self.state.tier2.send_message( + peer_id, + Arc::new(PeerMessage::StateRequestPart(shard_id, sync_hash, part_id)), ) { NetworkResponses::NoResponse } else { diff --git a/chain/network/src/peer_manager/tests/routing.rs b/chain/network/src/peer_manager/tests/routing.rs index 107224406ab..cf2727c1b79 100644 --- a/chain/network/src/peer_manager/tests/routing.rs +++ b/chain/network/src/peer_manager/tests/routing.rs @@ -787,7 +787,7 @@ async fn max_num_peers_limit() { let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); tracing::info!(target:"test", "start three nodes with max_num_peers=2"); - let mut cfgs = make_configs(&chain, rng, 4, 4, true); + let mut cfgs = make_configs(&chain, rng, 4, 4, false); for config in cfgs.iter_mut() { config.max_num_peers = 2; config.ideal_connections_lo = 2; @@ -802,6 +802,10 @@ async fn max_num_peers_limit() { let id1 = pm1.cfg.node_id(); let id2 = pm2.cfg.node_id(); + pm0.connect_to(&pm1.peer_info(), tcp::Tier::T2).await; + pm0.connect_to(&pm2.peer_info(), tcp::Tier::T2).await; + pm1.connect_to(&pm2.peer_info(), tcp::Tier::T2).await; + tracing::info!(target:"test", "wait for {id0} routing table"); pm0.wait_for_routing_table(&[ (id1.clone(), vec![id1.clone()]), @@ -831,18 +835,21 @@ async fn max_num_peers_limit() { let id3 = pm3.cfg.node_id(); tracing::info!(target:"test", "wait for {id0} to reject attempted connection"); + pm3.send_outbound_connect(&pm0.peer_info(), tcp::Tier::T2).await; wait_for_connection_closed( &mut pm0_ev, ClosingReason::RejectedByPeerManager(RegisterPeerError::ConnectionLimitExceeded), ) .await; tracing::info!(target:"test", "wait for {id1} to reject attempted connection"); + pm3.send_outbound_connect(&pm1.peer_info(), tcp::Tier::T2).await; wait_for_connection_closed( &mut pm1_ev, ClosingReason::RejectedByPeerManager(RegisterPeerError::ConnectionLimitExceeded), ) .await; tracing::info!(target:"test", "wait for {id2} to reject attempted connection"); + pm3.send_outbound_connect(&pm2.peer_info(), tcp::Tier::T2).await; wait_for_connection_closed( &mut pm2_ev, ClosingReason::RejectedByPeerManager(RegisterPeerError::ConnectionLimitExceeded), diff --git a/chain/network/src/raw/connection.rs b/chain/network/src/raw/connection.rs index 2e1cfae2b47..1d589da2bdd 100644 --- a/chain/network/src/raw/connection.rs +++ b/chain/network/src/raw/connection.rs @@ -45,8 +45,6 @@ pub struct Connection { pub enum RoutedMessage { Ping { nonce: u64 }, Pong { nonce: u64, source: PeerId }, - StateRequestPart(ShardId, CryptoHash, u64), - VersionedStateResponse(StateResponseInfo), PartialEncodedChunkRequest(PartialEncodedChunkRequestMsg), PartialEncodedChunkResponse(PartialEncodedChunkResponseMsg), } @@ -62,15 +60,6 @@ impl fmt::Debug for RoutedMessage { match self { Self::Ping { nonce } => write!(f, "Ping({})", nonce), Self::Pong { nonce, source } => write!(f, "Pong({}, {})", nonce, source), - Self::StateRequestPart(shard_id, hash, part_id) => { - write!(f, "StateRequestPart({}, {}, {})", shard_id, hash, part_id) - } - Self::VersionedStateResponse(r) => write!( - f, - "VersionedStateResponse(shard_id: {} sync_hash: {})", - r.shard_id(), - r.sync_hash() - ), Self::PartialEncodedChunkRequest(r) => write!(f, "PartialEncodedChunkRequest({:?})", r), Self::PartialEncodedChunkResponse(r) => write!( f, @@ -93,6 +82,9 @@ pub enum DirectMessage { Block(Block), BlockHeadersRequest(Vec), BlockHeaders(Vec), + StateRequestHeader(ShardId, CryptoHash), + StateRequestPart(ShardId, CryptoHash, u64), + VersionedStateResponse(StateResponseInfo), } impl fmt::Display for DirectMessage { @@ -115,6 +107,18 @@ impl fmt::Debug for DirectMessage { h.iter().map(|h| format!("#{} {}", h.height(), h.hash())).collect::>() ) } + Self::StateRequestHeader(shard_id, hash) => { + write!(f, "StateRequestHeader({}, {})", shard_id, hash) + } + Self::StateRequestPart(shard_id, hash, part_id) => { + write!(f, "StateRequestPart({}, {}, {})", shard_id, hash, part_id) + } + Self::VersionedStateResponse(r) => write!( + f, + "VersionedStateResponse(shard_id: {} sync_hash: {})", + r.shard_id(), + r.sync_hash() + ), } } } @@ -367,6 +371,15 @@ impl Connection { DirectMessage::Block(b) => PeerMessage::Block(b), DirectMessage::BlockHeadersRequest(h) => PeerMessage::BlockHeadersRequest(h), DirectMessage::BlockHeaders(h) => PeerMessage::BlockHeaders(h), + DirectMessage::StateRequestHeader(shard_id, sync_hash) => { + PeerMessage::StateRequestHeader(shard_id, sync_hash) + } + DirectMessage::StateRequestPart(shard_id, sync_hash, part_id) => { + PeerMessage::StateRequestPart(shard_id, sync_hash, part_id) + } + DirectMessage::VersionedStateResponse(request) => { + PeerMessage::VersionedStateResponse(request) + } }; self.stream.write_message(&peer_msg).await @@ -386,12 +399,6 @@ impl Connection { RoutedMessage::Pong { nonce, source } => { RoutedMessageBody::Pong(Pong { nonce, source }) } - RoutedMessage::StateRequestPart(shard_id, block_hash, part_id) => { - RoutedMessageBody::StateRequestPart(shard_id, block_hash, part_id) - } - RoutedMessage::VersionedStateResponse(response) => { - RoutedMessageBody::VersionedStateResponse(response) - } RoutedMessage::PartialEncodedChunkRequest(request) => { RoutedMessageBody::PartialEncodedChunkRequest(request) } @@ -431,12 +438,6 @@ impl Connection { RoutedMessageBody::Pong(p) => { Some(RoutedMessage::Pong { nonce: p.nonce, source: p.source.clone() }) } - RoutedMessageBody::VersionedStateResponse(state_response_info) => { - Some(RoutedMessage::VersionedStateResponse(state_response_info.clone())) - } - RoutedMessageBody::StateRequestPart(shard_id, hash, part_id) => { - Some(RoutedMessage::StateRequestPart(*shard_id, *hash, *part_id)) - } RoutedMessageBody::PartialEncodedChunkRequest(request) => { Some(RoutedMessage::PartialEncodedChunkRequest(request.clone())) } @@ -494,6 +495,12 @@ impl Connection { PeerMessage::BlockHeaders(headers) => { return Ok((Message::Direct(DirectMessage::BlockHeaders(headers)), timestamp)); } + PeerMessage::VersionedStateResponse(state_response) => { + return Ok(( + Message::Direct(DirectMessage::VersionedStateResponse(state_response)), + timestamp, + )); + } _ => {} } } diff --git a/chain/network/src/raw/tests.rs b/chain/network/src/raw/tests.rs index cabdaf41e82..6f9288e7d3d 100644 --- a/chain/network/src/raw/tests.rs +++ b/chain/network/src/raw/tests.rs @@ -104,38 +104,40 @@ async fn test_raw_conn_state_parts() { .unwrap(); let num_parts = 5; - let ttl = 100; // Block hash needs to correspond to the hash of the first block of an epoch. // But the fake node simply ignores the block hash. let block_hash = CryptoHash::new(); for part_id in 0..num_parts { - conn.send_routed_message( - raw::RoutedMessage::StateRequestPart(0, block_hash, part_id), - peer_id.clone(), - ttl, - ) - .await - .unwrap(); + conn.send_message(raw::DirectMessage::StateRequestPart(0, block_hash, part_id)) + .await + .unwrap(); } let mut part_id_received = -1i64; loop { - let (msg, _timestamp) = conn.recv().await.unwrap(); - if let raw::Message::Routed(raw::RoutedMessage::VersionedStateResponse(state_response)) = - msg - { - let response = state_response.take_state_response(); - let part_id = response.part_id(); - if part_id.is_none() || part_id.unwrap() as i64 != (part_id_received + 1) { - panic!( - "received out of order part_id {:?} when {} was expected", - part_id, - part_id_received + 1 - ); + match conn.recv().await { + Ok((msg, _timestamp)) => { + if let raw::Message::Direct(raw::DirectMessage::VersionedStateResponse( + state_response, + )) = msg + { + let response = state_response.take_state_response(); + let part_id = response.part_id(); + if part_id.is_none() || part_id.unwrap() as i64 != (part_id_received + 1) { + panic!( + "received out of order part_id {:?} when {} was expected", + part_id, + part_id_received + 1 + ); + } + part_id_received = part_id.unwrap() as i64; + if part_id_received + 1 == num_parts as i64 { + break; + } + } } - part_id_received = part_id.unwrap() as i64; - if part_id_received + 1 == num_parts as i64 { - break; + Err(e) => { + panic!("error receiving part: {:?}", e); } } } diff --git a/chain/network/src/test_utils.rs b/chain/network/src/test_utils.rs index 55c6f96bed3..c2ba09351cb 100644 --- a/chain/network/src/test_utils.rs +++ b/chain/network/src/test_utils.rs @@ -182,7 +182,7 @@ pub fn expected_routing_tables( } /// `GetInfo` gets `NetworkInfo` from `PeerManager`. -#[derive(actix::Message)] +#[derive(actix::Message, Debug)] #[rtype(result = "NetworkInfo")] pub struct GetInfo {} @@ -196,7 +196,7 @@ impl Handler> for PeerManagerActor { } // `StopSignal is used to stop PeerManagerActor for unit tests -#[derive(actix::Message, Default)] +#[derive(actix::Message, Default, Debug)] #[rtype(result = "()")] pub struct StopSignal { pub should_panic: bool, diff --git a/chain/network/src/types.rs b/chain/network/src/types.rs index b4e3a679924..b2dd97c3264 100644 --- a/chain/network/src/types.rs +++ b/chain/network/src/types.rs @@ -232,8 +232,6 @@ pub enum NetworkRequests { part_id: u64, target: AccountOrPeerIdOrHash, }, - /// Response to state request. - StateResponse { route_back: CryptoHash, response: StateResponseInfo }, /// Ban given peer. BanPeer { peer_id: PeerId, ban_reason: ReasonForBan }, /// Announce account @@ -397,7 +395,6 @@ mod tests { use super::*; use crate::network_protocol::{RawRoutedMessage, RoutedMessage, RoutedMessageBody}; use borsh::BorshSerialize as _; - use near_primitives::syncing::ShardStateSyncResponseV1; const ALLOWED_SIZE: usize = 1 << 20; const NOTIFY_SIZE: usize = 1024; @@ -469,18 +466,6 @@ mod tests { 42, ], ); - - check( - RoutedMessageBody::VersionedStateResponse(StateResponseInfo::V1(StateResponseInfoV1 { - shard_id: 62, - sync_hash: CryptoHash([92; 32]), - state_response: ShardStateSyncResponseV1 { header: None, part: None }, - })), - &[ - 17, 0, 62, 0, 0, 0, 0, 0, 0, 0, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, - 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 0, 0, - ], - ); } } diff --git a/chain/rosetta-rpc/src/adapters/mod.rs b/chain/rosetta-rpc/src/adapters/mod.rs index 4f3ea2500ac..81a8c7f9970 100644 --- a/chain/rosetta-rpc/src/adapters/mod.rs +++ b/chain/rosetta-rpc/src/adapters/mod.rs @@ -724,8 +724,8 @@ impl TryFrom> for NearActions { .try_set(&intitiate_signed_delegate_action_operation.sender_account)?; let delegate_action: near_primitives::transaction::Action = - near_primitives::delegate_action::SignedDelegateAction { - delegate_action: near_primitives::delegate_action::DelegateAction { + near_primitives::action::delegate::SignedDelegateAction { + delegate_action: near_primitives::action::delegate::DelegateAction { sender_id: initiate_delegate_action_operation .sender_account .address @@ -826,7 +826,7 @@ mod tests { use near_actix_test_utils::run_actix; use near_client::test_utils::setup_no_network; use near_crypto::{KeyType, SecretKey}; - use near_primitives::delegate_action::{DelegateAction, SignedDelegateAction}; + use near_primitives::action::delegate::{DelegateAction, SignedDelegateAction}; use near_primitives::runtime::config::RuntimeConfig; use near_primitives::transaction::{Action, TransferAction}; use near_primitives::views::RuntimeConfigView; @@ -1096,7 +1096,7 @@ mod tests { let original_near_actions = NearActions { sender_account_id: "proxy.near".parse().unwrap(), receiver_account_id: "account.near".parse().unwrap(), - actions: vec![Action::Delegate(SignedDelegateAction { + actions: vec![Action::Delegate(Box::new(SignedDelegateAction { delegate_action: DelegateAction { sender_id: "account.near".parse().unwrap(), receiver_id: "receiver.near".parse().unwrap(), @@ -1108,7 +1108,7 @@ mod tests { public_key: sk.public_key(), }, signature: sk.sign(&[0]), - })], + }))], }; let operations: Vec = diff --git a/chain/rosetta-rpc/src/adapters/validated_operations/delegate_action.rs b/chain/rosetta-rpc/src/adapters/validated_operations/delegate_action.rs index 8cc02ad0b31..0b70c9b05ad 100644 --- a/chain/rosetta-rpc/src/adapters/validated_operations/delegate_action.rs +++ b/chain/rosetta-rpc/src/adapters/validated_operations/delegate_action.rs @@ -54,8 +54,8 @@ impl TryFrom for DelegateActionOperation { } } -impl From for DelegateActionOperation { - fn from(delegate_action: near_primitives::delegate_action::DelegateAction) -> Self { +impl From for DelegateActionOperation { + fn from(delegate_action: near_primitives::action::delegate::DelegateAction) -> Self { DelegateActionOperation { receiver_id: delegate_action.receiver_id.into(), max_block_height: delegate_action.max_block_height, diff --git a/chain/telemetry/src/lib.rs b/chain/telemetry/src/lib.rs index 45fba181e81..11961aaa378 100644 --- a/chain/telemetry/src/lib.rs +++ b/chain/telemetry/src/lib.rs @@ -83,8 +83,8 @@ impl Handler> for TelemetryActor { #[perf] fn handle(&mut self, msg: WithSpanContext, _ctx: &mut Context) { - // let (_span, msg) = handler_span!(target: "telemetry", tracing::Level::DEBUG, msg, ); let (_span, msg) = handler_debug_span!(target: "telemetry", msg); + tracing::debug!(target: "client", ?msg); let now = StaticClock::instant(); if now.duration_since(self.last_telemetry_update) < self.config.reporting_interval { // Throttle requests to the telemetry endpoints, to at most one diff --git a/core/chain-configs/src/client_config.rs b/core/chain-configs/src/client_config.rs index 5b9f3b6b4f2..717fe6c3b06 100644 --- a/core/chain-configs/src/client_config.rs +++ b/core/chain-configs/src/client_config.rs @@ -212,8 +212,6 @@ pub struct ClientConfig { pub ttl_account_id_router: Duration, /// Horizon at which instead of fetching block, fetch full state. pub block_fetch_horizon: BlockHeightDelta, - /// Horizon to step from the latest block when fetching state. - pub state_fetch_horizon: NumBlocks, /// Time between check to perform catchup. pub catchup_step_period: Duration, /// Time between checking to re-request chunks. @@ -224,12 +222,13 @@ pub struct ClientConfig { pub block_header_fetch_horizon: BlockHeightDelta, /// Garbage collection configuration. pub gc: GCConfig, - /// Accounts that this client tracks + /// Accounts that this client tracks. pub tracked_accounts: Vec, - /// Shards that this client tracks + /// Shards that this client tracks. pub tracked_shards: Vec, /// Rotate between these sets of tracked shards. /// Used to simulate the behavior of chunk only producers without staking tokens. + /// This field is only used if `tracked_shards` is empty. pub tracked_shard_schedule: Vec>, /// Not clear old data, set `true` for archive nodes. pub archive: bool, @@ -317,7 +316,6 @@ impl ClientConfig { num_block_producer_seats, ttl_account_id_router: Duration::from_secs(60 * 60), block_fetch_horizon: 50, - state_fetch_horizon: 5, catchup_step_period: Duration::from_millis(1), chunk_request_retry_period: min( Duration::from_millis(100), diff --git a/core/o11y/src/lib.rs b/core/o11y/src/lib.rs index 2a255e6d2d7..bf3ec6993a3 100644 --- a/core/o11y/src/lib.rs +++ b/core/o11y/src/lib.rs @@ -188,13 +188,18 @@ fn add_simple_log_layer( filter: EnvFilter, writer: W, ansi: bool, + with_span_events: bool, subscriber: S, ) -> SimpleLogLayer where S: tracing::Subscriber + for<'span> LookupSpan<'span> + Send + Sync, W: for<'writer> fmt::MakeWriter<'writer> + 'static, { - let layer = fmt::layer().with_ansi(ansi).with_writer(writer).with_filter(filter); + let layer = fmt::layer() + .with_ansi(ansi) + .with_span_events(get_fmt_span(with_span_events)) + .with_writer(writer) + .with_filter(filter); subscriber.with(layer) } @@ -337,7 +342,13 @@ pub fn default_subscriber( }; let subscriber = tracing_subscriber::registry(); - let subscriber = add_simple_log_layer(env_filter, make_writer, color_output, subscriber); + let subscriber = add_simple_log_layer( + env_filter, + make_writer, + color_output, + options.log_span_events, + subscriber, + ); #[allow(unused_mut)] let mut io_trace_guard = None; diff --git a/core/o11y/src/metrics.rs b/core/o11y/src/metrics.rs index 5be4c10602e..db019c9fbb1 100644 --- a/core/o11y/src/metrics.rs +++ b/core/o11y/src/metrics.rs @@ -208,6 +208,7 @@ static EXCEPTIONS: Lazy> = Lazy::new(|| { "flat_storage_creation_threads_used", "flat_storage_distance_to_head", "flat_storage_head_height", + "flat_storage_hops_to_head", ]) }); diff --git a/core/primitives-core/Cargo.toml b/core/primitives-core/Cargo.toml index fde0b49fcf7..9d9a34e5d80 100644 --- a/core/primitives-core/Cargo.toml +++ b/core/primitives-core/Cargo.toml @@ -36,17 +36,16 @@ protocol_feature_fix_staking_threshold = [] protocol_feature_fix_contract_loading_cost = [] protocol_feature_reject_blocks_with_outdated_protocol_version = [] protocol_feature_simple_nightshade_v2 = [] -protocol_feature_block_header_v4 = [] protocol_feature_restrict_tla = [] nightly = [ "nightly_protocol", - "protocol_feature_block_header_v4", "protocol_feature_fix_contract_loading_cost", "protocol_feature_fix_staking_threshold", "protocol_feature_reject_blocks_with_outdated_protocol_version", - "protocol_feature_simple_nightshade_v2", "protocol_feature_restrict_tla", + "protocol_feature_simple_nightshade_v2", ] -nightly_protocol = [] +nightly_protocol = [ +] diff --git a/core/primitives-core/src/config.rs b/core/primitives-core/src/config.rs index 8ff2f819d43..338845da42f 100644 --- a/core/primitives-core/src/config.rs +++ b/core/primitives-core/src/config.rs @@ -1,154 +1,9 @@ use crate::parameter::Parameter; use crate::types::{Compute, Gas}; use enum_map::{enum_map, EnumMap}; -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; +use std::hash::Hash; use strum::Display; -/// Dynamic configuration parameters required for the WASM runtime to -/// execute a smart contract. -/// -/// This (`VMConfig`) and `RuntimeFeesConfig` combined are sufficient to define -/// protocol specific behavior of the contract runtime. The former contains -/// configuration for the WASM runtime specifically, while the latter contains -/// configuration for the transaction runtime and WASM runtime. -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct VMConfig { - /// Costs for runtime externals - pub ext_costs: ExtCostsConfig, - - /// Gas cost of a growing memory by single page. - pub grow_mem_cost: u32, - /// Gas cost of a regular operation. - pub regular_op_cost: u32, - - /// Describes limits for VM and Runtime. - pub limit_config: VMLimitConfig, -} - -/// Describes limits for VM and Runtime. -/// TODO #4139: consider switching to strongly-typed wrappers instead of raw quantities -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Hash, PartialEq, Eq)] -pub struct VMLimitConfig { - /// Max amount of gas that can be used, excluding gas attached to promises. - pub max_gas_burnt: Gas, - - /// How tall the stack is allowed to grow? - /// - /// See to find out how the stack frame cost - /// is calculated. - pub max_stack_height: u32, - /// Whether a legacy version of stack limiting should be used, see - /// [`ContractPrepareVersion`]. - #[serde(default = "ContractPrepareVersion::v0")] - pub contract_prepare_version: ContractPrepareVersion, - - /// The initial number of memory pages. - /// NOTE: It's not a limiter itself, but it's a value we use for initial_memory_pages. - pub initial_memory_pages: u32, - /// What is the maximal memory pages amount is allowed to have for a contract. - pub max_memory_pages: u32, - - /// Limit of memory used by registers. - pub registers_memory_limit: u64, - /// Maximum number of bytes that can be stored in a single register. - pub max_register_size: u64, - /// Maximum number of registers that can be used simultaneously. - /// - /// Note that due to an implementation quirk [read: a bug] in VMLogic, if we - /// have this number of registers, no subsequent writes to the registers - /// will succeed even if they replace an existing register. - pub max_number_registers: u64, - - /// Maximum number of log entries. - pub max_number_logs: u64, - /// Maximum total length in bytes of all log messages. - pub max_total_log_length: u64, - - /// Max total prepaid gas for all function call actions per receipt. - pub max_total_prepaid_gas: Gas, - - /// Max number of actions per receipt. - pub max_actions_per_receipt: u64, - /// Max total length of all method names (including terminating character) for a function call - /// permission access key. - pub max_number_bytes_method_names: u64, - /// Max length of any method name (without terminating character). - pub max_length_method_name: u64, - /// Max length of arguments in a function call action. - pub max_arguments_length: u64, - /// Max length of returned data - pub max_length_returned_data: u64, - /// Max contract size - pub max_contract_size: u64, - /// Max transaction size - pub max_transaction_size: u64, - /// Max storage key size - pub max_length_storage_key: u64, - /// Max storage value size - pub max_length_storage_value: u64, - /// Max number of promises that a function call can create - pub max_promises_per_function_call_action: u64, - /// Max number of input data dependencies - pub max_number_input_data_dependencies: u64, - /// If present, stores max number of functions in one contract - #[serde(skip_serializing_if = "Option::is_none")] - pub max_functions_number_per_contract: Option, - /// If present, stores the secondary stack limit as implemented by wasmer2. - /// - /// This limit should never be hit normally. - #[serde(default = "wasmer2_stack_limit_default")] - pub wasmer2_stack_limit: i32, - /// If present, stores max number of locals declared globally in one contract - #[serde(skip_serializing_if = "Option::is_none")] - pub max_locals_per_contract: Option, - /// Whether to enforce account_id well-formedness where it wasn't enforced - /// historically. - #[serde(default = "AccountIdValidityRulesVersion::v0")] - pub account_id_validity_rules_version: AccountIdValidityRulesVersion, -} - -fn wasmer2_stack_limit_default() -> i32 { - 100 * 1024 -} - -/// Our original code for limiting WASM stack was buggy. We fixed that, but we -/// still have to use old (`V0`) limiter for old protocol versions. -/// -/// This struct here exists to enforce that the value in the config is either -/// `0` or `1`. We could have used a `bool` instead, but there's a chance that -/// our current impl isn't perfect either and would need further tweaks in the -/// future. -#[derive( - Debug, - Clone, - Copy, - Hash, - PartialEq, - Eq, - serde_repr::Serialize_repr, - serde_repr::Deserialize_repr, -)] -#[repr(u8)] -pub enum ContractPrepareVersion { - /// Oldest, buggiest version. - /// - /// Don't use it unless specifically to support old protocol version. - V0, - /// Old, slow and buggy version. - /// - /// Better than V0, but don’t use this nevertheless. - V1, - /// finite-wasm 0.3.0 based contract preparation code. - V2, -} - -impl ContractPrepareVersion { - pub fn v0() -> ContractPrepareVersion { - ContractPrepareVersion::V0 - } -} - #[derive( Debug, Clone, @@ -168,94 +23,11 @@ pub enum AccountIdValidityRulesVersion { } impl AccountIdValidityRulesVersion { - fn v0() -> AccountIdValidityRulesVersion { + pub fn v0() -> AccountIdValidityRulesVersion { AccountIdValidityRulesVersion::V0 } } -impl VMConfig { - pub fn test() -> VMConfig { - VMConfig { - ext_costs: ExtCostsConfig::test(), - grow_mem_cost: 1, - regular_op_cost: (SAFETY_MULTIPLIER as u32) * 1285457, - limit_config: VMLimitConfig::test(), - } - } - - /// Computes non-cryptographically-proof hash. The computation is fast but not cryptographically - /// secure. - pub fn non_crypto_hash(&self) -> u64 { - let mut s = DefaultHasher::new(); - self.hash(&mut s); - s.finish() - } - - pub fn free() -> Self { - Self { - ext_costs: ExtCostsConfig::free(), - grow_mem_cost: 0, - regular_op_cost: 0, - // We shouldn't have any costs in the limit config. - limit_config: VMLimitConfig { max_gas_burnt: u64::MAX, ..VMLimitConfig::test() }, - } - } -} - -impl VMLimitConfig { - pub fn test() -> Self { - const KB: u32 = 1024; - let max_contract_size = 4 * 2u64.pow(20); - Self { - max_gas_burnt: 2 * 10u64.pow(14), // with 10**15 block gas limit this will allow 5 calls. - max_stack_height: 256 * KB, - contract_prepare_version: ContractPrepareVersion::V2, - initial_memory_pages: 2u32.pow(10), // 64Mib of memory. - max_memory_pages: 2u32.pow(11), // 128Mib of memory. - - // By default registers are limited by 1GiB of memory. - registers_memory_limit: 2u64.pow(30), - // By default each register is limited by 100MiB of memory. - max_register_size: 2u64.pow(20) * 100, - // By default there is at most 100 registers. - max_number_registers: 100, - - max_number_logs: 100, - // Total logs size is 16Kib - max_total_log_length: 16 * 1024, - - // Updating the maximum prepaid gas to limit the maximum depth of a transaction to 64 - // blocks. - // This based on `63 * min_receipt_with_function_call_gas()`. Where 63 is max depth - 1. - max_total_prepaid_gas: 300 * 10u64.pow(12), - - // Safety limit. Unlikely to hit it for most common transactions and receipts. - max_actions_per_receipt: 100, - // Should be low enough to deserialize an access key without paying. - max_number_bytes_method_names: 2000, - max_length_method_name: 256, // basic safety limit - max_arguments_length: 4 * 2u64.pow(20), // 4 Mib - max_length_returned_data: 4 * 2u64.pow(20), // 4 Mib - max_contract_size, // 4 Mib, - max_transaction_size: 4 * 2u64.pow(20), // 4 Mib - - max_length_storage_key: 4 * 2u64.pow(20), // 4 Mib - max_length_storage_value: 4 * 2u64.pow(20), // 4 Mib - // Safety limit and unlikely abusable. - max_promises_per_function_call_action: 1024, - // Unlikely to hit it for normal development. - max_number_input_data_dependencies: 128, - max_functions_number_per_contract: Some(10000), - wasmer2_stack_limit: 200 * 1024, - // To utilize a local in an useful way, at least two `local.*` instructions are - // necessary (they only take constant operands indicating the local to access), which - // is 4 bytes worth of code for each local. - max_locals_per_contract: Some(max_contract_size / 4), - account_id_validity_rules_version: AccountIdValidityRulesVersion::V1, - } - } -} - /// Configuration of view methods execution, during which no costs should be charged. #[derive(Default, Clone, serde::Serialize, serde::Deserialize, Debug, Hash, PartialEq, Eq)] pub struct ViewConfig { @@ -363,14 +135,6 @@ impl ExtCostsConfig { pub fn test() -> ExtCostsConfig { Self::test_with_undercharging_factor(1) } - - fn free() -> ExtCostsConfig { - ExtCostsConfig { - costs: enum_map! { - _ => ParameterCost { gas: 0, compute: 0 } - }, - } - } } /// Strongly-typed representation of the fees for counting. diff --git a/core/primitives-core/src/lib.rs b/core/primitives-core/src/lib.rs index aec329bf1e7..516a9ff87cc 100644 --- a/core/primitives-core/src/lib.rs +++ b/core/primitives-core/src/lib.rs @@ -3,7 +3,6 @@ pub use num_rational; pub mod account; pub mod config; -pub mod contract; pub mod hash; pub mod parameter; pub mod profile; @@ -11,3 +10,5 @@ pub mod runtime; pub mod serialize; pub mod types; pub mod version; + +pub use enum_map; diff --git a/core/primitives-core/src/parameter.rs b/core/primitives-core/src/parameter.rs index 258bc1349cc..1b112f0fb11 100644 --- a/core/primitives-core/src/parameter.rs +++ b/core/primitives-core/src/parameter.rs @@ -145,6 +145,13 @@ pub enum Parameter { Wasmer2StackLimit, MaxLocalsPerContract, AccountIdValidityRulesVersion, + + // Contract runtime features + #[strum(serialize = "disable_9393_fix")] + Disable9393Fix, + FlatStorageReads, + ImplicitAccountCreation, + FixContractLoadingCost, } #[derive( diff --git a/core/primitives-core/src/types.rs b/core/primitives-core/src/types.rs index 25bd1400f72..7cc5e279fff 100644 --- a/core/primitives-core/src/types.rs +++ b/core/primitives-core/src/types.rs @@ -32,17 +32,6 @@ pub type Compute = u64; #[derive(Clone, Debug, PartialEq, Eq)] pub struct GasWeight(pub u64); -/// Result from a gas distribution among function calls with ratios. -#[must_use] -#[non_exhaustive] -#[derive(Debug, PartialEq, Eq)] -pub enum GasDistribution { - /// All remaining gas was distributed to functions. - All, - /// There were no function call actions with a ratio specified. - NoRatios, -} - /// Number of blocks in current group. pub type NumBlocks = u64; /// Number of shards in current group. diff --git a/core/primitives-core/src/version.rs b/core/primitives-core/src/version.rs index 10d035b961f..b91848beeb3 100644 --- a/core/primitives-core/src/version.rs +++ b/core/primitives-core/src/version.rs @@ -101,15 +101,13 @@ pub enum ProtocolFeature { /// /// Flat Storage NEP-399: https://github.com/near/NEPs/blob/master/neps/nep-0399.md FlatStorageReads, - /// Enables preparation V2. Note that this setting is not supported in production settings /// without NearVmRuntime enabled alongside it, as the VM runner would be too slow. PreparationV2, - /// Enables Near-Vm. Note that this setting is not at all supported without PreparationV2, /// as it hardcodes preparation v2 code into the generated assembly. NearVmRuntime, - + BlockHeaderV4, /// In case not all validator seats are occupied our algorithm provide incorrect minimal seat /// price - it reports as alpha * sum_stake instead of alpha * sum_stake / (1 - alpha), where /// alpha is min stake ratio @@ -122,8 +120,6 @@ pub enum ProtocolFeature { RejectBlocksWithOutdatedProtocolVersions, #[cfg(feature = "protocol_feature_simple_nightshade_v2")] SimpleNightshadeV2, - #[cfg(feature = "protocol_feature_block_header_v4")] - BlockHeaderV4, #[cfg(feature = "protocol_feature_restrict_tla")] RestrictTLA, } @@ -168,6 +164,7 @@ impl ProtocolFeature { | ProtocolFeature::DelegateAction => 59, ProtocolFeature::ComputeCosts | ProtocolFeature::FlatStorageReads => 61, ProtocolFeature::PreparationV2 | ProtocolFeature::NearVmRuntime => 62, + ProtocolFeature::BlockHeaderV4 => 63, // Nightly features #[cfg(feature = "protocol_feature_fix_staking_threshold")] @@ -178,8 +175,6 @@ impl ProtocolFeature { ProtocolFeature::RejectBlocksWithOutdatedProtocolVersions => 132, #[cfg(feature = "protocol_feature_simple_nightshade_v2")] ProtocolFeature::SimpleNightshadeV2 => 135, - #[cfg(feature = "protocol_feature_block_header_v4")] - ProtocolFeature::BlockHeaderV4 => 138, #[cfg(feature = "protocol_feature_restrict_tla")] ProtocolFeature::RestrictTLA => 139, } @@ -189,7 +184,7 @@ impl ProtocolFeature { /// Current protocol version used on the mainnet. /// Some features (e. g. FixStorageUsage) require that there is at least one epoch with exactly /// the corresponding version -const STABLE_PROTOCOL_VERSION: ProtocolVersion = 62; +const STABLE_PROTOCOL_VERSION: ProtocolVersion = 63; /// Largest protocol version supported by the current binary. pub const PROTOCOL_VERSION: ProtocolVersion = if cfg!(feature = "nightly_protocol") { diff --git a/core/primitives/Cargo.toml b/core/primitives/Cargo.toml index 99c7d1dba1d..addb6b50e27 100644 --- a/core/primitives/Cargo.toml +++ b/core/primitives/Cargo.toml @@ -11,6 +11,7 @@ publish = true [dependencies] arbitrary.workspace = true +base64.workspace = true borsh.workspace = true bytesize.workspace = true cfg-if.workspace = true @@ -56,19 +57,15 @@ protocol_feature_reject_blocks_with_outdated_protocol_version = [ protocol_feature_simple_nightshade_v2 = [ "near-primitives-core/protocol_feature_simple_nightshade_v2", ] -protocol_feature_block_header_v4 = [ - "near-primitives-core/protocol_feature_block_header_v4", -] protocol_feature_restrict_tla = [ "near-primitives-core/protocol_feature_restrict_tla", ] nightly = [ "nightly_protocol", - "protocol_feature_restrict_tla", - "protocol_feature_block_header_v4", "protocol_feature_fix_contract_loading_cost", "protocol_feature_fix_staking_threshold", "protocol_feature_reject_blocks_with_outdated_protocol_version", + "protocol_feature_restrict_tla", "protocol_feature_simple_nightshade_v2", "near-fmt/nightly", "near-primitives-core/nightly", @@ -80,6 +77,7 @@ nightly_protocol = [ "near-primitives-core/nightly_protocol", "near-vm-runner/nightly_protocol", ] +new_epoch_sync = [] calimero_zero_storage = [] diff --git a/core/primitives/res/runtime_configs/129.yaml b/core/primitives/res/runtime_configs/129.yaml new file mode 100644 index 00000000000..c60af6224e9 --- /dev/null +++ b/core/primitives/res/runtime_configs/129.yaml @@ -0,0 +1 @@ +fix_contract_loading_cost: { old: false, new: true } diff --git a/core/primitives/res/runtime_configs/35.yaml b/core/primitives/res/runtime_configs/35.yaml new file mode 100644 index 00000000000..74b5b963cf5 --- /dev/null +++ b/core/primitives/res/runtime_configs/35.yaml @@ -0,0 +1 @@ +implicit_account_creation: { old: false, new: true } diff --git a/core/primitives/res/runtime_configs/61.yaml b/core/primitives/res/runtime_configs/61.yaml index 6f2943e1ea3..a7224cca644 100644 --- a/core/primitives/res/runtime_configs/61.yaml +++ b/core/primitives/res/runtime_configs/61.yaml @@ -5,3 +5,4 @@ wasm_storage_write_base: { old: 64_196_736_000, new: { gas: 64_196_736_000, c wasm_storage_remove_base: { old: 53_473_030_500, new: { gas: 53_473_030_500, compute: 200_000_000_000 } } wasm_storage_read_base: { old: 56_356_845_750, new: { gas: 56_356_845_750, compute: 200_000_000_000 } } wasm_storage_has_key_base: { old: 54_039_896_625, new: { gas: 54_039_896_625, compute: 200_000_000_000 } } +flat_storage_reads: { old: false, new: true } diff --git a/core/primitives/res/runtime_configs/62.yaml b/core/primitives/res/runtime_configs/62.yaml index 4171df5219a..3a19480c3de 100644 --- a/core/primitives/res/runtime_configs/62.yaml +++ b/core/primitives/res/runtime_configs/62.yaml @@ -3,3 +3,6 @@ # correct. max_stack_height: { old: 16384, new: 262144 } contract_prepare_version: { old: 1, new: 2 } + +# There was a bug for a short period of time that we need to reproduce... +disable_9393_fix: { old: false, new: true } diff --git a/core/primitives/res/runtime_configs/63.yaml b/core/primitives/res/runtime_configs/63.yaml new file mode 100644 index 00000000000..18dc4c1b745 --- /dev/null +++ b/core/primitives/res/runtime_configs/63.yaml @@ -0,0 +1 @@ +disable_9393_fix: { old: true, new: false } diff --git a/core/primitives/res/runtime_configs/parameters.snap b/core/primitives/res/runtime_configs/parameters.snap index 097cc69b462..665cc9452b7 100644 --- a/core/primitives/res/runtime_configs/parameters.snap +++ b/core/primitives/res/runtime_configs/parameters.snap @@ -163,4 +163,8 @@ max_functions_number_per_contract 10_000 wasmer2_stack_limit 204_800 max_locals_per_contract 1_000_000 account_id_validity_rules_version 1 +disable_9393_fix false +flat_storage_reads true +implicit_account_creation true +fix_contract_loading_cost true diff --git a/core/primitives/res/runtime_configs/parameters.yaml b/core/primitives/res/runtime_configs/parameters.yaml index 785da2f5181..20a5e34f49d 100644 --- a/core/primitives/res/runtime_configs/parameters.yaml +++ b/core/primitives/res/runtime_configs/parameters.yaml @@ -196,3 +196,9 @@ max_length_storage_value: 4_194_304 max_promises_per_function_call_action: 1_024 max_number_input_data_dependencies: 128 account_id_validity_rules_version: 0 + +# Contract runtime configuration +disable_9393_fix: false +flat_storage_reads: false +implicit_account_creation: false +fix_contract_loading_cost: false diff --git a/core/primitives/res/runtime_configs/parameters_testnet.yaml b/core/primitives/res/runtime_configs/parameters_testnet.yaml index a5a3fe7ea52..f979bf13d57 100644 --- a/core/primitives/res/runtime_configs/parameters_testnet.yaml +++ b/core/primitives/res/runtime_configs/parameters_testnet.yaml @@ -192,3 +192,8 @@ max_length_storage_key: 4_194_304 max_length_storage_value: 4_194_304 max_promises_per_function_call_action: 1_024 max_number_input_data_dependencies: 128 + +disable_9393_fix: false +flat_storage_reads: false +implicit_account_creation: false +fix_contract_loading_cost: false diff --git a/runtime/near-vm-runner/src/logic/delegate_action.rs b/core/primitives/src/action/delegate.rs similarity index 97% rename from runtime/near-vm-runner/src/logic/delegate_action.rs rename to core/primitives/src/action/delegate.rs index 8b3b2d55d21..0963d7e8dd2 100644 --- a/runtime/near-vm-runner/src/logic/delegate_action.rs +++ b/core/primitives/src/action/delegate.rs @@ -4,8 +4,8 @@ //! This is the module containing the types introduced for delegate actions. pub use self::private_non_delegate_action::NonDelegateAction; -use super::action::Action; -use super::signable_message::{SignableMessage, SignableMessageType}; +use super::Action; +use crate::signable_message::{SignableMessage, SignableMessageType}; use borsh::{BorshDeserialize, BorshSerialize}; use near_crypto::{PublicKey, Signature}; use near_primitives_core::hash::{hash, CryptoHash}; @@ -56,7 +56,7 @@ impl SignedDelegateAction { impl From for Action { fn from(delegate_action: SignedDelegateAction) -> Self { - Self::Delegate(delegate_action) + Self::Delegate(Box::new(delegate_action)) } } @@ -133,7 +133,7 @@ mod private_non_delegate_action { #[cfg(test)] mod tests { use super::*; - use crate::logic::action::CreateAccountAction; + use crate::action::CreateAccountAction; use near_crypto::KeyType; /// A serialized `Action::Delegate(SignedDelegateAction)` for testing. @@ -150,7 +150,7 @@ mod tests { ); fn create_delegate_action(actions: Vec) -> Action { - Action::Delegate(SignedDelegateAction { + Action::Delegate(Box::new(SignedDelegateAction { delegate_action: DelegateAction { sender_id: "aaa".parse().unwrap(), receiver_id: "bbb".parse().unwrap(), @@ -163,7 +163,7 @@ mod tests { public_key: PublicKey::empty(KeyType::ED25519), }, signature: Signature::empty(KeyType::ED25519), - }) + })) } #[test] diff --git a/runtime/near-vm-runner/src/logic/action.rs b/core/primitives/src/action/mod.rs similarity index 90% rename from runtime/near-vm-runner/src/logic/action.rs rename to core/primitives/src/action/mod.rs index 6e04c956dee..5c15d74ebf4 100644 --- a/runtime/near-vm-runner/src/logic/action.rs +++ b/core/primitives/src/action/mod.rs @@ -1,3 +1,5 @@ +pub mod delegate; + use borsh::{BorshDeserialize, BorshSerialize}; use near_crypto::PublicKey; use near_primitives_core::{ @@ -168,14 +170,18 @@ pub enum Action { CreateAccount(CreateAccountAction), /// Sets a Wasm code to a receiver_id DeployContract(DeployContractAction), - FunctionCall(FunctionCallAction), + FunctionCall(Box), Transfer(TransferAction), - Stake(StakeAction), - AddKey(AddKeyAction), - DeleteKey(DeleteKeyAction), + Stake(Box), + AddKey(Box), + DeleteKey(Box), DeleteAccount(DeleteAccountAction), - Delegate(super::delegate_action::SignedDelegateAction), + Delegate(Box), } +const _: () = assert!( + cfg!(not(target_pointer_width = "64")) || std::mem::size_of::() == 32, + "Action is less than 32 bytes for performance reasons, see #9451" +); impl Action { pub fn get_prepaid_gas(&self) -> Gas { @@ -207,7 +213,7 @@ impl From for Action { impl From for Action { fn from(function_call_action: FunctionCallAction) -> Self { - Self::FunctionCall(function_call_action) + Self::FunctionCall(Box::new(function_call_action)) } } @@ -219,19 +225,19 @@ impl From for Action { impl From for Action { fn from(stake_action: StakeAction) -> Self { - Self::Stake(stake_action) + Self::Stake(Box::new(stake_action)) } } impl From for Action { fn from(add_key_action: AddKeyAction) -> Self { - Self::AddKey(add_key_action) + Self::AddKey(Box::new(add_key_action)) } } impl From for Action { fn from(delete_key_action: DeleteKeyAction) -> Self { - Self::DeleteKey(delete_key_action) + Self::DeleteKey(Box::new(delete_key_action)) } } diff --git a/core/primitives/src/block.rs b/core/primitives/src/block.rs index 70a92322116..844640c8a85 100644 --- a/core/primitives/src/block.rs +++ b/core/primitives/src/block.rs @@ -159,13 +159,7 @@ impl Block { vrf_value: body.vrf_value, vrf_proof: body.vrf_proof, })) - } else if checked_feature!( - "protocol_feature_block_header_v4", - BlockHeaderV4, - this_epoch_protocol_version - ) { - Block::BlockV3(Arc::new(BlockV3 { header, body })) - } else { + } else if !checked_feature!("stable", BlockHeaderV4, this_epoch_protocol_version) { Block::BlockV2(Arc::new(BlockV2 { header, chunks: body.chunks, @@ -173,6 +167,8 @@ impl Block { vrf_value: body.vrf_value, vrf_proof: body.vrf_proof, })) + } else { + Block::BlockV3(Arc::new(BlockV3 { header, body })) } } @@ -197,7 +193,6 @@ impl Block { genesis_protocol_version, height, Block::compute_state_root(&body.chunks), - #[cfg(feature = "protocol_feature_block_header_v4")] Block::compute_block_body_hash_impl(&body), Block::compute_chunk_receipts_root(&body.chunks), Block::compute_chunk_headers_root(&body.chunks).0, @@ -229,7 +224,7 @@ impl Block { epoch_id: EpochId, next_epoch_id: EpochId, epoch_sync_data_hash: Option, - approvals: Vec>, + approvals: Vec>>, gas_price_adjustment_rate: Rational32, min_gas_price: Balance, max_gas_price: Balance, @@ -302,7 +297,6 @@ impl Block { next_epoch_protocol_version, height, *prev.hash(), - #[cfg(feature = "protocol_feature_block_header_v4")] Block::compute_block_body_hash_impl(&body), Block::compute_state_root(&body.chunks), Block::compute_chunk_receipts_root(&body.chunks), diff --git a/core/primitives/src/block_header.rs b/core/primitives/src/block_header.rs index 5f931389bb1..38491b52cb3 100644 --- a/core/primitives/src/block_header.rs +++ b/core/primitives/src/block_header.rs @@ -63,7 +63,7 @@ pub struct BlockHeaderInnerRest { pub last_ds_final_block: CryptoHash, /// All the approvals included in this block - pub approvals: Vec>, + pub approvals: Vec>>, /// Latest protocol version that this block producer has. pub latest_protocol_version: ProtocolVersion, @@ -99,7 +99,7 @@ pub struct BlockHeaderInnerRestV2 { pub last_ds_final_block: CryptoHash, /// All the approvals included in this block - pub approvals: Vec>, + pub approvals: Vec>>, /// Latest protocol version that this block producer has. pub latest_protocol_version: ProtocolVersion, @@ -145,7 +145,7 @@ pub struct BlockHeaderInnerRestV3 { pub epoch_sync_data_hash: Option, /// All the approvals included in this block - pub approvals: Vec>, + pub approvals: Vec>>, /// Latest protocol version that this block producer has. pub latest_protocol_version: ProtocolVersion, @@ -190,7 +190,7 @@ pub struct BlockHeaderInnerRestV4 { pub epoch_sync_data_hash: Option, /// All the approvals included in this block - pub approvals: Vec>, + pub approvals: Vec>>, /// Latest protocol version that this block producer has. pub latest_protocol_version: ProtocolVersion, @@ -407,7 +407,7 @@ impl BlockHeader { next_epoch_protocol_version: ProtocolVersion, height: BlockHeight, prev_hash: CryptoHash, - #[cfg(feature = "protocol_feature_block_header_v4")] block_body_hash: CryptoHash, + block_body_hash: CryptoHash, prev_state_root: MerkleHash, chunk_receipts_root: MerkleHash, chunk_headers_root: MerkleHash, @@ -428,7 +428,7 @@ impl BlockHeader { last_final_block: CryptoHash, last_ds_final_block: CryptoHash, epoch_sync_data_hash: Option, - approvals: Vec>, + approvals: Vec>>, next_bp_hash: CryptoHash, block_merkle_root: CryptoHash, prev_height: BlockHeight, @@ -510,46 +510,7 @@ impl BlockHeader { signature, hash, })) - } else { - #[cfg(feature = "protocol_feature_block_header_v4")] - if crate::checked_feature!( - "protocol_feature_block_header_v4", - BlockHeaderV4, - this_epoch_protocol_version - ) { - let inner_rest = BlockHeaderInnerRestV4 { - block_body_hash, - chunk_receipts_root, - chunk_headers_root, - chunk_tx_root, - challenges_root, - random_value, - validator_proposals, - chunk_mask, - gas_price, - block_ordinal, - total_supply, - challenges_result, - last_final_block, - last_ds_final_block, - prev_height, - epoch_sync_data_hash, - approvals, - latest_protocol_version: get_protocol_version(next_epoch_protocol_version), - }; - let (hash, signature) = signer.sign_block_header_parts( - prev_hash, - &inner_lite.try_to_vec().expect("Failed to serialize"), - &inner_rest.try_to_vec().expect("Failed to serialize"), - ); - return Self::BlockHeaderV4(Arc::new(BlockHeaderV4 { - prev_hash, - inner_lite, - inner_rest, - signature, - hash, - })); - } + } else if !crate::checked_feature!("stable", BlockHeaderV4, this_epoch_protocol_version) { let inner_rest = BlockHeaderInnerRestV3 { chunk_receipts_root, chunk_headers_root, @@ -581,6 +542,39 @@ impl BlockHeader { signature, hash, })) + } else { + let inner_rest = BlockHeaderInnerRestV4 { + block_body_hash, + chunk_receipts_root, + chunk_headers_root, + chunk_tx_root, + challenges_root, + random_value, + validator_proposals, + chunk_mask, + gas_price, + block_ordinal, + total_supply, + challenges_result, + last_final_block, + last_ds_final_block, + prev_height, + epoch_sync_data_hash, + approvals, + latest_protocol_version: get_protocol_version(next_epoch_protocol_version), + }; + let (hash, signature) = signer.sign_block_header_parts( + prev_hash, + &inner_lite.try_to_vec().expect("Failed to serialize"), + &inner_rest.try_to_vec().expect("Failed to serialize"), + ); + Self::BlockHeaderV4(Arc::new(BlockHeaderV4 { + prev_hash, + inner_lite, + inner_rest, + signature, + hash, + })) } } @@ -588,7 +582,7 @@ impl BlockHeader { genesis_protocol_version: ProtocolVersion, height: BlockHeight, state_root: MerkleHash, - #[cfg(feature = "protocol_feature_block_header_v4")] block_body_hash: CryptoHash, + block_body_hash: CryptoHash, chunk_receipts_root: MerkleHash, chunk_headers_root: MerkleHash, chunk_tx_root: MerkleHash, @@ -671,46 +665,7 @@ impl BlockHeader { signature: Signature::empty(KeyType::ED25519), hash, })) - } else { - #[cfg(feature = "protocol_feature_block_header_v4")] - if crate::checked_feature!( - "protocol_feature_block_header_v4", - BlockHeaderV4, - genesis_protocol_version - ) { - let inner_rest = BlockHeaderInnerRestV4 { - chunk_receipts_root, - chunk_headers_root, - chunk_tx_root, - challenges_root, - block_body_hash, - random_value: CryptoHash::default(), - validator_proposals: vec![], - chunk_mask: vec![true; chunks_included as usize], - block_ordinal: 1, // It is guaranteed that Chain has the only Block which is Genesis - gas_price: initial_gas_price, - total_supply: initial_total_supply, - challenges_result: vec![], - last_final_block: CryptoHash::default(), - last_ds_final_block: CryptoHash::default(), - prev_height: 0, - epoch_sync_data_hash: None, // Epoch Sync cannot be executed up to Genesis - approvals: vec![], - latest_protocol_version: genesis_protocol_version, - }; - let hash = BlockHeader::compute_hash( - CryptoHash::default(), - &inner_lite.try_to_vec().expect("Failed to serialize"), - &inner_rest.try_to_vec().expect("Failed to serialize"), - ); - return Self::BlockHeaderV4(Arc::new(BlockHeaderV4 { - prev_hash: CryptoHash::default(), - inner_lite, - inner_rest, - signature: Signature::empty(KeyType::ED25519), - hash, - })); - } + } else if !crate::checked_feature!("stable", BlockHeaderV4, genesis_protocol_version) { let inner_rest = BlockHeaderInnerRestV3 { chunk_receipts_root, chunk_headers_root, @@ -742,6 +697,39 @@ impl BlockHeader { signature: Signature::empty(KeyType::ED25519), hash, })) + } else { + let inner_rest = BlockHeaderInnerRestV4 { + chunk_receipts_root, + chunk_headers_root, + chunk_tx_root, + challenges_root, + block_body_hash, + random_value: CryptoHash::default(), + validator_proposals: vec![], + chunk_mask: vec![true; chunks_included as usize], + block_ordinal: 1, // It is guaranteed that Chain has the only Block which is Genesis + gas_price: initial_gas_price, + total_supply: initial_total_supply, + challenges_result: vec![], + last_final_block: CryptoHash::default(), + last_ds_final_block: CryptoHash::default(), + prev_height: 0, + epoch_sync_data_hash: None, // Epoch Sync cannot be executed up to Genesis + approvals: vec![], + latest_protocol_version: genesis_protocol_version, + }; + let hash = BlockHeader::compute_hash( + CryptoHash::default(), + &inner_lite.try_to_vec().expect("Failed to serialize"), + &inner_rest.try_to_vec().expect("Failed to serialize"), + ); + Self::BlockHeaderV4(Arc::new(BlockHeaderV4 { + prev_hash: CryptoHash::default(), + inner_lite, + inner_rest, + signature: Signature::empty(KeyType::ED25519), + hash, + })) } } @@ -1039,7 +1027,7 @@ impl BlockHeader { } #[inline] - pub fn approvals(&self) -> &[Option] { + pub fn approvals(&self) -> &[Option>] { match self { BlockHeader::BlockHeaderV1(header) => &header.inner_rest.approvals, BlockHeader::BlockHeaderV2(header) => &header.inner_rest.approvals, diff --git a/core/primitives/src/epoch_manager.rs b/core/primitives/src/epoch_manager.rs index 74f7d3f1946..f12672f7cd6 100644 --- a/core/primitives/src/epoch_manager.rs +++ b/core/primitives/src/epoch_manager.rs @@ -1000,3 +1000,26 @@ pub enum SlashState { /// All other cases (tokens should be entirely slashed), Other, } + +#[cfg(feature = "new_epoch_sync")] +pub mod epoch_sync { + use crate::block_header::BlockHeader; + use crate::types::validator_stake::ValidatorStake; + use borsh::{BorshDeserialize, BorshSerialize}; + + #[derive(BorshSerialize, BorshDeserialize)] + pub struct BlockHeaderPair { + pub header: BlockHeader, + pub last_finalised_header: BlockHeader, + } + + /// Struct to keep all the info that is transferred for one epoch during Epoch Sync. + #[derive(BorshSerialize, BorshDeserialize)] + pub struct EpochSyncInfo { + /// None is only used for corner case of the first epoch + pub first: BlockHeaderPair, + pub last: BlockHeaderPair, + pub prev_last: BlockHeaderPair, + pub block_producers: Vec, + } +} diff --git a/core/primitives/src/lib.rs b/core/primitives/src/lib.rs index 5df216ba2af..b5252d399e5 100644 --- a/core/primitives/src/lib.rs +++ b/core/primitives/src/lib.rs @@ -1,13 +1,12 @@ pub use near_primitives_core::account; pub use near_primitives_core::borsh; pub use near_primitives_core::config; -pub use near_primitives_core::contract; pub use near_primitives_core::hash; pub use near_primitives_core::num_rational; pub use near_primitives_core::profile; pub use near_primitives_core::serialize; -pub use near_vm_runner::logic::{delegate_action, signable_message}; +pub mod action; pub mod block; pub mod block_header; pub mod challenge; @@ -21,6 +20,7 @@ pub mod runtime; pub mod sandbox; pub mod shard_layout; pub mod sharding; +pub mod signable_message; pub mod state; pub mod state_part; pub mod state_record; diff --git a/core/primitives/src/runtime/config.rs b/core/primitives/src/runtime/config.rs index 6d75728ffef..2111a034d62 100644 --- a/core/primitives/src/runtime/config.rs +++ b/core/primitives/src/runtime/config.rs @@ -1,5 +1,4 @@ //! Settings of the parameters of the runtime. -use crate::config::VMConfig; use crate::runtime::config_store::INITIAL_TESTNET_CONFIG; use crate::runtime::fees::RuntimeFeesConfig; use crate::runtime::parameter_table::ParameterTable; @@ -20,7 +19,7 @@ pub struct RuntimeConfig { /// /// This contains all the configuration parameters that are only required by /// the WASM runtime. - pub wasm_config: VMConfig, + pub wasm_config: near_vm_runner::logic::Config, /// Config that defines rules for account creation. pub account_creation_config: AccountCreationConfig, } @@ -40,7 +39,7 @@ impl RuntimeConfig { pub fn test() -> Self { RuntimeConfig { fees: RuntimeFeesConfig::test(), - wasm_config: VMConfig::test(), + wasm_config: near_vm_runner::logic::Config::test(), account_creation_config: AccountCreationConfig::default(), } } @@ -48,7 +47,7 @@ impl RuntimeConfig { pub fn free() -> Self { Self { fees: RuntimeFeesConfig::free(), - wasm_config: VMConfig::free(), + wasm_config: near_vm_runner::logic::Config::free(), account_creation_config: AccountCreationConfig::default(), } } diff --git a/core/primitives/src/runtime/config_store.rs b/core/primitives/src/runtime/config_store.rs index d3ac43396ea..6d623744bc3 100644 --- a/core/primitives/src/runtime/config_store.rs +++ b/core/primitives/src/runtime/config_store.rs @@ -7,7 +7,7 @@ use std::sync::Arc; macro_rules! include_config { ($file:expr) => { - include_str!(concat!("../../res/runtime_configs/", $file)) + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/res/runtime_configs/", $file)) }; } @@ -18,6 +18,7 @@ static BASE_CONFIG: &str = include_config!("parameters.yaml"); /// Stores pairs of protocol versions for which runtime config was updated and /// the file containing the diffs in bytes. static CONFIG_DIFFS: &[(ProtocolVersion, &str)] = &[ + (35, include_config!("35.yaml")), (42, include_config!("42.yaml")), (48, include_config!("48.yaml")), (49, include_config!("49.yaml")), @@ -32,6 +33,8 @@ static CONFIG_DIFFS: &[(ProtocolVersion, &str)] = &[ (59, include_config!("59.yaml")), (61, include_config!("61.yaml")), (62, include_config!("62.yaml")), + (63, include_config!("63.yaml")), + (129, include_config!("129.yaml")), ]; /// Testnet parameters for versions <= 29, which (incorrectly) differed from mainnet parameters @@ -149,10 +152,39 @@ mod tests { LowerDataReceiptAndEcrecoverBaseCost, LowerStorageCost, LowerStorageKeyLimit, }; use near_primitives_core::config::{ActionCosts, ExtCosts}; + use std::collections::HashSet; const GENESIS_PROTOCOL_VERSION: ProtocolVersion = 29; const RECEIPTS_DEPTH: u64 = 63; + #[test] + fn all_configs_are_specified() { + let file_versions = + std::fs::read_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/res/runtime_configs/")) + .expect("can open config directory"); + let mut files = file_versions + .into_iter() + .map(|de| { + de.expect("direntry should read successfully") + .path() + .file_name() + .expect("direntry should have a filename") + .to_string_lossy() + .into_owned() + }) + .collect::>(); + + for (ver, _) in super::CONFIG_DIFFS { + assert!(files.remove(&format!("{ver}.yaml")), "{ver}.yaml file is missing?"); + } + + for file in files { + let Some((name, "yaml")) = file.rsplit_once(".") else { continue }; + let Ok(version_num) = name.parse::() else { continue }; + panic!("CONFIG_DIFFS does not contain reference to the {version_num}.yaml file!"); + } + } + #[test] fn test_max_prepaid_gas() { let store = RuntimeConfigStore::new(None); @@ -231,8 +263,8 @@ mod tests { ); let expected_config = { - let first_diff = CONFIG_DIFFS[0].1.parse().unwrap(); - base_params.apply_diff(first_diff).unwrap(); + base_params.apply_diff(CONFIG_DIFFS[0].1.parse().unwrap()).unwrap(); + base_params.apply_diff(CONFIG_DIFFS[1].1.parse().unwrap()).unwrap(); RuntimeConfig::new(&base_params).unwrap() }; assert_eq!(**config, expected_config); @@ -240,8 +272,7 @@ mod tests { let config = store.get_config(LowerDataReceiptAndEcrecoverBaseCost.protocol_version()); assert_eq!(config.fees.fee(ActionCosts::new_data_receipt_base).send_sir, 36_486_732_312); let expected_config = { - let second_diff = CONFIG_DIFFS[1].1.parse().unwrap(); - base_params.apply_diff(second_diff).unwrap(); + base_params.apply_diff(CONFIG_DIFFS[2].1.parse().unwrap()).unwrap(); RuntimeConfig::new(&base_params).unwrap() }; assert_eq!(config.as_ref(), &expected_config); diff --git a/core/primitives/src/runtime/parameter_table.rs b/core/primitives/src/runtime/parameter_table.rs index 3cb95aeba44..dadc53cfd89 100644 --- a/core/primitives/src/runtime/parameter_table.rs +++ b/core/primitives/src/runtime/parameter_table.rs @@ -1,9 +1,10 @@ use super::config::{AccountCreationConfig, RuntimeConfig}; use near_primitives_core::account::id::ParseAccountError; -use near_primitives_core::config::{ExtCostsConfig, ParameterCost, VMConfig}; +use near_primitives_core::config::{ExtCostsConfig, ParameterCost}; use near_primitives_core::parameter::{FeeParameter, Parameter}; use near_primitives_core::runtime::fees::{Fee, RuntimeFeesConfig, StorageUsageConfig}; use near_primitives_core::types::AccountId; +use near_vm_runner::logic::{Config, StorageGetMode}; use num_rational::Rational32; use std::collections::BTreeMap; @@ -19,6 +20,7 @@ pub(crate) enum ParameterValue { // for u128, but this is currently impossible to express in YAML (see // `canonicalize_yaml_string`). String(String), + Flag(bool), } #[derive(thiserror::Error, Debug)] @@ -74,6 +76,22 @@ impl TryFrom<&ParameterValue> for u128 { } } +impl TryFrom<&ParameterValue> for bool { + type Error = ValueConversionError; + + fn try_from(value: &ParameterValue) -> Result { + match value { + ParameterValue::Flag(b) => Ok(*b), + ParameterValue::String(s) => match &**s { + "true" => Ok(true), + "false" => Ok(false), + _ => Err(ValueConversionError::ParseType("bool", value.clone())), + }, + _ => Err(ValueConversionError::ParseType("bool", value.clone())), + } + } +} + impl TryFrom<&ParameterValue> for Rational32 { type Error = ValueConversionError; @@ -177,6 +195,7 @@ impl core::fmt::Display for ParameterValue { ) } ParameterValue::String(v) => write!(f, "{v}"), + ParameterValue::Flag(b) => write!(f, "{b:?}"), } } } @@ -262,7 +281,7 @@ impl TryFrom<&ParameterTable> for RuntimeConfig { num_extra_bytes_record: params.get(Parameter::StorageNumExtraBytesRecord)?, }, }, - wasm_config: VMConfig { + wasm_config: Config { ext_costs: ExtCostsConfig { costs: enum_map::enum_map! { cost => params.get(cost.param())? @@ -270,8 +289,15 @@ impl TryFrom<&ParameterTable> for RuntimeConfig { }, grow_mem_cost: params.get(Parameter::WasmGrowMemCost)?, regular_op_cost: params.get(Parameter::WasmRegularOpCost)?, + disable_9393_fix: params.get(Parameter::Disable9393Fix)?, limit_config: serde_yaml::from_value(params.yaml_map(Parameter::vm_limits())) .map_err(InvalidConfigError::InvalidYaml)?, + fix_contract_loading_cost: params.get(Parameter::FixContractLoadingCost)?, + storage_get_mode: match params.get(Parameter::FlatStorageReads)? { + true => StorageGetMode::FlatStorage, + false => StorageGetMode::Trie, + }, + implicit_account_creation: params.get(Parameter::ImplicitAccountCreation)?, }, account_creation_config: AccountCreationConfig { min_allowed_top_level_account_length: params diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__0.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__0.json.snap index a1af3e3e846..96116984994 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__0.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__0.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 3856371, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": false, "limit_config": { "max_gas_burnt": 200000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__129.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__129.json.snap new file mode 100644 index 00000000000..abf58990f9b --- /dev/null +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__129.json.snap @@ -0,0 +1,212 @@ +--- +source: core/primitives/src/runtime/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": 17212011, + "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": 6812999, + "execution": 64572944 + }, + "function_call_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 + }, + "function_call_cost_per_byte": { + "send_sir": 2235934, + "send_not_sir": 2235934, + "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": 1925331, + "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": 216750, + "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": 56356845750, + "storage_read_key_byte": 30952533, + "storage_read_value_byte": 5611005, + "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 + }, + "grow_mem_cost": 1, + "regular_op_cost": 822756, + "disable_9393_fix": false, + "storage_get_mode": "FlatStorage", + "fix_contract_loading_cost": true, + "implicit_account_creation": 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": 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 + } + }, + "account_creation_config": { + "min_allowed_top_level_account_length": 32, + "registrar_account_id": "registrar" + } +} diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__35.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__35.json.snap new file mode 100644 index 00000000000..46132b2ccf1 --- /dev/null +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__35.json.snap @@ -0,0 +1,210 @@ +--- +source: core/primitives/src/runtime/config_store.rs +expression: config_view +--- +{ + "storage_amount_per_byte": "100000000000000000000", + "transaction_costs": { + "action_receipt_creation_config": { + "send_sir": 108059500000, + "send_not_sir": 108059500000, + "execution": 108059500000 + }, + "data_receipt_creation_config": { + "base_cost": { + "send_sir": 4697339419375, + "send_not_sir": 4697339419375, + "execution": 4697339419375 + }, + "cost_per_byte": { + "send_sir": 59357464, + "send_not_sir": 59357464, + "execution": 59357464 + } + }, + "action_creation_config": { + "create_account_cost": { + "send_sir": 99607375000, + "send_not_sir": 99607375000, + "execution": 99607375000 + }, + "deploy_contract_cost": { + "send_sir": 184765750000, + "send_not_sir": 184765750000, + "execution": 184765750000 + }, + "deploy_contract_cost_per_byte": { + "send_sir": 6812999, + "send_not_sir": 6812999, + "execution": 6812999 + }, + "function_call_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 + }, + "function_call_cost_per_byte": { + "send_sir": 2235934, + "send_not_sir": 2235934, + "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": 1925331, + "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": 216750, + "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": 3365369625000, + "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": 56356845750, + "storage_read_key_byte": 30952533, + "storage_read_value_byte": 5611005, + "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 + }, + "grow_mem_cost": 1, + "regular_op_cost": 3856371, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, + "limit_config": { + "max_gas_burnt": 200000000000000, + "max_stack_height": 16384, + "contract_prepare_version": 0, + "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": 4194304, + "max_length_storage_key": 4194304, + "max_length_storage_value": 4194304, + "max_promises_per_function_call_action": 1024, + "max_number_input_data_dependencies": 128, + "wasmer2_stack_limit": 102400, + "account_id_validity_rules_version": 0 + } + }, + "account_creation_config": { + "min_allowed_top_level_account_length": 32, + "registrar_account_id": "registrar" + } +} diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__42.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__42.json.snap index 521666a572a..485ea869e1c 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__42.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__42.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 3856371, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 200000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__48.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__48.json.snap index 54ba7733198..dc15d8267fd 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__48.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__48.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 2207874, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 200000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__49.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__49.json.snap index 5845165e3da..ddbd164f4de 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__49.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__49.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 822756, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 200000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__50.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__50.json.snap index 779a7a28ef8..6179e7e450c 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__50.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__50.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 822756, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 200000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__52.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__52.json.snap index d0f47addd07..61a5d4fe3c2 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__52.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__52.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 822756, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 300000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__53.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__53.json.snap index 48bd59de1c8..dc43057d016 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__53.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__53.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 822756, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 300000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__57.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__57.json.snap index cc042ce7d63..a5ba3284abc 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__57.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__57.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 822756, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 300000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__59.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__59.json.snap index ef2462605a3..408098b1f20 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__59.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__59.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 822756, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 300000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__61.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__61.json.snap index ef2462605a3..2275de83df6 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__61.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__61.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 822756, + "disable_9393_fix": false, + "storage_get_mode": "FlatStorage", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 300000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__62.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__62.json.snap index 60a5bcddd39..026a3c603f3 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__62.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__62.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 822756, + "disable_9393_fix": true, + "storage_get_mode": "FlatStorage", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 300000000000000, "max_stack_height": 262144, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__63.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__63.json.snap new file mode 100644 index 00000000000..846427860b5 --- /dev/null +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__63.json.snap @@ -0,0 +1,212 @@ +--- +source: core/primitives/src/runtime/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": 17212011, + "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": 6812999, + "execution": 64572944 + }, + "function_call_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 + }, + "function_call_cost_per_byte": { + "send_sir": 2235934, + "send_not_sir": 2235934, + "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": 1925331, + "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": 216750, + "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": 56356845750, + "storage_read_key_byte": 30952533, + "storage_read_value_byte": 5611005, + "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 + }, + "grow_mem_cost": 1, + "regular_op_cost": 822756, + "disable_9393_fix": false, + "storage_get_mode": "FlatStorage", + "fix_contract_loading_cost": false, + "implicit_account_creation": 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": 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 + } + }, + "account_creation_config": { + "min_allowed_top_level_account_length": 32, + "registrar_account_id": "registrar" + } +} diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_0.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_0.json.snap index a1af3e3e846..96116984994 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_0.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_0.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 3856371, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": false, "limit_config": { "max_gas_burnt": 200000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_129.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_129.json.snap new file mode 100644 index 00000000000..abf58990f9b --- /dev/null +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_129.json.snap @@ -0,0 +1,212 @@ +--- +source: core/primitives/src/runtime/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": 17212011, + "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": 6812999, + "execution": 64572944 + }, + "function_call_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 + }, + "function_call_cost_per_byte": { + "send_sir": 2235934, + "send_not_sir": 2235934, + "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": 1925331, + "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": 216750, + "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": 56356845750, + "storage_read_key_byte": 30952533, + "storage_read_value_byte": 5611005, + "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 + }, + "grow_mem_cost": 1, + "regular_op_cost": 822756, + "disable_9393_fix": false, + "storage_get_mode": "FlatStorage", + "fix_contract_loading_cost": true, + "implicit_account_creation": 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": 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 + } + }, + "account_creation_config": { + "min_allowed_top_level_account_length": 32, + "registrar_account_id": "registrar" + } +} diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_35.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_35.json.snap new file mode 100644 index 00000000000..46132b2ccf1 --- /dev/null +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_35.json.snap @@ -0,0 +1,210 @@ +--- +source: core/primitives/src/runtime/config_store.rs +expression: config_view +--- +{ + "storage_amount_per_byte": "100000000000000000000", + "transaction_costs": { + "action_receipt_creation_config": { + "send_sir": 108059500000, + "send_not_sir": 108059500000, + "execution": 108059500000 + }, + "data_receipt_creation_config": { + "base_cost": { + "send_sir": 4697339419375, + "send_not_sir": 4697339419375, + "execution": 4697339419375 + }, + "cost_per_byte": { + "send_sir": 59357464, + "send_not_sir": 59357464, + "execution": 59357464 + } + }, + "action_creation_config": { + "create_account_cost": { + "send_sir": 99607375000, + "send_not_sir": 99607375000, + "execution": 99607375000 + }, + "deploy_contract_cost": { + "send_sir": 184765750000, + "send_not_sir": 184765750000, + "execution": 184765750000 + }, + "deploy_contract_cost_per_byte": { + "send_sir": 6812999, + "send_not_sir": 6812999, + "execution": 6812999 + }, + "function_call_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 + }, + "function_call_cost_per_byte": { + "send_sir": 2235934, + "send_not_sir": 2235934, + "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": 1925331, + "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": 216750, + "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": 3365369625000, + "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": 56356845750, + "storage_read_key_byte": 30952533, + "storage_read_value_byte": 5611005, + "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 + }, + "grow_mem_cost": 1, + "regular_op_cost": 3856371, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, + "limit_config": { + "max_gas_burnt": 200000000000000, + "max_stack_height": 16384, + "contract_prepare_version": 0, + "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": 4194304, + "max_length_storage_key": 4194304, + "max_length_storage_value": 4194304, + "max_promises_per_function_call_action": 1024, + "max_number_input_data_dependencies": 128, + "wasmer2_stack_limit": 102400, + "account_id_validity_rules_version": 0 + } + }, + "account_creation_config": { + "min_allowed_top_level_account_length": 32, + "registrar_account_id": "registrar" + } +} diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_42.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_42.json.snap index 521666a572a..485ea869e1c 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_42.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_42.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 3856371, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 200000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_48.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_48.json.snap index 54ba7733198..dc15d8267fd 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_48.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_48.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 2207874, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 200000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_49.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_49.json.snap index 5845165e3da..ddbd164f4de 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_49.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_49.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 822756, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 200000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_50.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_50.json.snap index 779a7a28ef8..6179e7e450c 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_50.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_50.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 822756, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 200000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_52.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_52.json.snap index d0f47addd07..61a5d4fe3c2 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_52.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_52.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 822756, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 300000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_53.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_53.json.snap index 48bd59de1c8..dc43057d016 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_53.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_53.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 822756, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 300000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_57.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_57.json.snap index cc042ce7d63..a5ba3284abc 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_57.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_57.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 822756, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 300000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_59.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_59.json.snap index ef2462605a3..408098b1f20 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_59.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_59.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 822756, + "disable_9393_fix": false, + "storage_get_mode": "Trie", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 300000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_61.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_61.json.snap index ef2462605a3..2275de83df6 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_61.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_61.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 822756, + "disable_9393_fix": false, + "storage_get_mode": "FlatStorage", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 300000000000000, "max_stack_height": 16384, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_62.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_62.json.snap index 60a5bcddd39..026a3c603f3 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_62.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_62.json.snap @@ -172,6 +172,10 @@ expression: config_view }, "grow_mem_cost": 1, "regular_op_cost": 822756, + "disable_9393_fix": true, + "storage_get_mode": "FlatStorage", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 300000000000000, "max_stack_height": 262144, diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_63.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_63.json.snap new file mode 100644 index 00000000000..846427860b5 --- /dev/null +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_63.json.snap @@ -0,0 +1,212 @@ +--- +source: core/primitives/src/runtime/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": 17212011, + "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": 6812999, + "execution": 64572944 + }, + "function_call_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 + }, + "function_call_cost_per_byte": { + "send_sir": 2235934, + "send_not_sir": 2235934, + "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": 1925331, + "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": 216750, + "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": 56356845750, + "storage_read_key_byte": 30952533, + "storage_read_value_byte": 5611005, + "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 + }, + "grow_mem_cost": 1, + "regular_op_cost": 822756, + "disable_9393_fix": false, + "storage_get_mode": "FlatStorage", + "fix_contract_loading_cost": false, + "implicit_account_creation": 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": 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 + } + }, + "account_creation_config": { + "min_allowed_top_level_account_length": 32, + "registrar_account_id": "registrar" + } +} diff --git a/core/primitives/src/sharding.rs b/core/primitives/src/sharding.rs index 12bc7026273..16c1dd6446c 100644 --- a/core/primitives/src/sharding.rs +++ b/core/primitives/src/sharding.rs @@ -10,6 +10,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use near_crypto::Signature; use reed_solomon_erasure::galois_8::{Field, ReedSolomon}; use reed_solomon_erasure::ReconstructShard; +use std::cmp::Ordering; use std::sync::Arc; #[derive( @@ -601,6 +602,21 @@ pub struct ShardProof { /// For each Merkle proof there is a subset of receipts which may be proven. pub struct ReceiptProof(pub Vec, pub ShardProof); +// Implement ordering to ensure `ReceiptProofs` are ordered consistently, +// because we expect messages with ReceiptProofs to be deterministic. +impl PartialOrd for ReceiptProof { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ReceiptProof { + fn cmp(&self, other: &Self) -> Ordering { + (self.1.from_shard_id, self.1.to_shard_id) + .cmp(&(other.1.from_shard_id, other.1.to_shard_id)) + } +} + #[derive(BorshSerialize, BorshDeserialize, Debug, Clone, Eq, PartialEq)] pub struct PartialEncodedChunkPart { pub part_ord: u64, diff --git a/runtime/near-vm-runner/src/logic/signable_message.rs b/core/primitives/src/signable_message.rs similarity index 95% rename from runtime/near-vm-runner/src/logic/signable_message.rs rename to core/primitives/src/signable_message.rs index d862cd962f7..efdd489acb1 100644 --- a/runtime/near-vm-runner/src/logic/signable_message.rs +++ b/core/primitives/src/signable_message.rs @@ -223,7 +223,7 @@ mod tests { use near_crypto::{InMemorySigner, KeyType, PublicKey}; use super::*; - use crate::logic::delegate_action::{DelegateAction, SignedDelegateAction}; + use crate::action::delegate::{DelegateAction, SignedDelegateAction}; // Note: this is currently a simplified copy of near-primitives::test_utils::create_user_test_signer // TODO: consider whether it’s worth re-unifying them? it’s test-only code anyway. @@ -241,10 +241,7 @@ mod tests { let delegate_action = delegate_action(sender_id, receiver_id, signer.public_key()); let signable = SignableMessage::new(&delegate_action, SignableMessageType::DelegateAction); - let signed = SignedDelegateAction { - signature: signable.sign(&signer), - delegate_action: delegate_action, - }; + let signed = SignedDelegateAction { signature: signable.sign(&signer), delegate_action }; assert!(signed.verify()); } @@ -262,10 +259,7 @@ mod tests { discriminant: MessageDiscriminant::new_on_chain(wrong_nep).unwrap(), msg: &delegate_action, }; - let signed = SignedDelegateAction { - signature: signable.sign(&signer), - delegate_action: delegate_action, - }; + let signed = SignedDelegateAction { signature: signable.sign(&signer), delegate_action }; assert!(!signed.verify()); } @@ -282,10 +276,7 @@ mod tests { // here we use it as an off-chain only signature let wrong_discriminant = MessageDiscriminant::new_off_chain(correct_nep).unwrap(); let signable = SignableMessage { discriminant: wrong_discriminant, msg: &delegate_action }; - let signed = SignedDelegateAction { - signature: signable.sign(&signer), - delegate_action: delegate_action, - }; + let signed = SignedDelegateAction { signature: signable.sign(&signer), delegate_action }; assert!(!signed.verify()); } diff --git a/core/primitives/src/snapshots/near_primitives__views__tests__runtime_config_view.snap b/core/primitives/src/snapshots/near_primitives__views__tests__runtime_config_view.snap index 4b2bd548491..d80617e4c33 100644 --- a/core/primitives/src/snapshots/near_primitives__views__tests__runtime_config_view.snap +++ b/core/primitives/src/snapshots/near_primitives__views__tests__runtime_config_view.snap @@ -172,6 +172,10 @@ expression: "&view" }, "grow_mem_cost": 1, "regular_op_cost": 3856371, + "disable_9393_fix": false, + "storage_get_mode": "FlatStorage", + "fix_contract_loading_cost": false, + "implicit_account_creation": true, "limit_config": { "max_gas_burnt": 200000000000000, "max_stack_height": 262144, diff --git a/core/primitives/src/test_utils.rs b/core/primitives/src/test_utils.rs index 4abfe6879b7..a3e32953712 100644 --- a/core/primitives/src/test_utils.rs +++ b/core/primitives/src/test_utils.rs @@ -6,9 +6,6 @@ use near_primitives_core::types::ProtocolVersion; use crate::account::{AccessKey, AccessKeyPermission, Account}; use crate::block::Block; -#[cfg(not(feature = "protocol_feature_block_header_v4"))] -use crate::block::BlockV2; -#[cfg(feature = "protocol_feature_block_header_v4")] use crate::block::BlockV3; use crate::block_header::BlockHeader; use crate::errors::EpochError; @@ -63,12 +60,12 @@ impl Transaction { gas: Gas, deposit: Balance, ) -> Self { - self.actions.push(Action::FunctionCall(FunctionCallAction { + self.actions.push(Action::FunctionCall(Box::new(FunctionCallAction { method_name, args, gas, deposit, - })); + }))); self } @@ -78,16 +75,16 @@ impl Transaction { } pub fn stake(mut self, stake: Balance, public_key: PublicKey) -> Self { - self.actions.push(Action::Stake(StakeAction { stake, public_key })); + self.actions.push(Action::Stake(Box::new(StakeAction { stake, public_key }))); self } pub fn add_key(mut self, public_key: PublicKey, access_key: AccessKey) -> Self { - self.actions.push(Action::AddKey(AddKeyAction { public_key, access_key })); + self.actions.push(Action::AddKey(Box::new(AddKeyAction { public_key, access_key }))); self } pub fn delete_key(mut self, public_key: PublicKey) -> Self { - self.actions.push(Action::DeleteKey(DeleteKeyAction { public_key })); + self.actions.push(Action::DeleteKey(Box::new(DeleteKeyAction { public_key }))); self } @@ -148,7 +145,7 @@ impl SignedTransaction { signer_id.clone(), signer_id, signer, - vec![Action::Stake(StakeAction { stake, public_key })], + vec![Action::Stake(Box::new(StakeAction { stake, public_key }))], block_hash, ) } @@ -169,10 +166,10 @@ impl SignedTransaction { signer, vec![ Action::CreateAccount(CreateAccountAction {}), - Action::AddKey(AddKeyAction { + Action::AddKey(Box::new(AddKeyAction { public_key, access_key: AccessKey { nonce: 0, permission: AccessKeyPermission::FullAccess }, - }), + })), Action::Transfer(TransferAction { deposit: amount }), ], block_hash, @@ -196,10 +193,10 @@ impl SignedTransaction { signer, vec![ Action::CreateAccount(CreateAccountAction {}), - Action::AddKey(AddKeyAction { + Action::AddKey(Box::new(AddKeyAction { public_key, access_key: AccessKey { nonce: 0, permission: AccessKeyPermission::FullAccess }, - }), + })), Action::Transfer(TransferAction { deposit: amount }), Action::DeployContract(DeployContractAction { code }), ], @@ -223,7 +220,12 @@ impl SignedTransaction { signer_id, receiver_id, signer, - vec![Action::FunctionCall(FunctionCallAction { args, method_name, gas, deposit })], + vec![Action::FunctionCall(Box::new(FunctionCallAction { + args, + method_name, + gas, + deposit, + }))], block_hash, ) } @@ -259,17 +261,6 @@ impl SignedTransaction { } impl Block { - #[cfg(not(feature = "protocol_feature_block_header_v4"))] - pub fn get_mut(&mut self) -> &mut BlockV2 { - match self { - Block::BlockV1(_) | Block::BlockV3(_) => { - panic!("older block version should not appear in tests") - } - Block::BlockV2(block) => Arc::make_mut(block), - } - } - - #[cfg(feature = "protocol_feature_block_header_v4")] pub fn get_mut(&mut self) -> &mut BlockV3 { match self { Block::BlockV1(_) | Block::BlockV2(_) => { @@ -281,19 +272,6 @@ impl Block { } impl BlockHeader { - #[cfg(not(feature = "protocol_feature_block_header_v4"))] - pub fn get_mut(&mut self) -> &mut crate::block_header::BlockHeaderV3 { - match self { - BlockHeader::BlockHeaderV1(_) - | BlockHeader::BlockHeaderV2(_) - | BlockHeader::BlockHeaderV4(_) => { - panic!("old header should not appear in tests") - } - BlockHeader::BlockHeaderV3(header) => Arc::make_mut(header), - } - } - - #[cfg(feature = "protocol_feature_block_header_v4")] pub fn get_mut(&mut self) -> &mut crate::block_header::BlockHeaderV4 { match self { BlockHeader::BlockHeaderV1(_) @@ -382,7 +360,7 @@ pub struct TestBlockBuilder { epoch_id: EpochId, next_epoch_id: EpochId, next_bp_hash: CryptoHash, - approvals: Vec>, + approvals: Vec>>, block_merkle_root: CryptoHash, } @@ -422,7 +400,7 @@ impl TestBlockBuilder { self.next_bp_hash = next_bp_hash; self } - pub fn approvals(mut self, approvals: Vec>) -> Self { + pub fn approvals(mut self, approvals: Vec>>) -> Self { self.approvals = approvals; self } @@ -435,6 +413,7 @@ impl TestBlockBuilder { } pub fn build(self) -> Block { + tracing::debug!(target: "test", height=self.height, ?self.epoch_id, "produce block"); Block::produce( PROTOCOL_VERSION, PROTOCOL_VERSION, diff --git a/core/primitives/src/transaction.rs b/core/primitives/src/transaction.rs index 007bfb2ca94..a33b26ffb4e 100644 --- a/core/primitives/src/transaction.rs +++ b/core/primitives/src/transaction.rs @@ -11,7 +11,7 @@ use std::borrow::Borrow; use std::fmt; use std::hash::{Hash, Hasher}; -pub use near_vm_runner::logic::action::{ +pub use crate::action::{ Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeleteKeyAction, DeployContractAction, FunctionCallAction, StakeAction, TransferAction, }; @@ -332,15 +332,18 @@ mod tests { actions: vec![ Action::CreateAccount(CreateAccountAction {}), Action::DeployContract(DeployContractAction { code: vec![1, 2, 3] }), - Action::FunctionCall(FunctionCallAction { + Action::FunctionCall(Box::new(FunctionCallAction { method_name: "qqq".to_string(), args: vec![1, 2, 3], gas: 1_000, deposit: 1_000_000, - }), + })), Action::Transfer(TransferAction { deposit: 123 }), - Action::Stake(StakeAction { public_key: public_key.clone(), stake: 1_000_000 }), - Action::AddKey(AddKeyAction { + Action::Stake(Box::new(StakeAction { + public_key: public_key.clone(), + stake: 1_000_000, + })), + Action::AddKey(Box::new(AddKeyAction { public_key: public_key.clone(), access_key: AccessKey { nonce: 0, @@ -350,8 +353,8 @@ mod tests { method_names: vec!["www".to_string()], }), }, - }), - Action::DeleteKey(DeleteKeyAction { public_key }), + })), + Action::DeleteKey(Box::new(DeleteKeyAction { public_key })), Action::DeleteAccount(DeleteAccountAction { beneficiary_id: "123".parse().unwrap(), }), diff --git a/core/primitives/src/trie_key.rs b/core/primitives/src/trie_key.rs index 98daf87b438..6e1b3e6bf24 100644 --- a/core/primitives/src/trie_key.rs +++ b/core/primitives/src/trie_key.rs @@ -356,7 +356,6 @@ pub mod trie_key_parsers { Ok(TrieKey::AccessKey { account_id, public_key }) } - #[allow(unused)] pub fn parse_account_id_from_raw_key( raw_key: &[u8], ) -> Result, std::io::Error> { diff --git a/core/primitives/src/types.rs b/core/primitives/src/types.rs index cc8d99c0f6a..1343c5aa51e 100644 --- a/core/primitives/src/types.rs +++ b/core/primitives/src/types.rs @@ -154,7 +154,7 @@ impl StateChangesKinds { } /// A structure used to index state changes due to transaction/receipt processing and other things. -#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq)] pub enum StateChangeCause { /// A type of update that does not get finalized. Used for verification and execution of /// immutable smart contract methods. Attempt fo finalize a `TrieUpdate` containing such @@ -960,7 +960,7 @@ pub trait EpochInfoProvider { } /// Mode of the trie cache. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum TrieCacheMode { /// In this mode we put each visited node to LRU cache to optimize performance. /// Presence of any exact node is not guaranteed. diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index 0681d198445..b4db20805ba 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -4,18 +4,15 @@ //! type gets changed, the view should preserve the old shape and only re-map the necessary bits //! from the source structure in the relevant `From` impl. use crate::account::{AccessKey, AccessKeyPermission, Account, FunctionCallPermission}; +use crate::action::delegate::{DelegateAction, SignedDelegateAction}; use crate::block::{Block, BlockHeader, Tip}; use crate::block_header::{ BlockHeaderInnerLite, BlockHeaderInnerRest, BlockHeaderInnerRestV2, BlockHeaderInnerRestV3, BlockHeaderV1, BlockHeaderV2, BlockHeaderV3, }; -#[cfg(feature = "protocol_feature_block_header_v4")] use crate::block_header::{BlockHeaderInnerRestV4, BlockHeaderV4}; use crate::challenge::{Challenge, ChallengesResult}; -#[cfg(feature = "protocol_feature_block_header_v4")] use crate::checked_feature; -use crate::contract::ContractCode; -use crate::delegate_action::{DelegateAction, SignedDelegateAction}; use crate::errors::TxExecutionError; use crate::hash::{hash, CryptoHash}; use crate::merkle::{combine_hash, MerklePath}; @@ -44,9 +41,10 @@ use borsh::{BorshDeserialize, BorshSerialize}; use chrono::DateTime; use near_crypto::{PublicKey, Signature}; use near_fmt::{AbbrBytes, Slice}; -use near_primitives_core::config::{ActionCosts, ExtCosts, ParameterCost, VMConfig}; +use near_primitives_core::config::{ActionCosts, ExtCosts, ParameterCost}; use near_primitives_core::runtime::fees::Fee; use near_vm_runner::logic::CompiledContractCache; +use near_vm_runner::ContractCode; use num_rational::Rational32; use serde_with::base64::Base64; use serde_with::serde_as; @@ -728,7 +726,6 @@ pub struct BlockHeaderView { pub hash: CryptoHash, pub prev_hash: CryptoHash, pub prev_state_root: CryptoHash, - #[cfg(feature = "protocol_feature_block_header_v4")] pub block_body_hash: Option, pub chunk_receipts_root: CryptoHash, pub chunk_headers_root: CryptoHash, @@ -760,7 +757,7 @@ pub struct BlockHeaderView { pub next_bp_hash: CryptoHash, pub block_merkle_root: CryptoHash, pub epoch_sync_data_hash: Option, - pub approvals: Vec>, + pub approvals: Vec>>, pub signature: Signature, pub latest_protocol_version: ProtocolVersion, } @@ -775,7 +772,6 @@ impl From for BlockHeaderView { hash: *header.hash(), prev_hash: *header.prev_hash(), prev_state_root: *header.prev_state_root(), - #[cfg(feature = "protocol_feature_block_header_v4")] block_body_hash: header.block_body_hash(), chunk_receipts_root: *header.chunk_receipts_root(), chunk_headers_root: *header.chunk_headers_root(), @@ -885,46 +881,7 @@ impl From for BlockHeader { }; header.init(); BlockHeader::BlockHeaderV2(Arc::new(header)) - } else { - #[cfg(feature = "protocol_feature_block_header_v4")] - if checked_feature!( - "protocol_feature_block_header_v4", - BlockHeaderV4, - view.latest_protocol_version - ) { - let mut header = BlockHeaderV4 { - prev_hash: view.prev_hash, - inner_lite, - inner_rest: BlockHeaderInnerRestV4 { - block_body_hash: view.block_body_hash.unwrap_or_default(), - chunk_receipts_root: view.chunk_receipts_root, - chunk_headers_root: view.chunk_headers_root, - chunk_tx_root: view.chunk_tx_root, - challenges_root: view.challenges_root, - random_value: view.random_value, - validator_proposals: view - .validator_proposals - .into_iter() - .map(Into::into) - .collect(), - chunk_mask: view.chunk_mask, - gas_price: view.gas_price, - block_ordinal: view.block_ordinal.unwrap_or(0), - total_supply: view.total_supply, - challenges_result: view.challenges_result, - last_final_block: view.last_final_block, - last_ds_final_block: view.last_ds_final_block, - prev_height: view.prev_height.unwrap_or_default(), - epoch_sync_data_hash: view.epoch_sync_data_hash, - approvals: view.approvals.clone(), - latest_protocol_version: view.latest_protocol_version, - }, - signature: view.signature, - hash: CryptoHash::default(), - }; - header.init(); - return BlockHeader::BlockHeaderV4(Arc::new(header)); - } + } else if !checked_feature!("stable", BlockHeaderV4, view.latest_protocol_version) { let mut header = BlockHeaderV3 { prev_hash: view.prev_hash, inner_lite, @@ -956,6 +913,39 @@ impl From for BlockHeader { }; header.init(); BlockHeader::BlockHeaderV3(Arc::new(header)) + } else { + let mut header = BlockHeaderV4 { + prev_hash: view.prev_hash, + inner_lite, + inner_rest: BlockHeaderInnerRestV4 { + block_body_hash: view.block_body_hash.unwrap_or_default(), + chunk_receipts_root: view.chunk_receipts_root, + chunk_headers_root: view.chunk_headers_root, + chunk_tx_root: view.chunk_tx_root, + challenges_root: view.challenges_root, + random_value: view.random_value, + validator_proposals: view + .validator_proposals + .into_iter() + .map(Into::into) + .collect(), + chunk_mask: view.chunk_mask, + gas_price: view.gas_price, + block_ordinal: view.block_ordinal.unwrap_or(0), + total_supply: view.total_supply, + challenges_result: view.challenges_result, + last_final_block: view.last_final_block, + last_ds_final_block: view.last_ds_final_block, + prev_height: view.prev_height.unwrap_or_default(), + epoch_sync_data_hash: view.epoch_sync_data_hash, + approvals: view.approvals.clone(), + latest_protocol_version: view.latest_protocol_version, + }, + signature: view.signature, + hash: CryptoHash::default(), + }; + header.init(); + BlockHeader::BlockHeaderV4(Arc::new(header)) } } } @@ -1241,31 +1231,28 @@ impl TryFrom for Action { Action::DeployContract(DeployContractAction { code }) } ActionView::FunctionCall { method_name, args, gas, deposit } => { - Action::FunctionCall(FunctionCallAction { + Action::FunctionCall(Box::new(FunctionCallAction { method_name, args: args.into(), gas, deposit, - }) + })) } ActionView::Transfer { deposit } => Action::Transfer(TransferAction { deposit }), ActionView::Stake { stake, public_key } => { - Action::Stake(StakeAction { stake, public_key }) + Action::Stake(Box::new(StakeAction { stake, public_key })) } ActionView::AddKey { public_key, access_key } => { - Action::AddKey(AddKeyAction { public_key, access_key: access_key.into() }) + Action::AddKey(Box::new(AddKeyAction { public_key, access_key: access_key.into() })) } ActionView::DeleteKey { public_key } => { - Action::DeleteKey(DeleteKeyAction { public_key }) + Action::DeleteKey(Box::new(DeleteKeyAction { public_key })) } ActionView::DeleteAccount { beneficiary_id } => { Action::DeleteAccount(DeleteAccountAction { beneficiary_id }) } ActionView::Delegate { delegate_action, signature } => { - Action::Delegate(SignedDelegateAction { - delegate_action: delegate_action, - signature, - }) + Action::Delegate(Box::new(SignedDelegateAction { delegate_action, signature })) } }) } @@ -2024,7 +2011,7 @@ pub struct LightClientBlockView { pub inner_lite: BlockHeaderInnerLiteView, pub inner_rest_hash: CryptoHash, pub next_bps: Option>, - pub approvals_after_next: Vec>, + pub approvals_after_next: Vec>>, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, BorshDeserialize, BorshSerialize)] @@ -2486,31 +2473,48 @@ pub struct VMConfigView { /// Gas cost of a regular operation. pub regular_op_cost: u32, + /// See [`VMConfig::disable_9393_fix`]. + pub disable_9393_fix: bool, + /// See [`VMConfig::flat_storage_reads`]. + pub storage_get_mode: near_vm_runner::logic::StorageGetMode, + /// See [`VMConfig::fix_contract_loading_cost`]. + pub fix_contract_loading_cost: bool, + /// See [`VMConfig::implicit_account_creation`]. + pub implicit_account_creation: bool, + /// Describes limits for VM and Runtime. /// /// TODO: Consider changing this to `VMLimitConfigView` to avoid dependency /// on runtime. - pub limit_config: near_primitives_core::config::VMLimitConfig, + pub limit_config: near_vm_runner::logic::LimitConfig, } -impl From for VMConfigView { - fn from(config: VMConfig) -> Self { +impl From for VMConfigView { + fn from(config: near_vm_runner::logic::Config) -> Self { Self { ext_costs: ExtCostsConfigView::from(config.ext_costs), grow_mem_cost: config.grow_mem_cost, regular_op_cost: config.regular_op_cost, + disable_9393_fix: config.disable_9393_fix, limit_config: config.limit_config, + storage_get_mode: config.storage_get_mode, + fix_contract_loading_cost: config.fix_contract_loading_cost, + implicit_account_creation: config.implicit_account_creation, } } } -impl From for VMConfig { +impl From for near_vm_runner::logic::Config { fn from(view: VMConfigView) -> Self { Self { ext_costs: near_primitives_core::config::ExtCostsConfig::from(view.ext_costs), grow_mem_cost: view.grow_mem_cost, regular_op_cost: view.regular_op_cost, + disable_9393_fix: view.disable_9393_fix, limit_config: view.limit_config, + storage_get_mode: view.storage_get_mode, + fix_contract_loading_cost: view.fix_contract_loading_cost, + implicit_account_creation: view.implicit_account_creation, } } } diff --git a/core/store/Cargo.toml b/core/store/Cargo.toml index cf45ac310b2..461a820d510 100644 --- a/core/store/Cargo.toml +++ b/core/store/Cargo.toml @@ -66,6 +66,7 @@ no_cache = [] single_thread_rocksdb = [] # Deactivate RocksDB IO background threads test_features = [] serialize_all_state_changes = [] +new_epoch_sync = [] nightly_protocol = [ "near-chain-configs/nightly_protocol", diff --git a/core/store/src/cold_storage.rs b/core/store/src/cold_storage.rs index 0bdfc54bac2..f567e8f3f64 100644 --- a/core/store/src/cold_storage.rs +++ b/core/store/src/cold_storage.rs @@ -424,7 +424,6 @@ where } } -#[allow(dead_code)] impl StoreWithCache<'_> { pub fn iter_prefix_with_callback( &mut self, diff --git a/core/store/src/columns.rs b/core/store/src/columns.rs index 3fc8b9fc4f3..650bf0d43f9 100644 --- a/core/store/src/columns.rs +++ b/core/store/src/columns.rs @@ -277,6 +277,12 @@ pub enum DBCol { /// - *Rows*: arbitrary string, see `crate::db::FLAT_STATE_VALUES_INLINING_MIGRATION_STATUS_KEY` for example /// - *Column type*: arbitrary bytes Misc, + /// Column to store data for Epoch Sync. + /// Does not contain data for genesis epoch. + /// - *Rows*: `epoch_id` + /// - *Column type*: `EpochSyncInfo + #[cfg(feature = "new_epoch_sync")] + EpochSyncInfo, } /// Defines different logical parts of a db key. @@ -470,7 +476,9 @@ impl DBCol { | DBCol::FlatState | DBCol::FlatStateChanges | DBCol::FlatStateDeltaMetadata - | DBCol::FlatStorageStatus => false, + | DBCol::FlatStorageStatus => false, + #[cfg(feature = "new_epoch_sync")] + DBCol::EpochSyncInfo => false } } @@ -539,6 +547,8 @@ impl DBCol { DBCol::FlatStateChanges => &[DBKeyType::ShardUId, DBKeyType::BlockHash], DBCol::FlatStateDeltaMetadata => &[DBKeyType::ShardUId, DBKeyType::BlockHash], DBCol::FlatStorageStatus => &[DBKeyType::ShardUId], + #[cfg(feature = "new_epoch_sync")] + DBCol::EpochSyncInfo => &[DBKeyType::EpochId], } } } diff --git a/core/store/src/db.rs b/core/store/src/db.rs index 83c4b971803..75dce86ac4e 100644 --- a/core/store/src/db.rs +++ b/core/store/src/db.rs @@ -33,6 +33,7 @@ pub const GENESIS_JSON_HASH_KEY: &[u8; 17] = b"GENESIS_JSON_HASH"; pub const GENESIS_STATE_ROOTS_KEY: &[u8; 19] = b"GENESIS_STATE_ROOTS"; pub const COLD_HEAD_KEY: &[u8; 9] = b"COLD_HEAD"; pub const STATE_SYNC_DUMP_KEY: &[u8; 15] = b"STATE_SYNC_DUMP"; +pub const STATE_SNAPSHOT_KEY: &[u8; 18] = b"STATE_SNAPSHOT_KEY"; // `DBCol::Misc` keys pub const FLAT_STATE_VALUES_INLINING_MIGRATION_STATUS_KEY: &[u8] = diff --git a/core/store/src/db/rocksdb.rs b/core/store/src/db/rocksdb.rs index bd448f0a194..aff7d91b506 100644 --- a/core/store/src/db/rocksdb.rs +++ b/core/store/src/db/rocksdb.rs @@ -297,6 +297,13 @@ impl RocksDB { _ => unreachable!(), } } + + pub fn compact_column(&self, col: DBCol) -> io::Result<()> { + let none = Option::<&[u8]>::None; + tracing::info!(target: "db", column = %col, "Compact column"); + self.db.compact_range_cf(self.cf_handle(col)?, none, none); + Ok(()) + } } impl Database for RocksDB { @@ -375,9 +382,8 @@ impl Database for RocksDB { } fn compact(&self) -> io::Result<()> { - let none = Option::<&[u8]>::None; for col in DBCol::iter() { - self.db.compact_range_cf(self.cf_handle(col)?, none, none); + self.compact_column(col)?; } Ok(()) } diff --git a/core/store/src/flat/chunk_view.rs b/core/store/src/flat/chunk_view.rs index 1a402016c01..96099da6562 100644 --- a/core/store/src/flat/chunk_view.rs +++ b/core/store/src/flat/chunk_view.rs @@ -21,7 +21,6 @@ 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. - #[allow(unused)] store: Store, /// 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. diff --git a/core/store/src/flat/delta.rs b/core/store/src/flat/delta.rs index d7bffeea840..cf493a8d809 100644 --- a/core/store/src/flat/delta.rs +++ b/core/store/src/flat/delta.rs @@ -3,7 +3,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use near_primitives::hash::hash; use near_primitives::shard_layout::ShardUId; use near_primitives::state::{FlatStateValue, ValueRef}; -use near_primitives::types::RawStateChangesWithTrieKey; +use near_primitives::types::{BlockHeight, RawStateChangesWithTrieKey}; use std::collections::HashMap; use std::sync::Arc; @@ -16,9 +16,19 @@ pub struct FlatStateDelta { pub changes: FlatStateChanges, } +#[derive(BorshSerialize, BorshDeserialize, Debug, Clone, Copy, serde::Serialize)] +pub struct BlockWithChangesInfo { + pub(crate) hash: CryptoHash, + pub(crate) height: BlockHeight, +} + #[derive(BorshSerialize, BorshDeserialize, Debug, Clone, Copy, serde::Serialize)] pub struct FlatStateDeltaMetadata { pub block: BlockInfo, + /// `None` if the block itself has flat state changes. + /// `Some` if the block has no flat state changes, and contains + /// info of the last block with some flat state changes. + pub prev_block_with_changes: Option, } #[derive(BorshSerialize, BorshDeserialize, Debug)] @@ -99,6 +109,15 @@ impl FlatStateChanges { Self(delta) } + pub fn from_raw_key_value(entries: &[(Vec, Option>)]) -> Self { + let mut delta = HashMap::new(); + for (key, raw_value) in entries { + let flat_state_value = raw_value.as_ref().map(|value| FlatStateValue::on_disk(value)); + delta.insert(key.to_vec(), flat_state_value); + } + Self(delta) + } + /// Applies delta to the flat state. pub fn apply_to_flat_state(self, store_update: &mut StoreUpdate, shard_uid: ShardUId) { for (key, value) in self.0.into_iter() { diff --git a/core/store/src/flat/manager.rs b/core/store/src/flat/manager.rs index 8058073d5e6..f1f557b1320 100644 --- a/core/store/src/flat/manager.rs +++ b/core/store/src/flat/manager.rs @@ -1,18 +1,21 @@ use crate::flat::{ store_helper, BlockInfo, FlatStorageReadyStatus, FlatStorageStatus, POISONED_LOCK_ERR, }; +use near_primitives::block::Block; use near_primitives::errors::StorageError; use near_primitives::hash::CryptoHash; use near_primitives::shard_layout::ShardUId; -use near_primitives::types::BlockHeight; +use near_primitives::types::{BlockHeight, RawStateChangesWithTrieKey}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; use tracing::debug; -use crate::{Store, StoreUpdate}; +use crate::{get_genesis_hash, Store, StoreUpdate}; use super::chunk_view::FlatStorageChunkView; -use super::FlatStorage; +use super::{ + FlatStateChanges, FlatStateDelta, FlatStateDeltaMetadata, FlatStorage, FlatStorageError, +}; /// `FlatStorageManager` provides a way to construct new flat state to pass to new tries. /// It is owned by NightshadeRuntime, and thus can be owned by multiple threads, so the implementation @@ -93,6 +96,102 @@ impl FlatStorageManager { Ok(()) } + /// Update flat storage for given processed or caught up block, which includes: + /// - merge deltas from current flat storage head to new one; + /// - update flat storage head to the hash of final block visible from given one; + /// - remove info about unreachable blocks from memory. + pub fn update_flat_storage_for_shard( + &self, + shard_uid: ShardUId, + block: &Block, + ) -> Result<(), StorageError> { + if let Some(flat_storage) = self.get_flat_storage_for_shard(shard_uid) { + let mut new_flat_head = *block.header().last_final_block(); + if new_flat_head == CryptoHash::default() { + let genesis_hash = get_genesis_hash(&self.0.store) + .map_err(|e| FlatStorageError::StorageInternalError(e.to_string()))? + .expect("Genesis hash must exist. Consider initialization."); + new_flat_head = genesis_hash; + } + // Try to update flat head. + flat_storage.update_flat_head(&new_flat_head, false).unwrap_or_else(|err| { + match &err { + FlatStorageError::BlockNotSupported(_) => { + // It's possible that new head is not a child of current flat head, e.g. when we have a + // fork: + // + // (flat head) /-------> 6 + // 1 -> 2 -> 3 -> 4 + // \---> 5 + // + // where during postprocessing (5) we call `update_flat_head(3)` and then for (6) we can + // call `update_flat_head(2)` because (2) will be last visible final block from it. + // In such case, just log an error. + debug!( + target: "store", + ?new_flat_head, + ?err, + ?shard_uid, + block_hash = ?block.header().hash(), + "Cannot update flat head"); + } + _ => { + // All other errors are unexpected, so we panic. + panic!("Cannot update flat head of shard {shard_uid:?} to {new_flat_head:?}: {err:?}"); + } + } + }); + } + Ok(()) + } + + pub fn save_flat_state_changes( + &self, + block_hash: CryptoHash, + prev_hash: CryptoHash, + height: BlockHeight, + shard_uid: ShardUId, + state_changes: &[RawStateChangesWithTrieKey], + ) -> Result { + 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))? + } else { + // The current block has flat state changes. + None + }; + + let delta = FlatStateDelta { + changes: FlatStateChanges::from_state_changes(state_changes), + metadata: FlatStateDeltaMetadata { + block: BlockInfo { hash: block_hash, height, prev_hash }, + prev_block_with_changes, + }, + }; + + let store_update = if let Some(flat_storage) = self.get_flat_storage_for_shard(shard_uid) { + // If flat storage exists, we add a block to it. + flat_storage.add_delta(delta).map_err(|e| StorageError::from(e))? + } else { + let shard_id = shard_uid.shard_id(); + // Otherwise, save delta to disk so it will be used for flat storage creation later. + debug!(target: "store", %shard_id, "Add delta for flat storage creation"); + let mut store_update = self.0.store.store_update(); + store_helper::set_delta(&mut store_update, shard_uid, &delta); + store_update + }; + + Ok(store_update) + } + 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") diff --git a/core/store/src/flat/metrics.rs b/core/store/src/flat/metrics.rs index dcbec0c70f8..10605c6f169 100644 --- a/core/store/src/flat/metrics.rs +++ b/core/store/src/flat/metrics.rs @@ -1,12 +1,13 @@ use crate::metrics::flat_state_metrics; use near_o11y::metrics::{IntCounter, IntGauge}; -use near_primitives::types::ShardId; +use near_primitives::types::{BlockHeight, ShardId}; use super::FlatStorageStatus; pub(crate) struct FlatStorageMetrics { flat_head_height: IntGauge, distance_to_head: IntGauge, + hops_to_head: IntGauge, cached_deltas: IntGauge, cached_changes_num_items: IntGauge, cached_changes_size: IntGauge, @@ -20,6 +21,8 @@ impl FlatStorageMetrics { .with_label_values(&[&shard_id_label]), distance_to_head: flat_state_metrics::FLAT_STORAGE_DISTANCE_TO_HEAD .with_label_values(&[&shard_id_label]), + hops_to_head: flat_state_metrics::FLAT_STORAGE_HOPS_TO_HEAD + .with_label_values(&[&shard_id_label]), cached_deltas: flat_state_metrics::FLAT_STORAGE_CACHED_DELTAS .with_label_values(&[&shard_id_label]), cached_changes_num_items: flat_state_metrics::FLAT_STORAGE_CACHED_CHANGES_NUM_ITEMS @@ -29,8 +32,9 @@ impl FlatStorageMetrics { } } - pub(crate) fn set_distance_to_head(&self, distance: usize) { - self.distance_to_head.set(distance as i64); + pub(crate) fn set_distance_to_head(&self, distance: usize, height: Option) { + self.distance_to_head.set(height.unwrap_or(0) as i64); + self.hops_to_head.set(distance as i64); } pub(crate) fn set_flat_head_height(&self, height: u64) { diff --git a/core/store/src/flat/storage.rs b/core/store/src/flat/storage.rs index 23c491dcf2a..68de1d3911b 100644 --- a/core/store/src/flat/storage.rs +++ b/core/store/src/flat/storage.rs @@ -5,16 +5,18 @@ use near_primitives::errors::StorageError; use near_primitives::hash::CryptoHash; use near_primitives::shard_layout::ShardUId; use near_primitives::state::FlatStateValue; +use near_primitives::types::BlockHeight; use tracing::{debug, warn}; -use crate::flat::delta::CachedFlatStateChanges; +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; -use super::{store_helper, BlockInfo}; /// FlatStorage stores information on which blocks flat storage current supports key lookups on. /// Note that this struct is shared by multiple threads, the chain thread, threads that apply chunks, @@ -53,14 +55,17 @@ impl FlatStorageInner { /// Expected limits for in-memory stored changes, under which flat storage must keep working. /// If they are exceeded, warnings are displayed. Flat storage still will work, but its /// performance will slow down, and eventually it can cause OOM error. - /// Limit for number of changes. When over 100, introduces 89 us overhead: + /// Limit for number of blocks needed to read. When over 100, introduces significant overhead. /// https://github.com/near/nearcore/issues/8006#issuecomment-1473621334 - const CACHED_CHANGES_LIMIT: usize = 100; + const HOPS_LIMIT: usize = 100; /// Limit for total size of cached changes. We allocate 600 MiB for cached deltas, which /// means 150 MiB per shards. const CACHED_CHANGES_SIZE_LIMIT: bytesize::ByteSize = bytesize::ByteSize(150 * bytesize::MIB); + const BLOCKS_WITH_CHANGES_FLAT_HEAD_GAP: BlockHeight = 2; + /// Creates `BlockNotSupported` error for the given block. + /// In the context of updating the flat head, the error is handled gracefully. fn create_block_not_supported_error(&self, block_hash: &CryptoHash) -> FlatStorageError { FlatStorageError::BlockNotSupported((self.flat_head.hash, *block_hash)) } @@ -87,17 +92,51 @@ impl FlatStorageInner { let flat_head = &self.flat_head; let mut block_hash = *target_block_hash; let mut blocks = vec![]; + let mut first_height = None; while block_hash != flat_head.hash { - blocks.push(block_hash); - block_hash = self + let metadata = self .deltas .get(&block_hash) .ok_or_else(|| self.create_block_not_supported_error(target_block_hash))? - .metadata - .block - .prev_hash; + .metadata; + if first_height.is_none() { + // Keep track of the height of the initial block. + first_height = Some(metadata.block.height); + } + + // Maybe skip to the previous block with changes. + block_hash = match metadata.prev_block_with_changes { + None => { + blocks.push(block_hash); + metadata.block.prev_hash + } + Some(prev_block_with_changes) => { + // Don't include blocks with no changes in the result, + // unless it's the target block. + if &block_hash == target_block_hash { + blocks.push(block_hash); + } + if prev_block_with_changes.height > flat_head.height { + prev_block_with_changes.hash + } else { + flat_head.hash + } + } + }; + } + self.metrics.set_distance_to_head( + blocks.len(), + first_height.map(|height| height - flat_head.height), + ); + if blocks.len() >= Self::HOPS_LIMIT { + warn!( + target: "chain", + shard_id = self.shard_uid.shard_id(), + flat_head_height = flat_head.height, + cached_deltas = self.deltas.len(), + num_hops = blocks.len(), + "Flat storage needs too many hops to access a block"); } - self.metrics.set_distance_to_head(blocks.len()); Ok(blocks) } @@ -119,13 +158,76 @@ impl FlatStorageInner { ); let cached_changes_size_bytes = bytesize::ByteSize(cached_changes_size); - if cached_deltas >= Self::CACHED_CHANGES_LIMIT - || cached_changes_size_bytes >= Self::CACHED_CHANGES_SIZE_LIMIT - { - let shard_id = self.shard_uid.shard_id(); - let flat_head_height = self.flat_head.height; - warn!(target: "chain", %shard_id, %flat_head_height, %cached_deltas, %cached_changes_size_bytes, "Flat storage cached deltas exceeded expected limits"); + if cached_changes_size_bytes >= Self::CACHED_CHANGES_SIZE_LIMIT { + warn!( + target: "chain", + shard_id = self.shard_uid.shard_id(), + flat_head_height = self.flat_head.height, + cached_deltas, + %cached_changes_size_bytes, + "Flat storage total size of cached deltas exceeded expected limits"); + } + } + + // Determine a block to make the new flat head. + // If `strict`, uses `block_hash` as the new flat head. + // If not `strict`, uses the second most recent block with flat state + // changes from `block_hash`, if it exists. + fn get_new_flat_head( + &self, + block_hash: CryptoHash, + strict: bool, + ) -> Result { + if strict { + return Ok(block_hash); + } + + let current_flat_head_hash = self.flat_head.hash; + let current_flat_head_height = self.flat_head.height; + + let mut new_head = block_hash; + let mut blocks_with_changes = 0; + // Delays updating flat head, keeps this many blocks with non-empty flat + // state changes between the requested flat head and the chosen head to + // make flat state snapshots function properly. + while blocks_with_changes < Self::BLOCKS_WITH_CHANGES_FLAT_HEAD_GAP { + if new_head == current_flat_head_hash { + return Ok(current_flat_head_hash); + } + let metadata = self + .deltas + .get(&new_head) + // BlockNotSupported error kind will be handled gracefully. + .ok_or(self.create_block_not_supported_error(&new_head))? + .metadata; + new_head = match metadata.prev_block_with_changes { + None => { + // The block has flat state changes. + blocks_with_changes += 1; + if blocks_with_changes == Self::BLOCKS_WITH_CHANGES_FLAT_HEAD_GAP { + break; + } + metadata.block.prev_hash + } + Some(BlockWithChangesInfo { hash, height, .. }) => { + // The block has no flat state changes. + if height <= current_flat_head_height { + return Ok(current_flat_head_hash); + } + hash + } + }; } + Ok(new_head) + } + + #[cfg(test)] + pub fn test_get_new_flat_head( + &self, + block_hash: CryptoHash, + strict: bool, + ) -> Result { + self.get_new_flat_head(block_hash, strict) } } @@ -181,7 +283,6 @@ impl FlatStorage { /// Get sequence of blocks `target_block_hash` (inclusive) to flat head (exclusive) /// in backwards chain order. Returns an error if there is no path between them. #[cfg(test)] - #[allow(unused)] pub(crate) fn get_blocks_to_head( &self, target_block_hash: &CryptoHash, @@ -213,17 +314,50 @@ impl FlatStorage { Ok(value) } - /// Update the head of the flat storage, including updating the flat state in memory and on disk - /// and updating the flat state to reflect the state at the new head. If updating to given head is not possible, - /// returns an error. - pub fn update_flat_head(&self, new_head: &CryptoHash) -> Result<(), FlatStorageError> { + /// Update the head of the flat storage, including updating the flat state + /// in memory and on disk and updating the flat state to reflect the state + /// at the new head. If updating to given head is not possible, returns an + /// error. + /// If `strict`, then unconditionally sets flat head to the given block. + /// If not `strict`, then it updates the flat head to the latest block X, + /// such that [X, block_hash] contains 2 blocks with flat state changes. If possible. + /// + /// The function respects the current flat head and will never try to + /// set flat head to a block older than the current flat head. + // + // Let's denote blocks with flat state changes as X, and blocks without + // flat state changes as O. + // + // block_hash + // | + // v + // ...-O-O-X-O-O-O-X-O-O-O-X-O-O-O-X-....->future + // ^ + // | + // new_head + // + // The segment [new_head, block_hash] contains two blocks with flat state changes. + pub fn update_flat_head( + &self, + block_hash: &CryptoHash, + strict: bool, + ) -> Result<(), FlatStorageError> { let mut guard = self.0.write().expect(crate::flat::POISONED_LOCK_ERR); if !guard.move_head_enabled { return Ok(()); } + + let new_head = guard.get_new_flat_head(*block_hash, strict)?; + if new_head == guard.flat_head.hash { + return Ok(()); + } + let shard_uid = guard.shard_uid; let shard_id = shard_uid.shard_id(); - let blocks = guard.get_blocks_to_head(new_head)?; + + tracing::debug!(target: "store", flat_head = ?guard.flat_head.hash, ?new_head, shard_id, "Moving flat head"); + 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()); // Delta must exist because flat storage is locked and we could retrieve @@ -231,29 +365,28 @@ impl FlatStorage { let changes = store_helper::get_delta_changes(&guard.store, shard_uid, block_hash)? .ok_or_else(|| missing_delta_error(&block_hash))?; changes.apply_to_flat_state(&mut store_update, guard.shard_uid); - let block = &guard.deltas[&block_hash].metadata.block; + let metadata = guard + .deltas + .get(&block_hash) + .ok_or_else(|| missing_delta_error(&block_hash))? + .metadata; + let block = metadata.block; let block_height = block.height; store_helper::set_flat_storage_status( &mut store_update, shard_uid, - FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: *block }), + FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: block }), ); guard.metrics.set_flat_head_height(block.height); - guard.flat_head = *block; + guard.flat_head = block; // Remove old deltas from disk and memory. // Do it for each head update separately to ensure that old data is removed properly if node was // interrupted in the middle. // TODO (#7327): in case of long forks it can take a while and delay processing of some chunk. // Consider avoid iterating over all blocks and make removals lazy. - let gc_height = guard - .deltas - .get(&block_hash) - .ok_or_else(|| missing_delta_error(&block_hash))? - .metadata - .block - .height; + let gc_height = metadata.block.height; let hashes_to_remove: Vec<_> = guard .deltas .iter() @@ -349,19 +482,23 @@ fn missing_delta_error(block_hash: &CryptoHash) -> FlatStorageError { #[cfg(test)] mod tests { - use crate::flat::delta::{FlatStateChanges, FlatStateDelta, FlatStateDeltaMetadata}; + use crate::flat::delta::{ + BlockWithChangesInfo, FlatStateChanges, FlatStateDelta, FlatStateDeltaMetadata, + }; use crate::flat::manager::FlatStorageManager; + use crate::flat::storage::FlatStorageInner; use crate::flat::types::{BlockInfo, FlatStorageError}; use crate::flat::{store_helper, FlatStorageReadyStatus, FlatStorageStatus}; use crate::test_utils::create_test_store; use crate::StorageError; + use assert_matches::assert_matches; use borsh::BorshSerialize; + use near_o11y::testonly::init_test_logger; use near_primitives::hash::{hash, CryptoHash}; - use near_primitives::types::BlockHeight; - - use assert_matches::assert_matches; use near_primitives::shard_layout::ShardUId; use near_primitives::state::FlatStateValue; + use near_primitives::types::BlockHeight; + use rand::{thread_rng, Rng}; use std::collections::HashMap; struct MockChain { @@ -478,7 +615,10 @@ mod tests { for i in 1..5 { let delta = FlatStateDelta { changes: FlatStateChanges::default(), - metadata: FlatStateDeltaMetadata { block: chain.get_block(i) }, + metadata: FlatStateDeltaMetadata { + block: chain.get_block(i), + prev_block_with_changes: None, + }, }; store_helper::set_delta(&mut store_update, shard_uid, &delta); } @@ -491,23 +631,23 @@ mod tests { // Check `BlockNotSupported` errors which are fine to occur during regular block processing. // First, check that flat head can be moved to block 1. let flat_head_hash = chain.get_block_hash(1); - assert_eq!(flat_storage.update_flat_head(&flat_head_hash), Ok(())); + assert_eq!(flat_storage.update_flat_head(&flat_head_hash, true), Ok(())); // Check that attempt to move flat head to block 2 results in error because it lays in unreachable fork. let fork_block_hash = chain.get_block_hash(2); assert_eq!( - flat_storage.update_flat_head(&fork_block_hash), + flat_storage.update_flat_head(&fork_block_hash, true), Err(FlatStorageError::BlockNotSupported((flat_head_hash, fork_block_hash))) ); // Check that attempt to move flat head to block 0 results in error because it is an unreachable parent. let parent_block_hash = chain.get_block_hash(0); assert_eq!( - flat_storage.update_flat_head(&parent_block_hash), + flat_storage.update_flat_head(&parent_block_hash, true), Err(FlatStorageError::BlockNotSupported((flat_head_hash, parent_block_hash))) ); // Check that attempt to move flat head to non-existent block results in the same error. let not_existing_hash = hash(&[1, 2, 3]); assert_eq!( - flat_storage.update_flat_head(¬_existing_hash), + flat_storage.update_flat_head(¬_existing_hash, true), Err(FlatStorageError::BlockNotSupported((flat_head_hash, not_existing_hash))) ); // Corrupt DB state for block 3 and try moving flat head to it. @@ -516,11 +656,72 @@ mod tests { store_helper::remove_delta(&mut store_update, shard_uid, chain.get_block_hash(3)); store_update.commit().unwrap(); assert_matches!( - flat_storage.update_flat_head(&chain.get_block_hash(3)), + flat_storage.update_flat_head(&chain.get_block_hash(3), true), Err(FlatStorageError::StorageInternalError(_)) ); } + #[test] + /// Builds a chain with occasional forks. + /// Checks that request to update flat head fail and are handled gracefully. + fn flat_storage_fork() { + init_test_logger(); + // Create a chain with two forks. Set flat head to be at block 0. + let num_blocks = 10 as BlockHeight; + + // Build a chain that looks lke this: + // 2 5 8 + // / / / + // 0-1---3-4---6-7---9 + // Note that forks [0,1,2], [3,4,5] and [6,7,8] form triples of consecutive blocks. + let chain = MockChain::build((0..num_blocks).collect(), |i| { + if i == 0 { + None + } else if i % 3 == 0 { + Some(i - 2) + } else { + Some(i - 1) + } + }); + let shard_uid = ShardUId::single_shard(); + let store = create_test_store(); + let mut store_update = store.store_update(); + store_helper::set_flat_storage_status( + &mut store_update, + shard_uid, + FlatStorageStatus::Ready(FlatStorageReadyStatus { flat_head: chain.get_block(0) }), + ); + for i in 1..num_blocks { + let delta = FlatStateDelta { + changes: FlatStateChanges::default(), + metadata: FlatStateDeltaMetadata { + block: chain.get_block(i), + prev_block_with_changes: None, + }, + }; + store_helper::set_delta(&mut store_update, shard_uid, &delta); + } + store_update.commit().unwrap(); + + let flat_storage_manager = FlatStorageManager::new(store); + flat_storage_manager.create_flat_storage_for_shard(shard_uid).unwrap(); + let flat_storage = flat_storage_manager.get_flat_storage_for_shard(shard_uid).unwrap(); + + // Simulates an actual sequence of calls to `update_flat_head()` in the + // presence of forks. + assert_eq!(flat_storage.update_flat_head(&chain.get_block_hash(0), false), Ok(())); + assert_eq!(flat_storage.update_flat_head(&chain.get_block_hash(3), false), Ok(())); + assert_matches!( + flat_storage.update_flat_head(&chain.get_block_hash(0), false), + Err(FlatStorageError::BlockNotSupported(_)) + ); + assert_eq!(flat_storage.update_flat_head(&chain.get_block_hash(6), false), Ok(())); + assert_matches!( + flat_storage.update_flat_head(&chain.get_block_hash(0), false), + Err(FlatStorageError::BlockNotSupported(_)) + ); + } + #[test] fn skipped_heights() { // Create a linear chain where some heights are skipped. @@ -536,7 +737,10 @@ mod tests { for i in 1..5 { let delta = FlatStateDelta { changes: FlatStateChanges::default(), - metadata: FlatStateDeltaMetadata { block: chain.get_block(i * 2) }, + metadata: FlatStateDeltaMetadata { + block: chain.get_block(i * 2), + prev_block_with_changes: None, + }, }; store_helper::set_delta(&mut store_update, shard_uid, &delta); } @@ -549,7 +753,7 @@ mod tests { // Check that flat head can be moved to block 8. let flat_head_hash = chain.get_block_hash(8); - assert_eq!(flat_storage.update_flat_head(&flat_head_hash), Ok(())); + assert_eq!(flat_storage.update_flat_head(&flat_head_hash, false), Ok(())); } // This tests basic use cases for FlatStorageChunkView and FlatStorage. @@ -581,7 +785,10 @@ mod tests { vec![1], Some(FlatStateValue::value_ref(&[i as u8])), )]), - metadata: FlatStateDeltaMetadata { block: chain.get_block(i) }, + metadata: FlatStateDeltaMetadata { + block: chain.get_block(i), + prev_block_with_changes: None, + }, }; store_helper::set_delta(&mut store_update, shard_uid, &delta); } @@ -612,7 +819,10 @@ mod tests { (vec![1], None), (vec![2], Some(FlatStateValue::value_ref(&[1]))), ]), - metadata: FlatStateDeltaMetadata { block: chain.get_block_info(&hash) }, + metadata: FlatStateDeltaMetadata { + block: chain.get_block_info(&hash), + prev_block_with_changes: None, + }, }) .unwrap(); store_update.commit().unwrap(); @@ -640,7 +850,7 @@ mod tests { // 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(&chain.get_block_hash(5)).unwrap(); + flat_storage.update_flat_head(&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])) @@ -664,7 +874,7 @@ mod tests { // 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(&chain.get_block_hash(10)).unwrap(); + flat_storage.update_flat_head(&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); @@ -679,4 +889,518 @@ mod tests { None ); } + + #[test] + fn flat_storage_with_hops() { + init_test_logger(); + // 1. Create a chain with no forks. Set flat head to be at block 0. + let num_blocks = 15; + let chain = MockChain::linear_chain(num_blocks); + let shard_uid = ShardUId::single_shard(); + let store = create_test_store(); + let mut store_update = store.store_update(); + store_helper::set_flat_storage_status( + &mut store_update, + 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.commit().unwrap(); + + for i in 1..num_blocks as BlockHeight { + let mut store_update = store.store_update(); + let changes = if i % 3 == 0 { + // Add a change. + FlatStateChanges::from([(vec![1], Some(FlatStateValue::value_ref(&[i as u8])))]) + } else { + // No changes. + FlatStateChanges::from([]) + }; + + // 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() + } else { + None + }; + let delta = FlatStateDelta { + changes, + metadata: FlatStateDeltaMetadata { + block: chain.get_block(i), + prev_block_with_changes, + }, + }; + tracing::info!(?i, ?delta); + store_helper::set_delta(&mut store_update, shard_uid, &delta); + store_update.commit().unwrap(); + } + + let flat_storage_manager = FlatStorageManager::new(store.clone()); + flat_storage_manager.create_flat_storage_for_shard(shard_uid).unwrap(); + let flat_storage = flat_storage_manager.get_flat_storage_for_shard(shard_uid).unwrap(); + + // 2. Check that the chunk_view at block i reads the value of key &[1] as &[round_down_to_a_multiple_of_3(i)] + for i in 0..num_blocks as BlockHeight { + let block_hash = chain.get_block_hash(i); + let blocks = flat_storage.get_blocks_to_head(&block_hash).unwrap(); + let chunk_view = flat_storage_manager.chunk_view(shard_uid, block_hash).unwrap(); + let value = chunk_view.get_value(&[1]).unwrap(); + tracing::info!(?i, ?block_hash, ?value, blocks_to_head = ?blocks); + assert_eq!(value, Some(FlatStateValue::value_ref(&[((i / 3) * 3) as u8]))); + + // 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(); + assert!( + !delta.0.is_empty(), + "i: {i}, block_hash: {block_hash:?}, delta: {delta:?}" + ); + } + } + + // 3. Simulate moving head forward with a delay of two from the tip. + // flat head is chosen to keep 2 blocks between the suggested head and the chosen head, + // resulting in <=4 blocks on the way from the tip to the chosen head. + for i in 2..num_blocks as BlockHeight { + let final_block_hash = chain.get_block_hash(i - 2); + flat_storage.update_flat_head(&final_block_hash, false).unwrap(); + + let block_hash = chain.get_block_hash(i); + let blocks = flat_storage.get_blocks_to_head(&block_hash).unwrap(); + + assert!( + blocks.len() <= 2 + FlatStorageInner::BLOCKS_WITH_CHANGES_FLAT_HEAD_GAP as usize + ); + } + } + + #[test] + /// Move flat storage to an exact height when flat storage has no changes. + fn flat_storage_with_no_changes() { + init_test_logger(); + let num_blocks = 10; + let chain = MockChain::linear_chain(num_blocks); + let shard_uid = ShardUId::single_shard(); + let store = create_test_store(); + let mut store_update = store.store_update(); + store_helper::set_flat_storage_status( + &mut store_update, + 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.commit().unwrap(); + + for i in 1..num_blocks as BlockHeight { + let mut store_update = store.store_update(); + // 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 delta = FlatStateDelta { + changes, + metadata: FlatStateDeltaMetadata { + block: chain.get_block(i), + prev_block_with_changes, + }, + }; + tracing::info!(?i, ?delta); + store_helper::set_delta(&mut store_update, shard_uid, &delta); + store_update.commit().unwrap(); + } + + let flat_storage_manager = FlatStorageManager::new(store); + flat_storage_manager.create_flat_storage_for_shard(shard_uid).unwrap(); + let flat_storage = flat_storage_manager.get_flat_storage_for_shard(shard_uid).unwrap(); + + let hashes = (0..num_blocks as BlockHeight) + .map(|height| (chain.get_block_hash(height), height)) + .collect::>(); + + let block_hash = chain.get_block_hash((num_blocks - 1) as BlockHeight); + flat_storage.update_flat_head(&block_hash, true).unwrap(); + + let flat_head_hash = flat_storage.get_head_hash(); + let flat_head_height = hashes.get(&flat_head_hash).unwrap(); + + assert_eq!(*flat_head_height, (num_blocks - 1) as BlockHeight); + } + + #[test] + fn flat_storage_with_hops_random() { + init_test_logger(); + // 1. Create a long chain with no forks. Set flat head to be at block 0. + let num_blocks = 1000; + let mut rng = thread_rng(); + let chain = MockChain::linear_chain(num_blocks); + let shard_uid = ShardUId::single_shard(); + let store = create_test_store(); + let mut store_update = store.store_update(); + store_helper::set_flat_storage_status( + &mut store_update, + 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.commit().unwrap(); + + for i in 1..num_blocks as BlockHeight { + let mut store_update = store.store_update(); + let changes = if rng.gen_bool(0.3) { + // Add a change. + FlatStateChanges::from([(vec![1], Some(FlatStateValue::value_ref(&[i as u8])))]) + } else { + // No changes. + FlatStateChanges::default() + }; + + // 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() + } else { + None + }; + let delta = FlatStateDelta { + changes, + metadata: FlatStateDeltaMetadata { + block: chain.get_block(i), + prev_block_with_changes, + }, + }; + tracing::info!(?i, ?delta); + store_helper::set_delta(&mut store_update, shard_uid, &delta); + store_update.commit().unwrap(); + } + + let flat_storage_manager = FlatStorageManager::new(store.clone()); + flat_storage_manager.create_flat_storage_for_shard(shard_uid).unwrap(); + let flat_storage = flat_storage_manager.get_flat_storage_for_shard(shard_uid).unwrap(); + + let hashes = (0..num_blocks as BlockHeight) + .map(|height| (chain.get_block_hash(height), height)) + .collect::>(); + + // 2. Simulate moving head, with the suggested head lagging by 2 blocks behind the tip. + let mut max_lag = None; + for i in 2..num_blocks as BlockHeight { + let final_block_hash = chain.get_block_hash(i - 2); + flat_storage.update_flat_head(&final_block_hash, false).unwrap(); + + let block_hash = chain.get_block_hash(i); + let blocks = flat_storage.get_blocks_to_head(&block_hash).unwrap(); + assert!( + blocks.len() <= 2 + FlatStorageInner::BLOCKS_WITH_CHANGES_FLAT_HEAD_GAP as usize + ); + // 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(); + assert!( + !delta.0.is_empty(), + "i: {i}, block_hash: {block_hash:?}, delta: {delta:?}" + ); + } + + let flat_head_hash = flat_storage.get_head_hash(); + 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 has_changes = !delta.0.is_empty(); + tracing::info!(?i, has_changes, ?flat_head_lag); + max_lag = max_lag.max(Some(flat_head_lag)); + } + tracing::info!(?max_lag); + } + + #[test] + fn test_new_flat_head() { + init_test_logger(); + + let shard_uid = ShardUId::single_shard(); + + // Case 1. Each block has flat state changes. + { + tracing::info!("Case 1"); + let num_blocks = 10; + let chain = MockChain::linear_chain(num_blocks); + let store = create_test_store(); + let mut store_update = store.store_update(); + store_helper::set_flat_storage_status( + &mut store_update, + 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.commit().unwrap(); + + for i in 1..num_blocks as BlockHeight { + let mut store_update = store.store_update(); + // Add a change. + let changes = FlatStateChanges::from([( + vec![1], + Some(FlatStateValue::value_ref(&[i as u8])), + )]); + let delta = FlatStateDelta { + changes, + metadata: FlatStateDeltaMetadata { + block: chain.get_block(i), + prev_block_with_changes: None, + }, + }; + tracing::info!(?i, ?delta); + store_helper::set_delta(&mut store_update, shard_uid, &delta); + store_update.commit().unwrap(); + } + + let flat_storage_manager = FlatStorageManager::new(store); + flat_storage_manager.create_flat_storage_for_shard(shard_uid).unwrap(); + let flat_storage = flat_storage_manager.get_flat_storage_for_shard(shard_uid).unwrap(); + let guard = flat_storage.0.write().expect(crate::flat::POISONED_LOCK_ERR); + + for i in 0..num_blocks as BlockHeight { + let block_hash = chain.get_block_hash(i); + let new_head = guard.test_get_new_flat_head(block_hash, false).unwrap(); + tracing::info!(i, ?block_hash, ?new_head); + if i == 0 { + assert_eq!(new_head, chain.get_block_hash(0)); + } else { + assert_eq!(new_head, chain.get_block_hash(i - 1)); + } + } + } + + // Case 2. Even-numbered blocks have flat changes + { + tracing::info!("Case 2"); + let num_blocks = 20; + let chain = MockChain::linear_chain(num_blocks); + let store = create_test_store(); + let mut store_update = store.store_update(); + store_helper::set_flat_storage_status( + &mut store_update, + 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.commit().unwrap(); + + for i in 1..num_blocks as BlockHeight { + let mut store_update = store.store_update(); + let (changes, prev_block_with_changes) = if i % 2 == 0 { + // Add a change. + ( + FlatStateChanges::from([( + vec![1], + Some(FlatStateValue::value_ref(&[i as u8])), + )]), + None, + ) + } else { + // No changes. + ( + FlatStateChanges::default(), + Some(BlockWithChangesInfo { + hash: chain.get_block_hash(i - 1), + height: i - 1, + }), + ) + }; + + let delta = FlatStateDelta { + changes, + metadata: FlatStateDeltaMetadata { + block: chain.get_block(i), + prev_block_with_changes, + }, + }; + tracing::info!(?i, ?delta); + store_helper::set_delta(&mut store_update, shard_uid, &delta); + store_update.commit().unwrap(); + } + + let flat_storage_manager = FlatStorageManager::new(store); + flat_storage_manager.create_flat_storage_for_shard(shard_uid).unwrap(); + let flat_storage = flat_storage_manager.get_flat_storage_for_shard(shard_uid).unwrap(); + let guard = flat_storage.0.write().expect(crate::flat::POISONED_LOCK_ERR); + + // A chain looks like this: + // X-O-X-O-X-O-... + // where X is a block with flat state changes and O is a block without flat state changes. + for i in 0..num_blocks as BlockHeight { + let new_head = guard.get_new_flat_head(chain.get_block_hash(i), false).unwrap(); + if i <= 3 { + assert_eq!(new_head, chain.get_block_hash(0)); + } else { + // if i is odd, then it's pointing at an O, and the head is expected to be 3 blocks ago. + // i + // | + // v + // X-O-X-O-X-O-X-O + // ^ + // | + // new_head + // + // if i is even, then it's pointing at an X, and the head is expected to be the previous X: + // i + // | + // v + // X-O-X-O-X-O-X-O + // ^ + // | + // new_head + + // Both of these cases can be computed as rounding (i-2) down to a multiple of 2. + assert_eq!(new_head, chain.get_block_hash(((i - 2) / 2) * 2)); + } + } + } + + // Case 3. Triplets of blocks: HasChanges, NoChanges, HasChanges. + { + tracing::info!("Case 3"); + let num_blocks = 20; + let chain = MockChain::linear_chain(num_blocks); + let store = create_test_store(); + let mut store_update = store.store_update(); + store_helper::set_flat_storage_status( + &mut store_update, + 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.commit().unwrap(); + + for i in 1..num_blocks as BlockHeight { + let mut store_update = store.store_update(); + let (changes, prev_block_with_changes) = if i % 3 == 1 { + // No changes. + ( + FlatStateChanges::default(), + Some(BlockWithChangesInfo { + hash: chain.get_block_hash(i - 1), + height: i - 1, + }), + ) + } else { + // Add a change. + ( + FlatStateChanges::from([( + vec![1], + Some(FlatStateValue::value_ref(&[i as u8])), + )]), + None, + ) + }; + + let delta = FlatStateDelta { + changes, + metadata: FlatStateDeltaMetadata { + block: chain.get_block(i), + prev_block_with_changes, + }, + }; + tracing::info!(?i, ?delta); + store_helper::set_delta(&mut store_update, shard_uid, &delta); + store_update.commit().unwrap(); + } + + let flat_storage_manager = FlatStorageManager::new(store); + flat_storage_manager.create_flat_storage_for_shard(shard_uid).unwrap(); + let flat_storage = flat_storage_manager.get_flat_storage_for_shard(shard_uid).unwrap(); + let guard = flat_storage.0.write().expect(crate::flat::POISONED_LOCK_ERR); + + for i in 0..num_blocks as BlockHeight { + let new_head = guard.get_new_flat_head(chain.get_block_hash(i), false).unwrap(); + // if i%3 == 0 + // i + // | + // v + // X-O-X-X-O-X-X-O-X + // ^ + // | + // new_head + // + // if i%3 == 1 + // i + // | + // v + // X-O-X-X-O-X-X-O-X + // ^ + // | + // new_head + // + // if i%3 == 2 + // i + // | + // v + // X-O-X-X-O-X-X-O-X + // ^ + // | + // new_head + if i <= 2 { + assert_eq!(new_head, chain.get_block_hash(0)); + } else if i % 3 == 0 { + assert_eq!(new_head, chain.get_block_hash(i - 1)); + } else { + assert_eq!(new_head, chain.get_block_hash(i - 2)); + } + } + } + } } diff --git a/core/store/src/flat/store_helper.rs b/core/store/src/flat/store_helper.rs index f0405a6be09..d0b4a015d38 100644 --- a/core/store/src/flat/store_helper.rs +++ b/core/store/src/flat/store_helper.rs @@ -1,21 +1,20 @@ //! 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 std::io; - +use super::delta::{FlatStateDelta, FlatStateDeltaMetadata}; +use super::types::{ + FlatStateIterator, FlatStateValuesInliningMigrationStatus, FlatStorageResult, FlatStorageStatus, +}; use crate::db::FLAT_STATE_VALUES_INLINING_MIGRATION_STATUS_KEY; -use crate::flat::delta::{FlatStateChanges, KeyForFlatStateDelta}; +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 super::delta::{FlatStateDelta, FlatStateDeltaMetadata}; -use super::types::{ - FlatStateIterator, FlatStateValuesInliningMigrationStatus, FlatStorageResult, FlatStorageStatus, -}; +use std::io; pub fn get_delta_changes( store: &Store, @@ -46,6 +45,56 @@ pub fn get_all_deltas_metadata( .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 diff --git a/core/store/src/flat/types.rs b/core/store/src/flat/types.rs index e7148702a60..0cce82be461 100644 --- a/core/store/src/flat/types.rs +++ b/core/store/src/flat/types.rs @@ -12,8 +12,8 @@ pub struct BlockInfo { } impl BlockInfo { - pub fn genesis(hash: CryptoHash, height: BlockHeight) -> BlockInfo { - BlockInfo { hash, height, prev_hash: CryptoHash::default() } + pub fn genesis(hash: CryptoHash, height: BlockHeight) -> Self { + Self { hash, height, prev_hash: CryptoHash::default() } } } diff --git a/core/store/src/genesis/state_applier.rs b/core/store/src/genesis/state_applier.rs index 3a00a1d09fb..1f821d96b8e 100644 --- a/core/store/src/genesis/state_applier.rs +++ b/core/store/src/genesis/state_applier.rs @@ -7,13 +7,13 @@ use borsh::BorshSerialize; use near_chain_configs::Genesis; use near_crypto::PublicKey; use near_primitives::account::{AccessKey, Account}; -use near_primitives::contract::ContractCode; use near_primitives::receipt::{DelayedReceiptIndices, Receipt, ReceiptEnum, ReceivedData}; use near_primitives::runtime::fees::StorageUsageConfig; use near_primitives::shard_layout::ShardUId; use near_primitives::state_record::{state_record_to_account_id, StateRecord}; use near_primitives::trie_key::TrieKey; use near_primitives::types::{AccountId, Balance, ShardId, StateChangeCause, StateRoot}; +use near_vm_runner::ContractCode; use std::collections::{HashMap, HashSet}; use std::sync::atomic; diff --git a/core/store/src/lib.rs b/core/store/src/lib.rs index 357db5a1c4d..d81bb8e328a 100644 --- a/core/store/src/lib.rs +++ b/core/store/src/lib.rs @@ -15,12 +15,11 @@ pub use columns::DBCol; pub use db::{ 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_SYNC_DUMP_KEY, TAIL_KEY, + LATEST_KNOWN_KEY, STATE_SNAPSHOT_KEY, STATE_SYNC_DUMP_KEY, TAIL_KEY, }; use near_crypto::PublicKey; use near_fmt::{AbbrBytes, StorageKey}; use near_primitives::account::{AccessKey, Account}; -use near_primitives::contract::ContractCode; pub use near_primitives::errors::StorageError; use near_primitives::hash::CryptoHash; use near_primitives::receipt::{DelayedReceiptIndices, Receipt, ReceivedData}; @@ -28,6 +27,7 @@ pub use near_primitives::shard_layout::ShardUId; use near_primitives::trie_key::{trie_key_parsers, TrieKey}; use near_primitives::types::{AccountId, StateRoot}; use near_vm_runner::logic::{CompiledContract, CompiledContractCache}; +use near_vm_runner::ContractCode; use crate::db::{refcount, DBIterator, DBOp, DBSlice, DBTransaction, Database, StoreStatistics}; pub use crate::trie::iterator::{TrieIterator, TrieTraversalItem}; @@ -856,6 +856,17 @@ pub fn set_genesis_state_roots(store_update: &mut StoreUpdate, genesis_roots: &[ .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), + } +} + pub struct StoreCompiledContractCache { db: Arc, } diff --git a/core/store/src/metadata.rs b/core/store/src/metadata.rs index b516296a5c7..eab2c05041c 100644 --- a/core/store/src/metadata.rs +++ b/core/store/src/metadata.rs @@ -2,7 +2,7 @@ pub type DbVersion = u32; /// Current version of the database. -pub const DB_VERSION: DbVersion = 37; +pub const DB_VERSION: DbVersion = 38; /// Database version at which point DbKind was introduced. const DB_VERSION_WITH_KIND: DbVersion = 34; diff --git a/core/store/src/metrics.rs b/core/store/src/metrics.rs index ea8ba309608..0820027f9c2 100644 --- a/core/store/src/metrics.rs +++ b/core/store/src/metrics.rs @@ -18,6 +18,7 @@ pub(crate) static DATABASE_OP_LATENCY_HIST: Lazy = Lazy::new(|| { .unwrap() }); +// TODO(#9054): Rename the metric to be consistent with "accounting cache". pub static CHUNK_CACHE_HITS: Lazy = Lazy::new(|| { try_create_int_counter_vec( "near_chunk_cache_hits", @@ -27,6 +28,7 @@ pub static CHUNK_CACHE_HITS: Lazy = Lazy::new(|| { .unwrap() }); +// TODO(#9054): Rename the metric to be consistent with "accounting cache". pub static CHUNK_CACHE_MISSES: Lazy = Lazy::new(|| { try_create_int_counter_vec( "near_chunk_cache_misses", @@ -68,6 +70,7 @@ pub static SHARD_CACHE_SIZE: Lazy = Lazy::new(|| { .unwrap() }); +// TODO(#9054): Rename the metric to be consistent with "accounting cache". pub static CHUNK_CACHE_SIZE: Lazy = Lazy::new(|| { try_create_int_gauge_vec("near_chunk_cache_size", "Chunk cache size", &["shard_id", "is_view"]) .unwrap() @@ -461,7 +464,15 @@ pub mod flat_state_metrics { pub static FLAT_STORAGE_DISTANCE_TO_HEAD: Lazy = Lazy::new(|| { try_create_int_gauge_vec( "flat_storage_distance_to_head", - "Distance between processed block and flat storage head", + "Height distance between processed block and flat storage head", + &["shard_id"], + ) + .unwrap() + }); + pub static FLAT_STORAGE_HOPS_TO_HEAD: Lazy = Lazy::new(|| { + try_create_int_gauge_vec( + "flat_storage_hops_to_head", + "Number of blocks visited to flat storage head", &["shard_id"], ) .unwrap() diff --git a/core/store/src/migrations.rs b/core/store/src/migrations.rs index 8e137c60441..a08475dc4e0 100644 --- a/core/store/src/migrations.rs +++ b/core/store/src/migrations.rs @@ -206,3 +206,27 @@ pub fn migrate_36_to_37(store: &Store) -> anyhow::Result<()> { update.commit()?; Ok(()) } + +/// Migrates the database from version 37 to 38. +/// +/// Rewrites FlatStateDeltaMetadata to add a bit to Metadata, `prev_block_with_changes`. +/// That bit is initialized with a `None` regardless of the corresponding flat state changes. +pub fn migrate_37_to_38(store: &Store) -> anyhow::Result<()> { + #[derive(borsh::BorshDeserialize)] + struct LegacyFlatStateDeltaMetadata { + block: crate::flat::BlockInfo, + } + + let mut update = store.store_update(); + update.delete_all(DBCol::FlatStateDeltaMetadata); + for result in store.iter(DBCol::FlatStateDeltaMetadata) { + let (key, old_value) = result?; + let LegacyFlatStateDeltaMetadata { block } = + LegacyFlatStateDeltaMetadata::try_from_slice(&old_value)?; + let new_value = + crate::flat::FlatStateDeltaMetadata { block, prev_block_with_changes: None }; + update.set(DBCol::FlatStateDeltaMetadata, &key, &new_value.try_to_vec()?); + } + update.commit()?; + Ok(()) +} diff --git a/core/store/src/test_utils.rs b/core/store/src/test_utils.rs index 5630821853c..66e63089f7f 100644 --- a/core/store/src/test_utils.rs +++ b/core/store/src/test_utils.rs @@ -1,15 +1,17 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; +use near_primitives::state::{FlatStateValue, ValueRef}; use near_primitives::trie_key::TrieKey; use rand::seq::SliceRandom; use rand::Rng; use crate::db::TestDB; +use crate::flat::{store_helper, BlockInfo, FlatStorageReadyStatus}; use crate::metadata::{DbKind, DbVersion, DB_VERSION}; use crate::{get, get_delayed_receipt_indices, DBCol, NodeStorage, ShardTries, Store}; use near_primitives::account::id::AccountId; -use near_primitives::hash::CryptoHash; +use near_primitives::hash::{hash, CryptoHash}; use near_primitives::receipt::{DataReceipt, Receipt, ReceiptEnum}; use near_primitives::shard_layout::{ShardUId, ShardVersion}; use near_primitives::types::{NumShards, StateRoot}; @@ -87,6 +89,34 @@ pub fn test_populate_trie( root } +pub fn test_populate_flat_storage( + tries: &ShardTries, + shard_uid: ShardUId, + block_hash: &CryptoHash, + prev_block_hash: &CryptoHash, + changes: &Vec<(Vec, Option>)>, +) { + let mut store_update = tries.store_update(); + store_helper::set_flat_storage_status( + &mut store_update, + 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, + shard_uid, + key.clone(), + value.as_ref().map(|value| { + FlatStateValue::Ref(ValueRef { hash: hash(value), length: value.len() as u32 }) + }), + ); + } + store_update.commit().unwrap(); +} + /// Insert values to non-reference-counted columns in the store. pub fn test_populate_store(store: &Store, data: &[(DBCol, Vec, Vec)]) { let mut update = store.store_update(); diff --git a/core/store/src/trie/accounting_cache.rs b/core/store/src/trie/accounting_cache.rs new file mode 100644 index 00000000000..125479dfad2 --- /dev/null +++ b/core/store/src/trie/accounting_cache.rs @@ -0,0 +1,122 @@ +use near_o11y::metrics::prometheus; +use near_o11y::metrics::prometheus::core::{GenericCounter, GenericGauge}; +use near_primitives::errors::StorageError; +use near_primitives::hash::CryptoHash; +use near_primitives::shard_layout::ShardUId; +use near_vm_runner::logic::TrieNodesCount; +use std::collections::HashMap; +use std::sync::Arc; + +use crate::{metrics, TrieStorage}; + +/// Deterministic cache to store trie nodes that have been accessed so far +/// during the cache's lifetime. It is used for deterministic gas accounting +/// so that previously accessed trie nodes and values are charged at a +/// cheaper gas cost. +/// +/// This cache's correctness is critical as it contributes to the gas +/// accounting of storage operations during contract execution. For that +/// reason, a new TrieAccountingCache must be created at the beginning of a +/// chunk's execution, and the db_read_nodes and mem_read_nodes must be taken +/// into account whenever a storage operation is performed to calculate what +/// kind of operation it was. +/// +/// Note that we don't have a size limit for values in the accounting cache. +/// There are two reasons: +/// - for nodes, value size is an implementation detail. If we change +/// internal representation of a node (e.g. change `memory_usage` field +/// from `RawTrieNodeWithSize`), this would have to be a protocol upgrade. +/// - total size of all values is limited by the runtime fees. More +/// thoroughly: +/// - number of nodes is limited by receipt gas limit / touching trie +/// node fee ~= 500 Tgas / 16 Ggas = 31_250; +/// - size of trie keys and values is limited by receipt gas limit / +/// lowest per byte fee (`storage_read_value_byte`) ~= +/// (500 * 10**12 / 5611005) / 2**20 ~= 85 MB. +/// All values are given as of 16/03/2022. We may consider more precise limit +/// for the accounting cache as well. +/// +/// Note that in general, it is NOT true that all storage access is either a +/// db read or mem read. It can also be a flat storage read, which is not +/// tracked via TrieAccountingCache. +pub struct TrieAccountingCache { + /// Whether the cache is enabled. By default it is not, but it can be + /// turned on or off on the fly. + enable: bool, + /// Cache of trie node hash -> trie node body, or a leaf value hash -> + /// leaf value. + cache: HashMap>, + /// The number of times a key was accessed by reading from the underlying + /// storage. (This does not necessarily mean it was accessed from *disk*, + /// as the underlying storage layer may have a best-effort cache.) + db_read_nodes: u64, + /// The number of times a key was accessed when it was deterministically + /// already cached during the processing of this chunk. + mem_read_nodes: u64, + /// Prometheus metrics. It's optional - in testing it can be None. + metrics: Option, +} + +struct TrieAccountingCacheMetrics { + accounting_cache_hits: GenericCounter, + accounting_cache_misses: GenericCounter, + accounting_cache_size: GenericGauge, +} + +impl TrieAccountingCache { + /// Constructs a new accounting cache. By default it is not enabled. + /// The optional parameter is passed in if prometheus metrics are desired. + pub fn new(shard_uid_and_is_view: Option<(ShardUId, bool)>) -> Self { + let metrics = shard_uid_and_is_view.map(|(shard_uid, is_view)| { + let mut buffer = itoa::Buffer::new(); + let shard_id = buffer.format(shard_uid.shard_id); + + let metrics_labels: [&str; 2] = [&shard_id, if is_view { "1" } else { "0" }]; + TrieAccountingCacheMetrics { + accounting_cache_hits: metrics::CHUNK_CACHE_HITS.with_label_values(&metrics_labels), + accounting_cache_misses: metrics::CHUNK_CACHE_MISSES + .with_label_values(&metrics_labels), + accounting_cache_size: metrics::CHUNK_CACHE_SIZE.with_label_values(&metrics_labels), + } + }); + Self { enable: false, cache: HashMap::new(), db_read_nodes: 0, mem_read_nodes: 0, metrics } + } + + pub fn set_enabled(&mut self, enabled: bool) { + self.enable = enabled; + } + + /// Retrieve raw bytes from the cache if it exists, otherwise retrieve it + /// from the given storage, and count it as a db access. + pub fn retrieve_raw_bytes_with_accounting( + &mut self, + hash: &CryptoHash, + storage: &dyn TrieStorage, + ) -> Result, StorageError> { + if let Some(node) = self.cache.get(hash) { + self.mem_read_nodes += 1; + if let Some(metrics) = &self.metrics { + metrics.accounting_cache_hits.inc(); + } + Ok(node.clone()) + } else { + self.db_read_nodes += 1; + if let Some(metrics) = &self.metrics { + metrics.accounting_cache_misses.inc(); + } + let node = storage.retrieve_raw_bytes(hash)?; + + if self.enable { + self.cache.insert(*hash, node.clone()); + if let Some(metrics) = &self.metrics { + metrics.accounting_cache_size.set(self.cache.len() as i64); + } + } + Ok(node) + } + } + + pub fn get_trie_nodes_count(&self) -> TrieNodesCount { + TrieNodesCount { db_reads: self.db_read_nodes, mem_reads: self.mem_read_nodes } + } +} diff --git a/core/store/src/trie/iterator.rs b/core/store/src/trie/iterator.rs index 200931f7508..c0fb3b96528 100644 --- a/core/store/src/trie/iterator.rs +++ b/core/store/src/trie/iterator.rs @@ -296,10 +296,7 @@ impl<'a> TrieIterator<'a> { prefix } - /// Note that path_begin and path_end are not bytes, they are nibbles - /// Visits all nodes belonging to the interval [path_begin, path_end) in depth-first search - /// order and return key-value pairs for each visited node with value stored - /// Used to generate split states for re-sharding + // TODO(#9446) remove function when shifting to flat storage iteration for resharding pub(crate) fn get_trie_items( &mut self, path_begin: &[u8], @@ -372,7 +369,7 @@ impl<'a> TrieIterator<'a> { if self.key_nibbles[prefix..] >= path_end[prefix..] { break; } - self.trie.storage.retrieve_raw_bytes(&hash)?; + self.trie.retrieve_value(&hash)?; nodes_list.push(TrieTraversalItem { hash, key: self.has_value().then(|| self.key()), @@ -417,10 +414,7 @@ impl<'a> Iterator for TrieIterator<'a> { }, (IterStep::Value(hash), true) => { return Some( - self.trie - .storage - .retrieve_raw_bytes(&hash) - .map(|value| (self.key(), value.to_vec())), + self.trie.retrieve_value(&hash).map(|value| (self.key(), value.to_vec())), ) } } @@ -478,15 +472,8 @@ mod tests { } test_seek_prefix(&trie, &map, &[]); - let empty_vec = vec![]; - let max_key = map.keys().max().unwrap_or(&empty_vec); - let min_key = map.keys().min().unwrap_or(&empty_vec); - test_get_trie_items(&trie, &map, &[], &[]); - test_get_trie_items(&trie, &map, min_key, max_key); for (seek_key, _) in trie_changes.iter() { test_seek_prefix(&trie, &map, seek_key); - test_get_trie_items(&trie, &map, min_key, seek_key); - test_get_trie_items(&trie, &map, seek_key, max_key); } for _ in 0..20 { let alphabet = &b"abcdefgh"[0..rng.gen_range(2..8)]; @@ -494,12 +481,6 @@ mod tests { let seek_key: Vec = (0..key_length).map(|_| *alphabet.choose(&mut rng).unwrap()).collect(); test_seek_prefix(&trie, &map, &seek_key); - - let seek_key2: Vec = - (0..key_length).map(|_| *alphabet.choose(&mut rng).unwrap()).collect(); - let path_begin = seek_key.clone().min(seek_key2.clone()); - let path_end = seek_key.clone().max(seek_key2.clone()); - test_get_trie_items(&trie, &map, &path_begin, &path_end); } } } @@ -647,29 +628,6 @@ mod tests { (trie_changes, map, trie) } - fn test_get_trie_items( - trie: &Trie, - map: &BTreeMap, Vec>, - path_begin: &[u8], - path_end: &[u8], - ) { - let path_begin_nibbles: Vec<_> = NibbleSlice::new(path_begin).iter().collect(); - let path_end_nibbles: Vec<_> = NibbleSlice::new(path_end).iter().collect(); - let result1 = - trie.iter().unwrap().get_trie_items(&path_begin_nibbles, &path_end_nibbles).unwrap(); - let result2: Vec<_> = map - .range(path_begin.to_vec()..path_end.to_vec()) - .map(|(k, v)| (k.clone(), v.clone())) - .collect(); - assert_eq!(result1, result2); - - // test when path_end ends in [16] - let result1 = trie.iter().unwrap().get_trie_items(&path_begin_nibbles, &[16u8]).unwrap(); - let result2: Vec<_> = - map.range(path_begin.to_vec()..).map(|(k, v)| (k.clone(), v.clone())).collect(); - assert_eq!(result1, result2); - } - fn test_seek_prefix(trie: &Trie, map: &BTreeMap, Vec>, seek_key: &[u8]) { let mut iterator = trie.iter().unwrap(); iterator.seek_prefix(&seek_key).unwrap(); diff --git a/core/store/src/trie/mod.rs b/core/store/src/trie/mod.rs index aa6a1efd59a..b20088d98d7 100644 --- a/core/store/src/trie/mod.rs +++ b/core/store/src/trie/mod.rs @@ -11,11 +11,9 @@ pub use crate::trie::shard_tries::{ KeyForStateChanges, ShardTries, StateSnapshot, StateSnapshotConfig, WrappedTrieChanges, }; pub use crate::trie::trie_storage::{TrieCache, TrieCachingStorage, TrieDBStorage, TrieStorage}; -use crate::trie::trie_storage::{TrieMemoryPartialStorage, TrieRecordingStorage}; use crate::StorageError; use borsh::{BorshDeserialize, BorshSerialize}; use near_primitives::challenge::PartialState; -use near_primitives::contract::ContractCode; use near_primitives::hash::{hash, CryptoHash}; pub use near_primitives::shard_layout::ShardUId; use near_primitives::state::ValueRef; @@ -23,6 +21,7 @@ use near_primitives::state_record::StateRecord; use near_primitives::trie_key::TrieKey; pub use near_primitives::types::TrieNodesCount; use near_primitives::types::{StateRoot, StateRootNode}; +use near_vm_runner::ContractCode; pub use raw_node::{Children, RawTrieNode, RawTrieNodeWithSize}; use std::cell::RefCell; use std::collections::HashMap; @@ -30,7 +29,9 @@ use std::fmt::Write; use std::hash::{Hash, Hasher}; use std::rc::Rc; use std::str; +use std::sync::Arc; +pub mod accounting_cache; mod config; mod from_flat; mod insert_delete; @@ -41,11 +42,15 @@ mod raw_node; mod shard_tries; pub mod split_state; mod state_parts; +mod trie_recording; mod trie_storage; #[cfg(test)] mod trie_tests; pub mod update; +use self::accounting_cache::TrieAccountingCache; +use self::trie_recording::TrieRecorder; +use self::trie_storage::TrieMemoryPartialStorage; pub use from_flat::construct_trie_from_flat; const POISONED_LOCK_ERR: &str = "The lock was poisoned."; @@ -321,9 +326,31 @@ impl std::fmt::Debug for TrieNode { } pub struct Trie { - pub storage: Rc, + storage: Rc, root: StateRoot, - pub flat_storage_chunk_view: Option, + /// If present, flat storage is used to look up keys (if asked for). + /// Otherwise, we would crawl through the trie. + flat_storage_chunk_view: Option, + /// This is the deterministic accounting cache, meaning that for the + /// lifetime of this Trie struct, whenever the accounting cache is enabled + /// (which can be toggled on the fly), trie nodes that have been looked up + /// once will be guaranteed to be cached, and further reads to these nodes + /// will encounter less gas cost. + accounting_cache: RefCell, + /// If present, we're capturing all trie nodes that have been accessed + /// during the lifetime of this Trie struct. This is used to produce a + /// state proof so that the same access pattern can be replayed using only + /// the captured result. + recorder: Option>, + /// This can only be true if the storage is based on a recorded partial + /// trie, i.e. replaying lookups on a state proof, where flat storage may + /// not be available so we always have to go through the trie. If this + /// flag is true, trie node lookups will not go through the accounting + /// cache, i.e. the access is free from the trie's perspective, just like + /// flat storage. (Note that dereferencing ValueRef still has the same cost + /// no matter what.) This allows us to accurately calculate storage gas + /// costs even with only a state proof. + skip_accounting_cache_for_trie_nodes: bool, } /// Trait for reading data from a trie. @@ -426,44 +453,105 @@ enum NodeOrValue { impl Trie { pub const EMPTY_ROOT: StateRoot = StateRoot::new(); + /// Starts accessing a trie with the given storage. + /// By default, the accounting cache is not enabled. To enable or disable it + /// (only in this crate), call self.accounting_cache.borrow_mut().set_enabled(). pub fn new( storage: Rc, root: StateRoot, flat_storage_chunk_view: Option, ) -> Self { - Trie { storage, root, flat_storage_chunk_view } + let accounting_cache = match storage.as_caching_storage() { + Some(caching_storage) => RefCell::new(TrieAccountingCache::new(Some(( + caching_storage.shard_uid, + caching_storage.is_view, + )))), + None => RefCell::new(TrieAccountingCache::new(None)), + }; + Trie { + storage, + root, + flat_storage_chunk_view, + accounting_cache: accounting_cache, + recorder: None, + skip_accounting_cache_for_trie_nodes: false, + } } + /// Makes a new trie that has everything the same except that access + /// through that trie accumulates a state proof for all nodes accessed. pub fn recording_reads(&self) -> Self { - let storage = TrieRecordingStorage { - storage: Rc::clone(&self.storage), - recorded: RefCell::new(Default::default()), - }; - Trie { storage: Rc::new(storage), root: self.root, flat_storage_chunk_view: None } + let mut trie = + Self::new(self.storage.clone(), self.root, self.flat_storage_chunk_view.clone()); + trie.recorder = Some(RefCell::new(TrieRecorder::new())); + trie } + /// Takes the recorded state proof out of the trie. pub fn recorded_storage(&self) -> Option { - let storage = self.storage.as_recording_storage()?; - let mut nodes: Vec<_> = - storage.recorded.borrow_mut().drain().map(|(_key, value)| value).collect(); - nodes.sort(); - Some(PartialStorage { nodes: PartialState::TrieValues(nodes) }) + self.recorder.as_ref().map(|recorder| recorder.borrow_mut().recorded_storage()) } - pub fn from_recorded_storage(partial_storage: PartialStorage, root: StateRoot) -> Self { + /// Constructs a Trie from the partial storage (i.e. state proof) that + /// was returned from recorded_storage(). If used to access the same trie + /// nodes as when the partial storage was generated, this trie will behave + /// identically. + /// + /// The flat_storage_used parameter should be true iff originally the trie + /// was accessed with flat storage present. It will be used to simulate the + /// same costs as if flat storage were present. + pub fn from_recorded_storage( + partial_storage: PartialStorage, + root: StateRoot, + flat_storage_used: bool, + ) -> Self { let PartialState::TrieValues(nodes) = partial_storage.nodes; let recorded_storage = nodes.into_iter().map(|value| (hash(&value), value)).collect(); let storage = Rc::new(TrieMemoryPartialStorage::new(recorded_storage)); - Self::new(storage, root, None) + let mut trie = Self::new(storage, root, None); + trie.skip_accounting_cache_for_trie_nodes = flat_storage_used; + trie } pub fn get_root(&self) -> &StateRoot { &self.root } + pub fn has_flat_storage_chunk_view(&self) -> bool { + self.flat_storage_chunk_view.is_some() + } + + pub fn internal_get_storage_as_caching_storage(&self) -> Option<&TrieCachingStorage> { + self.storage.as_caching_storage() + } + + /// All access to trie nodes or values must go through this method, so it + /// can be properly cached and recorded. + /// + /// count_cost can be false to skip caching. This is used when we're + /// generating a state proof, but the value is supposed to fetched from + /// flat storage. + fn internal_retrieve_trie_node( + &self, + hash: &CryptoHash, + use_accounting_cache: bool, + ) -> Result, StorageError> { + let result = if 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()); + } + Ok(result) + } + #[cfg(test)] fn memory_usage_verify(&self, memory: &NodesStorage, handle: NodeHandle) -> u64 { - if self.storage.as_recording_storage().is_some() { + if self.recorder.is_some() { return 0; } let TrieNodeWithSize { node, memory_usage } = match handle { @@ -510,7 +598,7 @@ impl Trie { ) -> Result<(), StorageError> { match value { ValueHandle::HashAndSize(value) => { - let bytes = self.storage.retrieve_raw_bytes(&value.hash)?; + let bytes = self.internal_retrieve_trie_node(&value.hash, true)?; memory .refcount_changes .entry(value.hash) @@ -527,7 +615,7 @@ impl Trie { // Prints the trie nodes starting from hash, up to max_depth depth. // The node hash can be any node in the trie. pub fn print_recursive(&self, f: &mut dyn std::io::Write, hash: &CryptoHash, max_depth: u32) { - match self.retrieve_raw_node_or_value(hash) { + match self.debug_retrieve_raw_node_or_value(hash) { Ok(NodeOrValue::Node(_)) => { let mut prefix: Vec = Vec::new(); self.print_recursive_internal(f, hash, max_depth, &mut "".to_string(), &mut prefix) @@ -606,7 +694,7 @@ impl Trie { return Ok(()); } - let (bytes, raw_node, mem_usage) = match self.retrieve_raw_node(hash) { + let (bytes, raw_node, mem_usage) = match self.retrieve_raw_node(hash, 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}"), @@ -682,11 +770,12 @@ impl Trie { fn retrieve_raw_node( &self, hash: &CryptoHash, + use_accounting_cache: bool, ) -> Result, RawTrieNodeWithSize)>, StorageError> { if hash == &Self::EMPTY_ROOT { return Ok(None); } - let bytes = self.storage.retrieve_raw_bytes(hash)?; + let bytes = self.internal_retrieve_trie_node(hash, use_accounting_cache)?; let node = RawTrieNodeWithSize::try_from_slice(&bytes).map_err(|err| { StorageError::StorageInconsistentState(format!("Failed to decode node {hash}: {err}")) })?; @@ -696,8 +785,11 @@ impl Trie { // Similar to retrieve_raw_node but handles the case where there is a Value (and not a Node) in the database. // This method is not safe to be used in any real scenario as it can incorrectly interpret a value as a trie node. // It's only provided as a convenience for debugging tools. - fn retrieve_raw_node_or_value(&self, hash: &CryptoHash) -> Result { - let bytes = self.storage.retrieve_raw_bytes(hash)?; + fn debug_retrieve_raw_node_or_value( + &self, + hash: &CryptoHash, + ) -> Result { + let bytes = self.internal_retrieve_trie_node(hash, true)?; match RawTrieNodeWithSize::try_from_slice(&bytes) { Ok(node) => Ok(NodeOrValue::Node(node)), Err(_) => Ok(NodeOrValue::Value(bytes)), @@ -709,7 +801,7 @@ impl Trie { memory: &mut NodesStorage, hash: &CryptoHash, ) -> Result { - match self.retrieve_raw_node(hash)? { + match self.retrieve_raw_node(hash, true)? { None => Ok(memory.store(TrieNodeWithSize::empty())), Some((bytes, node)) => { let result = memory.store(TrieNodeWithSize::from_raw(node)); @@ -729,14 +821,14 @@ impl Trie { &self, hash: &CryptoHash, ) -> Result<(Option>, TrieNodeWithSize), StorageError> { - match self.retrieve_raw_node(hash)? { + match self.retrieve_raw_node(hash, 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)? { + match self.retrieve_raw_node(&self.root, true)? { None => Ok(StateRootNode::empty()), Some((bytes, node)) => { Ok(StateRootNode { data: bytes, memory_usage: node.memory_usage }) @@ -744,10 +836,14 @@ impl Trie { } } - fn lookup(&self, mut key: NibbleSlice<'_>) -> Result, StorageError> { + fn lookup( + &self, + mut key: NibbleSlice<'_>, + use_accounting_cache: bool, + ) -> Result, StorageError> { let mut hash = self.root; loop { - let node = match self.retrieve_raw_node(&hash)? { + let node = match self.retrieve_raw_node(&hash, use_accounting_cache)? { None => return Ok(None), Some((_bytes, node)) => node.node, }; @@ -822,7 +918,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)?; + let mut node = self.retrieve_raw_node(&self.root, true)?; while !key.is_empty() { match node { Some((_, raw_node)) => match raw_node.node { @@ -834,7 +930,7 @@ impl Trie { let child = children[key.at(0)]; match child { Some(child) => { - node = self.retrieve_raw_node(&child)?; + node = self.retrieve_raw_node(&child, true)?; key = key.mid(1); } None => return Ok(None), @@ -843,7 +939,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)?; + node = self.retrieve_raw_node(&child, true)?; key = key.mid(existing_key.len()); } else { return Ok(None); @@ -859,10 +955,10 @@ impl Trie { } } - /// For debugging only. Returns the raw bytes corresponding to a ValueRef that came - /// from a node with value (either Leaf or BranchWithValue). - pub fn debug_get_value(&self, value_ref: &ValueRef) -> Result, StorageError> { - let bytes = self.storage.retrieve_raw_bytes(&value_ref.hash)?; + /// 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)?; Ok(bytes.to_vec()) } @@ -885,19 +981,32 @@ impl Trie { matches!(mode, KeyLookupMode::FlatStorage) && self.flat_storage_chunk_view.is_some(); if use_flat_storage { - let flat_state_value = - self.flat_storage_chunk_view.as_ref().unwrap().get_value(&key)?; - Ok(flat_state_value.map(|value| value.to_value_ref())) + let value_from_flat_storage = self + .flat_storage_chunk_view + .as_ref() + .unwrap() + .get_value(&key)? + .map(|value| value.to_value_ref()); + if 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 key_nibbles = NibbleSlice::new(key); + let value_from_trie = self.lookup(key_nibbles, false)?; + assert_eq!(&value_from_flat_storage, &value_from_trie); + } + Ok(value_from_flat_storage) } else { let key_nibbles = NibbleSlice::new(key); - self.lookup(key_nibbles) + self.lookup(key_nibbles, !self.skip_accounting_cache_for_trie_nodes) } } pub fn get(&self, key: &[u8]) -> Result>, StorageError> { match self.get_ref(key, KeyLookupMode::FlatStorage)? { Some(ValueRef { hash, .. }) => { - self.storage.retrieve_raw_bytes(&hash).map(|bytes| Some(bytes.to_vec())) + self.internal_retrieve_trie_node(&hash, true).map(|bytes| Some(bytes.to_vec())) } None => Ok(None), } @@ -972,7 +1081,7 @@ impl Trie { } pub fn get_trie_nodes_count(&self) -> TrieNodesCount { - self.storage.get_trie_nodes_count() + self.accounting_cache.borrow().get_trie_nodes_count() } } @@ -1346,7 +1455,7 @@ mod tests { trie2.get(b"horse").unwrap(); let partial_storage = trie2.recorded_storage(); - let trie3 = Trie::from_recorded_storage(partial_storage.unwrap(), root); + let trie3 = Trie::from_recorded_storage(partial_storage.unwrap(), root, false); assert_eq!(trie3.get(b"dog"), Ok(Some(b"puppy".to_vec()))); assert_eq!(trie3.get(b"horse"), Ok(Some(b"stallion".to_vec()))); diff --git a/core/store/src/trie/prefetching_trie_storage.rs b/core/store/src/trie/prefetching_trie_storage.rs index 83cf7ac7ac9..6b872dea6c4 100644 --- a/core/store/src/trie/prefetching_trie_storage.rs +++ b/core/store/src/trie/prefetching_trie_storage.rs @@ -10,7 +10,7 @@ use near_o11y::tracing::error; use near_primitives::hash::CryptoHash; use near_primitives::shard_layout::ShardUId; use near_primitives::trie_key::TrieKey; -use near_primitives::types::{AccountId, ShardId, StateRoot, TrieNodesCount}; +use near_primitives::types::{AccountId, ShardId, StateRoot}; use std::collections::HashMap; use std::rc::Rc; use std::sync::Arc; @@ -291,10 +291,6 @@ impl TrieStorage for TriePrefetchingStorage { )), } } - - fn get_trie_nodes_count(&self) -> TrieNodesCount { - unimplemented!() - } } impl TriePrefetchingStorage { diff --git a/core/store/src/trie/shard_tries.rs b/core/store/src/trie/shard_tries.rs index 45d8c24142b..f1264a8002a 100644 --- a/core/store/src/trie/shard_tries.rs +++ b/core/store/src/trie/shard_tries.rs @@ -1,11 +1,15 @@ +use crate::db::STATE_SNAPSHOT_KEY; use crate::flat::FlatStorageManager; +use crate::option_to_not_found; use crate::trie::config::TrieConfig; use crate::trie::prefetching_trie_storage::PrefetchingThreadsHandle; use crate::trie::trie_storage::{TrieCache, TrieCachingStorage}; use crate::trie::{TrieRefcountChange, POISONED_LOCK_ERR}; +use crate::Mode; use crate::{checkpoint_hot_storage_and_cleanup_columns, metrics, DBCol, NodeStorage, PrefetchApi}; use crate::{Store, StoreConfig, StoreUpdate, Trie, TrieChanges, TrieUpdate}; use borsh::BorshSerialize; +use near_primitives::block::Block; use near_primitives::borsh::maybestd::collections::HashMap; use near_primitives::errors::EpochError; use near_primitives::errors::StorageError; @@ -16,9 +20,9 @@ use near_primitives::trie_key::TrieKey; use near_primitives::types::{ NumShards, RawStateChange, RawStateChangesWithTrieKey, StateChangeCause, StateRoot, }; +use std::io; use std::path::{Path, PathBuf}; use std::rc::Rc; -use std::str::FromStr; use std::sync::{Arc, RwLock, TryLockError}; struct ShardTriesInner { @@ -60,22 +64,36 @@ impl StateSnapshot { prev_block_hash: CryptoHash, flat_storage_manager: FlatStorageManager, 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 { 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"); - } else { + continue; + } + if let Some(block) = block { let flat_storage = flat_storage_manager.get_flat_storage_for_shard(*shard_uid).unwrap(); - tracing::debug!(target: "state_snapshot", ?shard_uid, current_flat_head = ?flat_storage.get_head_hash(), desired_flat_head = ?prev_block_hash, "Moving FlatStorage head of the snapshot"); + let current_flat_head = flat_storage.get_head_hash(); + tracing::debug!(target: "state_snapshot", ?shard_uid, ?current_flat_head, block_hash = ?block.header().hash(), block_height = block.header().height(), "Moving FlatStorage head of the snapshot"); let _timer = metrics::MOVE_STATE_SNAPSHOT_FLAT_HEAD_ELAPSED .with_label_values(&[&shard_uid.shard_id.to_string()]) .start_timer(); - if let Err(err) = flat_storage.update_flat_head(&prev_block_hash) { - tracing::error!(target: "state_snapshot", ?err, ?shard_uid, current_flat_head = ?flat_storage.get_head_hash(), ?prev_block_hash, "Failed to Move FlatStorage head of the snapshot"); + if let Some(chunk) = block.chunks().get(shard_uid.shard_id as usize) { + // Flat state snapshot needs to be at a height that lets it + // replay the last chunk of the shard. + let desired_flat_head = chunk.prev_block_hash(); + match flat_storage.update_flat_head(desired_flat_head, true) { + Ok(_) => { + tracing::debug!(target: "state_snapshot", ?shard_uid, ?current_flat_head, ?desired_flat_head, "Successfully moved FlatStorage head of the snapshot"); + } + Err(err) => { + tracing::error!(target: "state_snapshot", ?shard_uid, ?err, ?current_flat_head, ?desired_flat_head, "Failed to move FlatStorage head of the snapshot"); + } + } } else { - tracing::debug!(target: "state_snapshot", ?shard_uid, new_flat_head = ?flat_storage.get_head_hash(), desired_flat_head = ?prev_block_hash, "Successfully moved FlatStorage head of the snapshot"); + tracing::error!(target: "state_snapshot", ?shard_uid, current_flat_head = ?flat_storage.get_head_hash(), ?prev_block_hash, "Failed to move FlatStorage head of the snapshot, no chunk"); } } } @@ -176,7 +194,6 @@ impl ShardTries { TrieUpdate::new(self.get_view_trie_for_shard(shard_uid, state_root)) } - #[allow(unused_variables)] fn get_trie_for_shard_internal( &self, shard_uid: ShardUId, @@ -437,6 +454,7 @@ impl ShardTries { &self, prev_block_hash: &CryptoHash, shard_uids: &[ShardUId], + block: &Block, ) -> Result<(), anyhow::Error> { metrics::HAS_STATE_SNAPSHOT.set(0); // The function returns an `anyhow::Error`, because no special handling of errors is done yet. The errors are logged and ignored. @@ -458,21 +476,55 @@ impl ShardTries { let _timer = metrics::MAKE_STATE_SNAPSHOT_ELAPSED.start_timer(); // `write()` lock is held for the whole duration of this function. // Accessing the snapshot in other parts of the system will fail. - let mut state_snapshot_lock = self.0.state_snapshot.write().unwrap(); + let mut state_snapshot_lock = self.0.state_snapshot.write().map_err(|_| { + anyhow::Error::msg("error accessing write lock of state_snapshot") + })?; + let db_snapshot_hash = self.get_state_snapshot_hash(); + if let Some(state_snapshot) = &*state_snapshot_lock { - if &state_snapshot.prev_block_hash == prev_block_hash { + // 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() + && db_snapshot_hash.unwrap() == *prev_block_hash + && state_snapshot.prev_block_hash == *prev_block_hash + { tracing::warn!(target: "state_snapshot", ?prev_block_hash, "Requested a state snapshot but that is already available"); return Ok(()); } else { - let prev_block_hash = state_snapshot.prev_block_hash; // Drop Store before deleting the underlying data. *state_snapshot_lock = None; - self.delete_state_snapshot( - &prev_block_hash, - home_dir, - hot_store_path, - state_snapshot_subdir, - ); + + // This will delete all existing snapshots from file system. If failed, will retry until success + let mut delete_state_snapshots_from_file_system = false; + let mut file_system_delete_retries = 0; + while !delete_state_snapshots_from_file_system + && file_system_delete_retries < 3 + { + delete_state_snapshots_from_file_system = self + .delete_all_state_snapshots( + home_dir, + hot_store_path, + state_snapshot_subdir, + ); + file_system_delete_retries += 1; + } + + // this will delete the STATE_SNAPSHOT_KEY-value pair from db. If failed, will retry until success + let mut delete_state_snapshot_from_db = false; + let mut db_delete_retries = 0; + while !delete_state_snapshot_from_db && db_delete_retries < 3 { + delete_state_snapshot_from_db = match self.set_state_snapshot_hash(None) + { + Ok(_) => true, + Err(err) => { + // This will be retried. + tracing::debug!(target: "state_snapshot", ?err, "Failed to delete the old state snapshot for BlockMisc::STATE_SNAPSHOT_KEY in rocksdb"); + false + } + }; + db_delete_retries += 1; + } + + metrics::HAS_STATE_SNAPSHOT.set(0); } } @@ -507,7 +559,24 @@ impl ShardTries { *prev_block_hash, flat_storage_manager, shard_uids, + Some(block), )); + + // 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, + 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 + } + } + } + metrics::HAS_STATE_SNAPSHOT.set(1); tracing::info!(target: "state_snapshot", ?prev_block_hash, "Made a checkpoint"); Ok(()) @@ -520,7 +589,11 @@ impl ShardTries { let _span = tracing::info_span!(target: "state_snapshot", "compact_state_snapshot").entered(); // It's fine if the access to state snapshot blocks. - let state_snapshot_lock = self.0.state_snapshot.read().unwrap(); + let state_snapshot_lock = self + .0 + .state_snapshot + .read() + .map_err(|_| anyhow::Error::msg("error accessing read lock of state_snapshot"))?; if let Some(state_snapshot) = &*state_snapshot_lock { let _timer = metrics::COMPACT_STATE_SNAPSHOT_ELAPSED.start_timer(); Ok(state_snapshot.store.compact()?) @@ -530,30 +603,25 @@ impl ShardTries { } } - /// Deletes a previously open state snapshot. - /// Drops the Store and deletes the files. - fn delete_state_snapshot( + /// Deletes all existing state snapshots in the parent directory + fn delete_all_state_snapshots( &self, - prev_block_hash: &CryptoHash, home_dir: &Path, hot_store_path: &Path, state_snapshot_subdir: &Path, - ) { + ) -> bool { let _timer = metrics::DELETE_STATE_SNAPSHOT_ELAPSED.start_timer(); let _span = tracing::info_span!(target: "state_snapshot", "delete_state_snapshot").entered(); - let path = Self::get_state_snapshot_base_dir( - prev_block_hash, - home_dir, - hot_store_path, - state_snapshot_subdir, - ); + let path = home_dir.join(hot_store_path).join(state_snapshot_subdir); match std::fs::remove_dir_all(&path) { Ok(_) => { - tracing::info!(target: "state_snapshot", ?path, ?prev_block_hash, "Deleted a state snapshot"); + tracing::info!(target: "state_snapshot", ?path, "Deleted all state snapshots"); + true } Err(err) => { - tracing::warn!(target: "state_snapshot", ?err, ?path, ?prev_block_hash, "Failed to delete a state snapshot"); + tracing::warn!(target: "state_snapshot", ?err, ?path, "Failed to delete all state snapshots"); + false } } } @@ -570,8 +638,27 @@ impl ShardTries { home_dir.join(hot_store_path).join(state_snapshot_subdir).join(format!("{prev_block_hash}")) } - /// Looks for directories on disk with names that look like `prev_block_hash`. - /// Checks that there is at most one such directory. Opens it as a Store. + /// Retrieves STATE_SNAPSHOT_KEY + pub fn get_state_snapshot_hash(&self) -> Result { + option_to_not_found( + self.0.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.0.store.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().map_err(|err| err.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( &self, get_shard_uids_fn: impl Fn(CryptoHash) -> Result, EpochError>, @@ -590,66 +677,41 @@ impl ShardTries { state_snapshot_subdir, compaction_enabled: _, } => { - let path = Self::get_state_snapshot_base_dir( - &CryptoHash::new(), + // directly return error if no snapshot is found + let snapshot_hash: CryptoHash = self.get_state_snapshot_hash()?; + + let snapshot_path = Self::get_state_snapshot_base_dir( + &snapshot_hash, &home_dir, &hot_store_path, &state_snapshot_subdir, ); - let parent_path = - path.parent().ok_or(anyhow::anyhow!("{path:?} needs to have a parent dir"))?; - tracing::debug!(target: "state_snapshot", ?path, ?parent_path); - - let snapshots = match std::fs::read_dir(parent_path) { - Err(err) => { - if err.kind() == std::io::ErrorKind::NotFound { - tracing::debug!(target: "state_snapshot", ?parent_path, "State Snapshot base directory doesn't exist."); - return Ok(()); - } else { - return Err(err.into()); - } - } - Ok(entries) => { - let mut snapshots = vec![]; - for entry in entries.filter_map(Result::ok) { - let file_name = entry.file_name().into_string().map_err(|err| { - anyhow::anyhow!("Can't display file_name: {err:?}") - })?; - if let Ok(prev_block_hash) = CryptoHash::from_str(&file_name) { - snapshots.push((prev_block_hash, entry.path())); - } - } - snapshots - } - }; + let parent_path = snapshot_path + .parent() + .ok_or(anyhow::anyhow!("{snapshot_path:?} needs to have a parent dir"))?; + tracing::debug!(target: "state_snapshot", ?snapshot_path, ?parent_path); - if snapshots.is_empty() { - Ok(()) - } else if snapshots.len() == 1 { - let (prev_block_hash, snapshot_dir) = &snapshots[0]; - - let store_config = StoreConfig::default(); - - let opener = NodeStorage::opener(&snapshot_dir, false, &store_config, None); - let storage = opener.open()?; - let store = storage.get_hot_store(); - let flat_storage_manager = FlatStorageManager::new(store.clone()); - - let shard_uids = get_shard_uids_fn(*prev_block_hash)?; - let mut guard = self.0.state_snapshot.write().unwrap(); - *guard = Some(StateSnapshot::new( - store, - *prev_block_hash, - flat_storage_manager, - &shard_uids, - )); - metrics::HAS_STATE_SNAPSHOT.set(1); - tracing::info!(target: "runtime", ?prev_block_hash, ?snapshot_dir, "Detected and opened a state snapshot."); - Ok(()) - } else { - tracing::error!(target: "runtime", ?snapshots, "Detected multiple state snapshots. Please keep at most one snapshot and delete others."); - Err(anyhow::anyhow!("More than one state snapshot detected {:?}", snapshots)) - } + let store_config = StoreConfig::default(); + + 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 shard_uids = get_shard_uids_fn(snapshot_hash)?; + let mut guard = self.0.state_snapshot.write().map_err(|_| { + anyhow::Error::msg("error accessing write lock of state_snapshot") + })?; + *guard = Some(StateSnapshot::new( + store, + snapshot_hash, + flat_storage_manager, + &shard_uids, + None, + )); + metrics::HAS_STATE_SNAPSHOT.set(1); + tracing::info!(target: "runtime", ?snapshot_hash, ?snapshot_path, "Detected and opened a state snapshot."); + Ok(()) } } } @@ -663,6 +725,20 @@ pub struct WrappedTrieChanges { block_hash: CryptoHash, } +// Partial implementation. Skips `tries` due to its complexity and +// `trie_changes` and `state_changes` due to their large volume. +impl std::fmt::Debug for WrappedTrieChanges { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WrappedTrieChanges") + .field("tries", &"") + .field("shard_uid", &self.shard_uid) + .field("trie_changes", &"") + .field("state_changes", &"") + .field("block_hash", &self.block_hash) + .finish() + } +} + impl WrappedTrieChanges { pub fn new( tries: ShardTries, @@ -692,7 +768,7 @@ impl WrappedTrieChanges { /// /// NOTE: the changes are drained from `self`. pub fn state_changes_into(&mut self, store_update: &mut StoreUpdate) { - for change_with_trie_key in self.state_changes.drain(..) { + for mut change_with_trie_key in self.state_changes.drain(..) { assert!( !change_with_trie_key.changes.iter().any(|RawStateChange { cause, .. }| matches!( cause, @@ -701,13 +777,14 @@ impl WrappedTrieChanges { "NotWritableToDisk changes must never be finalized." ); - assert!( - !change_with_trie_key.changes.iter().any(|RawStateChange { cause, .. }| matches!( - cause, - StateChangeCause::Resharding - )), - "Resharding changes must never be finalized." - ); + // Resharding changes must not be finalized, however they may be introduced here when we are + // evaluating changes for split state in process_split_state function + change_with_trie_key + .changes + .retain(|change| change.cause != StateChangeCause::Resharding); + if change_with_trie_key.changes.is_empty() { + continue; + } let storage_key = if cfg!(feature = "serialize_all_state_changes") { // Serialize all kinds of state changes without any filtering. diff --git a/core/store/src/trie/split_state.rs b/core/store/src/trie/split_state.rs index 11d836231bd..63e427ffaf4 100644 --- a/core/store/src/trie/split_state.rs +++ b/core/store/src/trie/split_state.rs @@ -1,8 +1,5 @@ use crate::flat::FlatStateChanges; -use crate::trie::iterator::TrieItem; -use crate::{ - get, get_delayed_receipt_indices, set, ShardTries, StoreUpdate, Trie, TrieChanges, TrieUpdate, -}; +use crate::{get, get_delayed_receipt_indices, set, ShardTries, StoreUpdate, Trie, TrieUpdate}; use borsh::BorshDeserialize; use bytesize::ByteSize; use near_primitives::account::id::AccountId; @@ -17,14 +14,10 @@ use near_primitives::types::{ }; use std::collections::HashMap; +use super::iterator::TrieItem; + impl Trie { - /// Computes the set of trie items (nodes with keys and values) for a state part. - /// - /// # Panics - /// part_id must be in [0..num_parts) - /// - /// # Errors - /// StorageError if the storage is corrupted + // 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)?; @@ -34,7 +27,7 @@ impl Trie { impl ShardTries { /// applies `changes` to split states - /// and returns the generated TrieChanges for all split states + /// and returns the generated TrieUpdate for all split states /// Note that this function is different from the function `add_values_to_split_states` /// This function is used for applying updates to split states when processing blocks /// `add_values_to_split_states` are used to generate the initial states for shards split @@ -43,8 +36,8 @@ impl ShardTries { &self, state_roots: &HashMap, changes: StateChangesForSplitStates, - account_id_to_shard_id: &dyn Fn(&AccountId) -> ShardUId, - ) -> Result, StorageError> { + account_id_to_shard_uid: &dyn Fn(&AccountId) -> ShardUId, + ) -> Result, StorageError> { let mut trie_updates: HashMap<_, _> = self.get_trie_updates(state_roots); let mut insert_receipts = Vec::new(); for ConsolidatedStateChange { trie_key, value } in changes.changes { @@ -71,8 +64,9 @@ impl ShardTries { | TrieKey::PendingDataCount { receiver_id: account_id, .. } | TrieKey::PostponedReceipt { receiver_id: account_id, .. } | TrieKey::ContractData { account_id, .. } => { - let new_shard_uid = account_id_to_shard_id(account_id); - // we can safely unwrap here because the caller of this function guarantees trie_updates contains all shard_uids for the new shards + let new_shard_uid = account_id_to_shard_uid(account_id); + // we can safely unwrap here because the caller of this function guarantees trie_updates + // contains all shard_uids for the new shards let trie_update = trie_updates.get_mut(&new_shard_uid).unwrap(); match value { Some(value) => trie_update.set(trie_key, value), @@ -82,6 +76,9 @@ impl ShardTries { } } for (_, update) in trie_updates.iter_mut() { + // StateChangeCause should always be Resharding for processing split state. + // We do not want to commit the state_changes from resharding as they are already handled while + // processing parent shard update.commit(StateChangeCause::Resharding); } @@ -94,15 +91,10 @@ impl ShardTries { &mut trie_updates, &insert_receipts, &changes.processed_delayed_receipts, - account_id_to_shard_id, + account_id_to_shard_uid, )?; - let mut trie_changes_map = HashMap::new(); - for (shard_uid, update) in trie_updates { - let (_, trie_changes, _) = update.finalize()?; - trie_changes_map.insert(shard_uid, trie_changes); - } - Ok(trie_changes_map) + Ok(trie_updates) } /// add `values` (key-value pairs of items stored in states) to build states for new shards @@ -149,6 +141,8 @@ impl ShardTries { let mut new_state_roots = state_roots.clone(); 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); // 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)?; @@ -174,14 +168,14 @@ impl ShardTries { &self, state_roots: &HashMap, receipts: &[Receipt], - account_id_to_shard_id: &dyn Fn(&AccountId) -> ShardUId, + account_id_to_shard_uid: &dyn Fn(&AccountId) -> ShardUId, ) -> Result<(StoreUpdate, HashMap), StorageError> { let mut trie_updates: HashMap<_, _> = self.get_trie_updates(state_roots); apply_delayed_receipts_to_split_states_impl( &mut trie_updates, receipts, &[], - account_id_to_shard_id, + account_id_to_shard_uid, )?; self.finalize_and_apply_trie_updates(trie_updates) } @@ -207,7 +201,7 @@ fn apply_delayed_receipts_to_split_states_impl( trie_updates: &mut HashMap, insert_receipts: &[Receipt], delete_receipts: &[Receipt], - account_id_to_shard_id: &dyn Fn(&AccountId) -> ShardUId, + account_id_to_shard_uid: &dyn Fn(&AccountId) -> ShardUId, ) -> Result<(), StorageError> { let mut delayed_receipts_indices_by_shard = HashMap::new(); for (shard_uid, update) in trie_updates.iter() { @@ -215,7 +209,7 @@ fn apply_delayed_receipts_to_split_states_impl( } for receipt in insert_receipts { - let new_shard_uid: ShardUId = account_id_to_shard_id(&receipt.receiver_id); + let new_shard_uid: ShardUId = account_id_to_shard_uid(&receipt.receiver_id); if !trie_updates.contains_key(&new_shard_uid) { let err = format!( "Account {} is in new shard {:?} but state_roots only contains {:?}", @@ -244,7 +238,7 @@ fn apply_delayed_receipts_to_split_states_impl( } for receipt in delete_receipts { - let new_shard_uid: ShardUId = account_id_to_shard_id(&receipt.receiver_id); + let new_shard_uid: ShardUId = account_id_to_shard_uid(&receipt.receiver_id); if !trie_updates.contains_key(&new_shard_uid) { let err = format!( "Account {} is in new shard {:?} but state_roots only contains {:?}", @@ -275,6 +269,9 @@ fn apply_delayed_receipts_to_split_states_impl( TrieKey::DelayedReceiptIndices, delayed_receipts_indices_by_shard.get(shard_uid).unwrap(), ); + // StateChangeCause should always be Resharding for processing split state. + // We do not want to commit the state_changes from resharding as they are already handled while + // processing parent shard trie_update.commit(StateChangeCause::Resharding); } Ok(()) @@ -322,8 +319,7 @@ pub fn get_delayed_receipts( mod tests { use crate::split_state::{apply_delayed_receipts_to_split_states_impl, get_delayed_receipts}; use crate::test_utils::{ - create_tries, gen_changes, gen_larger_changes, gen_receipts, get_all_delayed_receipts, - simplify_changes, test_populate_trie, + create_tries, gen_changes, gen_receipts, get_all_delayed_receipts, test_populate_trie, }; use crate::{set, ShardTries, ShardUId, Trie}; @@ -331,53 +327,11 @@ mod tests { use near_primitives::borsh::BorshSerialize; use near_primitives::hash::hash; use near_primitives::receipt::{DelayedReceiptIndices, Receipt}; - use near_primitives::state_part::PartId; use near_primitives::trie_key::TrieKey; use near_primitives::types::{NumShards, StateChangeCause, StateRoot}; use rand::Rng; use std::collections::HashMap; - #[test] - fn test_get_trie_items_for_part() { - let mut rng = rand::thread_rng(); - let tries = create_tries(); - let num_parts = rng.gen_range(5..10); - - let changes = gen_larger_changes(&mut rng, 1000); - let changes = simplify_changes(&changes); - let state_root = test_populate_trie( - &tries, - &Trie::EMPTY_ROOT, - ShardUId::single_shard(), - changes.clone(), - ); - let mut expected_trie_items: Vec<_> = - changes.into_iter().map(|(key, value)| (key, value.unwrap())).collect(); - expected_trie_items.sort(); - - let trie = tries.get_trie_for_shard(ShardUId::single_shard(), state_root); - let total_trie_items = trie.get_trie_items_for_part(PartId::new(0, 1)).unwrap(); - assert_eq!(expected_trie_items, total_trie_items); - - let mut combined_trie_items = vec![]; - for part_id in 0..num_parts { - let trie_items = trie.get_trie_items_for_part(PartId::new(part_id, num_parts)).unwrap(); - combined_trie_items.extend_from_slice(&trie_items); - // check that items are split relatively evenly across all parts - assert!( - trie_items.len() - >= (total_trie_items.len() / num_parts as usize / 2) - .checked_sub(10) - .unwrap_or_default() - && trie_items.len() <= total_trie_items.len() / num_parts as usize * 2 + 10, - "part length {} avg length {}", - trie_items.len(), - total_trie_items.len() / num_parts as usize - ); - } - assert_eq!(expected_trie_items, combined_trie_items); - } - #[test] fn test_add_values_to_split_states() { let mut rng = rand::thread_rng(); diff --git a/core/store/src/trie/state_parts.rs b/core/store/src/trie/state_parts.rs index f72155b8c14..a690b4d3aaf 100644 --- a/core/store/src/trie/state_parts.rs +++ b/core/store/src/trie/state_parts.rs @@ -23,15 +23,15 @@ use crate::trie::trie_storage::TrieMemoryPartialStorage; use crate::trie::{ ApplyStatePartResult, NodeHandle, RawTrieNodeWithSize, TrieNode, TrieNodeWithSize, }; -use crate::{metrics, PartialStorage, StorageError, Trie, TrieChanges, TrieStorage}; +use crate::{metrics, PartialStorage, StorageError, Trie, TrieChanges}; use borsh::BorshDeserialize; use near_primitives::challenge::PartialState; -use near_primitives::contract::ContractCode; use near_primitives::hash::{hash, CryptoHash}; use near_primitives::state::FlatStateValue; use near_primitives::state_part::PartId; use near_primitives::state_record::is_contract_code_key; use near_primitives::types::{ShardId, StateRoot}; +use near_vm_runner::ContractCode; use std::collections::{HashMap, HashSet}; use std::rc::Rc; use std::sync::Arc; @@ -173,14 +173,14 @@ impl Trie { /// * part_id - number of the state part, mainly for metrics. /// * partial_state - nodes needed to generate and proof state part boundaries. /// * nibbles_begin and nibbles_end specify the range of flat storage to be read. - /// * state_storage - provides access to State for random lookups of values by hash. + /// * state_trie - provides access to State for random lookups of values by hash. pub fn get_trie_nodes_for_part_with_flat_storage( &self, part_id: PartId, partial_state: PartialState, nibbles_begin: Vec, nibbles_end: Vec, - state_storage: Rc, + state_trie: &Trie, ) -> Result { let shard_id: ShardId = self.flat_storage_chunk_view.as_ref().map_or( ShardId::MAX, // Fake value for metrics. @@ -230,9 +230,7 @@ impl Trie { .start_timer(); let looked_up_value_refs: Vec<_> = value_refs .iter() - .map(|(k, hash)| { - Ok((k.clone(), Some(state_storage.retrieve_raw_bytes(hash)?.to_vec()))) - }) + .map(|(k, hash)| Ok((k.clone(), Some(state_trie.retrieve_value(hash)?.to_vec())))) .collect::>() .unwrap(); all_state_part_items.extend(looked_up_value_refs.iter().cloned()); @@ -425,8 +423,11 @@ impl Trie { ) -> Result<(), StorageError> { let PartialState::TrieValues(nodes) = &partial_state; let num_nodes = nodes.len(); - let trie = - Trie::from_recorded_storage(PartialStorage { nodes: partial_state }, *state_root); + let trie = Trie::from_recorded_storage( + PartialStorage { nodes: partial_state }, + *state_root, + false, + ); trie.visit_nodes_for_state_part(part_id)?; let storage = trie.storage.as_partial_storage().unwrap(); @@ -451,7 +452,7 @@ impl Trie { contract_codes: vec![], }); } - let trie = Trie::from_recorded_storage(PartialStorage { nodes: part }, *state_root); + let trie = Trie::from_recorded_storage(PartialStorage { nodes: part }, *state_root, false); let path_begin = trie.find_state_part_boundary(part_id.idx, part_id.total)?; let path_end = trie.find_state_part_boundary(part_id.idx + 1, part_id.total)?; let mut iterator = trie.iter()?; @@ -460,7 +461,7 @@ impl Trie { let mut flat_state_delta = FlatStateChanges::default(); let mut contract_codes = Vec::new(); for TrieTraversalItem { hash, key } in trie_traversal_items { - let value = trie.storage.retrieve_raw_bytes(&hash)?; + let value = trie.retrieve_value(&hash)?; map.entry(hash).or_insert_with(|| (value.to_vec(), 0)).1 += 1; if let Some(trie_key) = key { let flat_state_value = FlatStateValue::on_disk(&value); @@ -501,9 +502,6 @@ impl Trie { } } -/// TODO (#8997): test set seems incomplete. Perhaps `get_trie_items_for_part` -/// should also belong to this file. We need to use it to check that state -/// parts are continuous and disjoint. Maybe it is checked in split_state.rs. #[cfg(test)] mod tests { use assert_matches::assert_matches; @@ -617,13 +615,13 @@ mod tests { .cloned() .collect(), ); - let trie = Trie::from_recorded_storage(PartialStorage { nodes }, *state_root); + let trie = Trie::from_recorded_storage(PartialStorage { nodes }, *state_root, false); let mut insertions = , u32)>>::new(); trie.traverse_all_nodes(|hash| { if let Some((_bytes, rc)) = insertions.get_mut(hash) { *rc += 1; } else { - let bytes = trie.storage.retrieve_raw_bytes(hash)?; + let bytes = trie.retrieve_value(hash)?; insertions.insert(*hash, (bytes.to_vec(), 1)); } Ok(()) @@ -1190,7 +1188,7 @@ mod tests { partial_state, nibbles_begin, nibbles_end, - trie_without_flat.storage.clone(), + &trie_without_flat, ), Err(StorageError::MissingTrieValue) ); @@ -1213,7 +1211,7 @@ mod tests { partial_state.clone(), nibbles_begin.clone(), nibbles_end.clone(), - trie_without_flat.storage.clone(), + &trie_without_flat, ); assert_eq!(state_part_with_flat, Ok(state_part.clone())); @@ -1237,7 +1235,7 @@ mod tests { partial_state.clone(), nibbles_begin.clone(), nibbles_end.clone(), - trie_without_flat.storage.clone(), + &trie_without_flat, ), Ok(state_part) ); @@ -1256,7 +1254,7 @@ mod tests { partial_state, nibbles_begin, nibbles_end, - trie_without_flat.storage.clone(), + &trie_without_flat, ), Err(StorageError::MissingTrieValue) ); diff --git a/core/store/src/trie/trie_recording.rs b/core/store/src/trie/trie_recording.rs new file mode 100644 index 00000000000..cc7aaff9ef9 --- /dev/null +++ b/core/store/src/trie/trie_recording.rs @@ -0,0 +1,253 @@ +use crate::PartialStorage; +use near_primitives::challenge::PartialState; +use near_primitives::hash::CryptoHash; +use std::collections::HashMap; +use std::sync::Arc; + +/// A simple struct to capture a state proof as it's being accumulated. +pub struct TrieRecorder { + recorded: HashMap>, +} + +impl TrieRecorder { + pub fn new() -> Self { + Self { recorded: HashMap::new() } + } + + pub fn record(&mut self, hash: &CryptoHash, node: Arc<[u8]>) { + self.recorded.insert(*hash, node); + } + + pub fn recorded_storage(&mut self) -> PartialStorage { + let mut nodes: Vec<_> = self.recorded.drain().map(|(_key, value)| value).collect(); + nodes.sort(); + PartialStorage { nodes: PartialState::TrieValues(nodes) } + } +} + +#[cfg(test)] +mod trie_recording_tests { + use crate::test_utils::{ + create_tries_complex, gen_larger_changes, simplify_changes, test_populate_flat_storage, + test_populate_trie, + }; + use crate::Trie; + use near_primitives::hash::CryptoHash; + use near_primitives::shard_layout::ShardUId; + use near_vm_runner::logic::TrieNodesCount; + use std::collections::HashMap; + + const NUM_ITERATIONS_PER_TEST: usize = 100; + + /// Verifies that when operating on a trie, the results are completely consistent + /// regardless of whether we're operating on the real storage (with or without chunk + /// cache), while recording reads, or when operating on recorded partial storage. + fn test_trie_recording_consistency(enable_accounting_cache: bool, use_missing_keys: bool) { + let mut rng = rand::thread_rng(); + for _ in 0..NUM_ITERATIONS_PER_TEST { + let tries = create_tries_complex(1, 2); + + let shard_uid = ShardUId { version: 1, shard_id: 0 }; + let trie_changes = gen_larger_changes(&mut rng, 50); + let trie_changes = simplify_changes(&trie_changes); + if trie_changes.is_empty() { + continue; + } + let state_root = + test_populate_trie(&tries, &Trie::EMPTY_ROOT, shard_uid, trie_changes.clone()); + let data_in_trie = trie_changes + .iter() + .map(|(key, value)| (key.clone(), value.clone().unwrap())) + .collect::>(); + let keys_to_test_with = trie_changes + .iter() + .map(|(key, _)| { + let mut key = key.clone(); + if use_missing_keys { + key.push(100); + } + key + }) + .collect::>(); + + // Let's capture the baseline node counts - this is what will happen + // in production. + let trie = tries.get_trie_for_shard(shard_uid, state_root); + trie.accounting_cache.borrow_mut().set_enabled(enable_accounting_cache); + for key in &keys_to_test_with { + assert_eq!(trie.get(key).unwrap(), data_in_trie.get(key).cloned()); + } + let baseline_trie_nodes_count = trie.get_trie_nodes_count(); + println!("Baseline trie nodes count: {:?}", baseline_trie_nodes_count); + + // Now let's do this again while recording, and make sure that the counters + // we get are exactly the same. + let trie = tries.get_trie_for_shard(shard_uid, state_root).recording_reads(); + trie.accounting_cache.borrow_mut().set_enabled(enable_accounting_cache); + for key in &keys_to_test_with { + assert_eq!(trie.get(key).unwrap(), data_in_trie.get(key).cloned()); + } + assert_eq!(trie.get_trie_nodes_count(), baseline_trie_nodes_count); + + // Now, let's check that when doing the same lookups with the captured partial storage, + // we still get the same counters. + let partial_storage = trie.recorded_storage().unwrap(); + println!( + "Partial storage has {} nodes from {} entries", + partial_storage.nodes.len(), + trie_changes.len() + ); + let trie = Trie::from_recorded_storage(partial_storage, state_root, false); + trie.accounting_cache.borrow_mut().set_enabled(enable_accounting_cache); + for key in &keys_to_test_with { + assert_eq!(trie.get(key).unwrap(), data_in_trie.get(key).cloned()); + } + assert_eq!(trie.get_trie_nodes_count(), baseline_trie_nodes_count); + } + } + + #[test] + fn test_trie_recording_consistency_no_accounting_cache() { + test_trie_recording_consistency(false, false); + } + + #[test] + fn test_trie_recording_consistency_with_accounting_cache() { + test_trie_recording_consistency(true, false); + } + + #[test] + fn test_trie_recording_consistency_no_accounting_cache_with_missing_keys() { + test_trie_recording_consistency(false, true); + } + + #[test] + fn test_trie_recording_consistency_with_accounting_cache_and_missing_keys() { + test_trie_recording_consistency(true, true); + } + + /// Verifies that when operating on a trie, the results are completely consistent + /// regardless of whether we're operating on the real storage (with or without chunk + /// cache), while recording reads, or when operating on recorded partial storage. + /// This test additionally verifies this when flat storage is used. + fn test_trie_recording_consistency_with_flat_storage( + enable_accounting_cache: bool, + use_missing_keys: bool, + ) { + let mut rng = rand::thread_rng(); + for _ in 0..NUM_ITERATIONS_PER_TEST { + let tries = create_tries_complex(1, 2); + + let shard_uid = ShardUId { version: 1, shard_id: 0 }; + let trie_changes = gen_larger_changes(&mut rng, 50); + let trie_changes = simplify_changes(&trie_changes); + if trie_changes.is_empty() { + continue; + } + let state_root = + test_populate_trie(&tries, &Trie::EMPTY_ROOT, shard_uid, trie_changes.clone()); + test_populate_flat_storage( + &tries, + shard_uid, + &CryptoHash::default(), + &CryptoHash::default(), + &trie_changes, + ); + + let data_in_trie = trie_changes + .iter() + .map(|(key, value)| (key.clone(), value.clone().unwrap())) + .collect::>(); + let keys_to_test_with = trie_changes + .iter() + .map(|(key, _)| { + let mut key = key.clone(); + if use_missing_keys { + key.push(100); + } + key + }) + .collect::>(); + + // First, check that the trie is using flat storage, so that counters are all zero. + // Only use get_ref(), because get() will actually dereference values which can + // cause trie reads. + let trie = tries.get_trie_with_block_hash_for_shard( + shard_uid, + state_root, + &CryptoHash::default(), + false, + ); + for key in &keys_to_test_with { + trie.get_ref(&key, crate::KeyLookupMode::FlatStorage).unwrap(); + } + assert_eq!(trie.get_trie_nodes_count(), TrieNodesCount { db_reads: 0, mem_reads: 0 }); + + // Now, let's capture the baseline node counts - this is what will happen + // in production. + let trie = tries.get_trie_with_block_hash_for_shard( + shard_uid, + state_root, + &CryptoHash::default(), + false, + ); + trie.accounting_cache.borrow_mut().set_enabled(enable_accounting_cache); + for key in &keys_to_test_with { + assert_eq!(trie.get(key).unwrap(), data_in_trie.get(key).cloned()); + } + let baseline_trie_nodes_count = trie.get_trie_nodes_count(); + println!("Baseline trie nodes count: {:?}", baseline_trie_nodes_count); + + // Let's do this again, but this time recording reads. We'll make sure + // the counters are exactly the same even when we're recording. + let trie = tries + .get_trie_with_block_hash_for_shard( + shard_uid, + state_root, + &CryptoHash::default(), + false, + ) + .recording_reads(); + trie.accounting_cache.borrow_mut().set_enabled(enable_accounting_cache); + for key in &keys_to_test_with { + assert_eq!(trie.get(key).unwrap(), data_in_trie.get(key).cloned()); + } + assert_eq!(trie.get_trie_nodes_count(), baseline_trie_nodes_count); + + // Now, let's check that when doing the same lookups with the captured partial storage, + // we still get the same counters. + let partial_storage = trie.recorded_storage().unwrap(); + println!( + "Partial storage has {} nodes from {} entries", + partial_storage.nodes.len(), + trie_changes.len() + ); + let trie = Trie::from_recorded_storage(partial_storage, state_root, true); + trie.accounting_cache.borrow_mut().set_enabled(enable_accounting_cache); + for key in &keys_to_test_with { + assert_eq!(trie.get(key).unwrap(), data_in_trie.get(key).cloned()); + } + assert_eq!(trie.get_trie_nodes_count(), baseline_trie_nodes_count); + } + } + + #[test] + fn test_trie_recording_consistency_with_flat_storage_no_accounting_cache() { + test_trie_recording_consistency_with_flat_storage(false, false); + } + + #[test] + fn test_trie_recording_consistency_with_flat_storage_with_accounting_cache() { + test_trie_recording_consistency_with_flat_storage(true, false); + } + + #[test] + fn test_trie_recording_consistency_with_flat_storage_no_accounting_cache_with_missing_keys() { + test_trie_recording_consistency_with_flat_storage(false, true); + } + + #[test] + fn test_trie_recording_consistency_with_flat_storage_with_accounting_cache_and_missing_keys() { + test_trie_recording_consistency_with_flat_storage(true, true); + } +} diff --git a/core/store/src/trie/trie_storage.rs b/core/store/src/trie/trie_storage.rs index ba94c1c5e74..9fbe3df1468 100644 --- a/core/store/src/trie/trie_storage.rs +++ b/core/store/src/trie/trie_storage.rs @@ -9,10 +9,9 @@ use near_o11y::metrics::prometheus::core::{GenericCounter, GenericGauge}; use near_primitives::challenge::PartialState; use near_primitives::hash::CryptoHash; use near_primitives::shard_layout::ShardUId; -use near_primitives::types::{ShardId, TrieCacheMode, TrieNodesCount}; -use std::cell::{Cell, RefCell}; +use near_primitives::types::ShardId; +use std::cell::RefCell; use std::collections::{HashMap, HashSet, VecDeque}; -use std::rc::Rc; use std::sync::{Arc, Mutex}; pub(crate) struct BoundedQueue { @@ -292,42 +291,9 @@ pub trait TrieStorage { None } - fn as_recording_storage(&self) -> Option<&TrieRecordingStorage> { - None - } - fn as_partial_storage(&self) -> Option<&TrieMemoryPartialStorage> { None } - - fn get_trie_nodes_count(&self) -> TrieNodesCount; -} - -/// Records every value read by retrieve_raw_bytes. -/// Used for obtaining state parts (and challenges in the future). -/// TODO (#6316): implement proper nodes counting logic as in TrieCachingStorage -pub struct TrieRecordingStorage { - pub(crate) storage: Rc, - pub(crate) recorded: RefCell>>, -} - -impl TrieStorage for TrieRecordingStorage { - fn retrieve_raw_bytes(&self, hash: &CryptoHash) -> Result, StorageError> { - if let Some(val) = self.recorded.borrow().get(hash).cloned() { - return Ok(val); - } - let val = self.storage.retrieve_raw_bytes(hash)?; - self.recorded.borrow_mut().insert(*hash, Arc::clone(&val)); - Ok(val) - } - - fn as_recording_storage(&self) -> Option<&TrieRecordingStorage> { - Some(self) - } - - fn get_trie_nodes_count(&self) -> TrieNodesCount { - unimplemented!(); - } } /// Storage for validating recorded partial storage. @@ -350,10 +316,6 @@ impl TrieStorage for TrieMemoryPartialStorage { fn as_partial_storage(&self) -> Option<&TrieMemoryPartialStorage> { Some(self) } - - fn get_trie_nodes_count(&self) -> TrieNodesCount { - unimplemented!(); - } } impl TrieMemoryPartialStorage { @@ -381,43 +343,33 @@ impl TrieMemoryPartialStorage { } /// Storage for reading State nodes and values from DB which caches reads. +/// +/// Important: The TrieCachingStorage contains the shard cache, which is +/// different from the accounting cache. The former is a best-effort +/// 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) shard_uid: ShardUId, + pub(crate) is_view: bool, /// Caches ever requested items for the shard `shard_uid`. Used to speed up DB operations, presence of any item is /// not guaranteed. pub(crate) shard_cache: TrieCache, - /// Caches all items requested in the mode `TrieCacheMode::CachingChunk`. It is created in - /// `apply_transactions_with_optional_storage_proof` by calling `get_trie_for_shard`. Before we start to apply - /// txs and receipts in the chunk, it must be empty, and all items placed here must remain until applying - /// txs/receipts ends. Then cache is removed automatically in `apply_transactions_with_optional_storage_proof` when - /// `TrieCachingStorage` is removed. - /// Note that for both caches key is the hash of value, so for the fixed key the value is unique. - pub(crate) chunk_cache: RefCell>>, - pub(crate) cache_mode: Cell, /// The entry point for the runtime to submit prefetch requests. pub(crate) prefetch_api: Option, - /// Counts potentially expensive trie node reads which are served from disk in the worst case. Here we count reads - /// from DB or shard cache. - pub(crate) db_read_nodes: Cell, - /// Counts trie nodes retrieved from the chunk cache. - pub(crate) mem_read_nodes: Cell, // Counters tracking operations happening inside the shard cache. // Stored here to avoid overhead of looking them up on hot paths. metrics: TrieCacheInnerMetrics, } struct TrieCacheInnerMetrics { - chunk_cache_hits: GenericCounter, - chunk_cache_misses: GenericCounter, shard_cache_hits: GenericCounter, shard_cache_misses: GenericCounter, shard_cache_too_large: GenericCounter, shard_cache_size: GenericGauge, - chunk_cache_size: GenericGauge, shard_cache_current_total_size: GenericGauge, prefetch_hits: GenericCounter, prefetch_pending: GenericCounter, @@ -441,14 +393,11 @@ impl TrieCachingStorage { let metrics_labels: [&str; 2] = [&shard_id, if is_view { "1" } else { "0" }]; let metrics = TrieCacheInnerMetrics { - chunk_cache_hits: metrics::CHUNK_CACHE_HITS.with_label_values(&metrics_labels), - chunk_cache_misses: metrics::CHUNK_CACHE_MISSES.with_label_values(&metrics_labels), shard_cache_hits: metrics::SHARD_CACHE_HITS.with_label_values(&metrics_labels), shard_cache_misses: metrics::SHARD_CACHE_MISSES.with_label_values(&metrics_labels), shard_cache_too_large: metrics::SHARD_CACHE_TOO_LARGE .with_label_values(&metrics_labels), shard_cache_size: metrics::SHARD_CACHE_SIZE.with_label_values(&metrics_labels), - chunk_cache_size: metrics::CHUNK_CACHE_SIZE.with_label_values(&metrics_labels), shard_cache_current_total_size: metrics::SHARD_CACHE_CURRENT_TOTAL_SIZE .with_label_values(&metrics_labels), prefetch_hits: metrics::PREFETCH_HITS.with_label_values(&metrics_labels[..1]), @@ -460,17 +409,7 @@ impl TrieCachingStorage { prefetch_retry: metrics::PREFETCH_RETRY.with_label_values(&metrics_labels[..1]), prefetch_conflict: metrics::PREFETCH_CONFLICT.with_label_values(&metrics_labels[..1]), }; - TrieCachingStorage { - store, - shard_uid, - shard_cache, - cache_mode: Cell::new(TrieCacheMode::CachingShard), - prefetch_api, - chunk_cache: RefCell::new(Default::default()), - db_read_nodes: Cell::new(0), - mem_read_nodes: Cell::new(0), - metrics, - } + 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] { @@ -479,33 +418,10 @@ impl TrieCachingStorage { key[8..].copy_from_slice(hash.as_ref()); key } - - fn inc_db_read_nodes(&self) { - self.db_read_nodes.set(self.db_read_nodes.get() + 1); - } - - fn inc_mem_read_nodes(&self) { - self.mem_read_nodes.set(self.mem_read_nodes.get() + 1); - } - - /// Set cache mode. - pub fn set_mode(&self, state: TrieCacheMode) { - self.cache_mode.set(state); - } } impl TrieStorage for TrieCachingStorage { fn retrieve_raw_bytes(&self, hash: &CryptoHash) -> Result, StorageError> { - self.metrics.chunk_cache_size.set(self.chunk_cache.borrow().len() as i64); - // Try to get value from chunk cache containing nodes with cheaper access. We can do it for any `TrieCacheMode`, - // because we charge for reading nodes only when `CachingChunk` mode is enabled anyway. - if let Some(val) = self.chunk_cache.borrow_mut().get(hash) { - self.metrics.chunk_cache_hits.inc(); - self.inc_mem_read_nodes(); - return Ok(val.clone()); - } - self.metrics.chunk_cache_misses.inc(); - // Try to get value from shard cache containing most recently touched nodes. let mut guard = self.shard_cache.lock(); self.metrics.shard_cache_size.set(guard.len() as i64); @@ -579,9 +495,9 @@ impl TrieStorage for TrieCachingStorage { } // Insert value to shard cache, if its size is small enough. - // It is fine to have a size limit for shard cache and **not** have a limit for chunk cache, because key + // It is fine to have a size limit for shard cache and **not** have a limit for accounting cache, because key // is always a value hash, so for each key there could be only one value, and it is impossible to have - // **different** values for the given key in shard and chunk caches. + // **different** values for the given key in shard and accounting caches. if val.len() < TrieConfig::max_cached_value_size() { let mut guard = self.shard_cache.lock(); guard.put(*hash, val.clone()); @@ -599,31 +515,12 @@ impl TrieStorage for TrieCachingStorage { } }; - // Because node is not present in chunk cache, increment the nodes counter and optionally insert it into the - // chunk cache. - // Note that we don't have a size limit for values in the chunk cache. There are two reasons: - // - for nodes, value size is an implementation detail. If we change internal representation of a node (e.g. - // change `memory_usage` field from `RawTrieNodeWithSize`), this would have to be a protocol upgrade. - // - total size of all values is limited by the runtime fees. More thoroughly: - // - - number of nodes is limited by receipt gas limit / touching trie node fee ~= 500 Tgas / 16 Ggas = 31_250; - // - - size of trie keys and values is limited by receipt gas limit / lowest per byte fee - // (`storage_read_value_byte`) ~= (500 * 10**12 / 5611005) / 2**20 ~= 85 MB. - // All values are given as of 16/03/2022. We may consider more precise limit for the chunk cache as well. - self.inc_db_read_nodes(); - if let TrieCacheMode::CachingChunk = self.cache_mode.get() { - self.chunk_cache.borrow_mut().insert(*hash, val.clone()); - }; - Ok(val) } fn as_caching_storage(&self) -> Option<&TrieCachingStorage> { Some(self) } - - fn get_trie_nodes_count(&self) -> TrieNodesCount { - TrieNodesCount { db_reads: self.db_read_nodes.get(), mem_reads: self.mem_read_nodes.get() } - } } fn read_node_from_db( @@ -659,7 +556,6 @@ pub struct TrieDBStorage { } impl TrieDBStorage { - #[allow(unused)] pub fn new(store: Store, shard_uid: ShardUId) -> Self { Self { store, shard_uid } } @@ -669,10 +565,6 @@ impl TrieStorage for TrieDBStorage { fn retrieve_raw_bytes(&self, hash: &CryptoHash) -> Result, StorageError> { read_node_from_db(&self.store, self.shard_uid, hash) } - - fn get_trie_nodes_count(&self) -> TrieNodesCount { - unimplemented!(); - } } #[cfg(test)] diff --git a/core/store/src/trie/trie_tests.rs b/core/store/src/trie/trie_tests.rs index 8ae1aef7f91..3be3f3ba237 100644 --- a/core/store/src/trie/trie_tests.rs +++ b/core/store/src/trie/trie_tests.rs @@ -35,16 +35,18 @@ impl IncompletePartialStorage { impl TrieStorage for IncompletePartialStorage { fn retrieve_raw_bytes(&self, hash: &CryptoHash) -> Result, StorageError> { - let result = self.recorded_storage.get(hash).cloned().ok_or(StorageError::MissingTrieValue); + let result = self + .recorded_storage + .get(hash) + .cloned() + .expect("Recorded storage is missing the given hash"); - if result.is_ok() { - self.visited_nodes.borrow_mut().insert(*hash); - } + self.visited_nodes.borrow_mut().insert(*hash); if self.visited_nodes.borrow().len() > self.node_count_to_fail_after { Err(StorageError::MissingTrieValue) } else { - result + Ok(result) } } @@ -52,10 +54,6 @@ impl TrieStorage for IncompletePartialStorage { // Make sure it's not called - it pretends to be PartialStorage but is not unimplemented!() } - - fn get_trie_nodes_count(&self) -> TrieNodesCount { - unimplemented!(); - } } fn setup_storage(trie: Trie, test: &mut F) -> (PartialStorage, Trie, Out) @@ -75,14 +73,10 @@ where { let (storage, trie, expected) = setup_storage(trie, &mut test); let size = storage.nodes.len(); - print!("Test touches {} nodes, expected result {:?}...", size, expected); + println!("Test touches {} nodes, expected result {:?}...", size, expected); for i in 0..(size + 1) { let storage = IncompletePartialStorage::new(storage.clone(), i); - let new_trie = Trie { - storage: Rc::new(storage), - root: *trie.get_root(), - flat_storage_chunk_view: None, - }; + let new_trie = Trie::new(Rc::new(storage), *trie.get_root(), None); let expected_result = if i < size { Err(&StorageError::MissingTrieValue) } else { Ok(&expected) }; assert_eq!(test(new_trie).map(|v| v.1).as_ref(), expected_result); @@ -199,12 +193,12 @@ mod nodes_counter_tests { mod trie_storage_tests { use super::*; use crate::test_utils::{create_test_store, create_tries}; + use crate::trie::accounting_cache::TrieAccountingCache; use crate::trie::trie_storage::{TrieCache, TrieCachingStorage, TrieDBStorage}; use crate::trie::TrieRefcountChange; use crate::{Store, TrieChanges, TrieConfig}; use assert_matches::assert_matches; use near_primitives::hash::hash; - use near_primitives::types::TrieCacheMode; fn create_store_with_values(values: &[Vec], shard_uid: ShardUId) -> Store { let tries = create_tries(); @@ -247,14 +241,16 @@ mod trie_storage_tests { let trie_cache = TrieCache::new(&TrieConfig::default(), shard_uid, false); let trie_caching_storage = TrieCachingStorage::new(store, trie_cache.clone(), shard_uid, false, None); + let mut accounting_cache = TrieAccountingCache::new(None); let key = hash(&value); assert_eq!(trie_cache.get(&key), None); for _ in 0..2 { - let count_before = trie_caching_storage.get_trie_nodes_count(); - let result = trie_caching_storage.retrieve_raw_bytes(&key); + let count_before = accounting_cache.get_trie_nodes_count(); + let result = + accounting_cache.retrieve_raw_bytes_with_accounting(&key, &trie_caching_storage); let count_delta = - trie_caching_storage.get_trie_nodes_count().checked_sub(&count_before).unwrap(); + accounting_cache.get_trie_nodes_count().checked_sub(&count_before).unwrap(); assert_eq!(result.unwrap().as_ref(), value); assert_eq!(count_delta.db_reads, 1); assert_eq!(count_delta.mem_reads, 0); @@ -281,7 +277,7 @@ mod trie_storage_tests { assert_matches!(result, Err(StorageError::MissingTrieValue)); } - /// Check that large values does not fall into shard cache, but fall into chunk cache. + /// Check that large values does not fall into shard cache, but fall into accounting cache. #[test] fn test_large_value() { let value = vec![1u8].repeat(TrieConfig::max_cached_value_size() + 1); @@ -291,15 +287,17 @@ mod trie_storage_tests { let trie_cache = TrieCache::new(&TrieConfig::default(), shard_uid, false); let trie_caching_storage = TrieCachingStorage::new(store, trie_cache.clone(), shard_uid, false, None); + let mut accounting_cache = TrieAccountingCache::new(None); let key = hash(&value); - trie_caching_storage.set_mode(TrieCacheMode::CachingChunk); - let _ = trie_caching_storage.retrieve_raw_bytes(&key); + accounting_cache.set_enabled(true); + let _ = accounting_cache.retrieve_raw_bytes_with_accounting(&key, &trie_caching_storage); - let count_before = trie_caching_storage.get_trie_nodes_count(); - let result = trie_caching_storage.retrieve_raw_bytes(&key); + let count_before: TrieNodesCount = accounting_cache.get_trie_nodes_count(); + let result = + accounting_cache.retrieve_raw_bytes_with_accounting(&key, &trie_caching_storage); let count_delta = - trie_caching_storage.get_trie_nodes_count().checked_sub(&count_before).unwrap(); + accounting_cache.get_trie_nodes_count().checked_sub(&count_before).unwrap(); assert_eq!(trie_cache.get(&key), None); assert_eq!(result.unwrap().as_ref(), value); assert_eq!(count_delta.db_reads, 0); @@ -315,6 +313,7 @@ mod trie_storage_tests { let trie_cache = TrieCache::new(&TrieConfig::default(), shard_uid, false); let trie_caching_storage = TrieCachingStorage::new(store, trie_cache.clone(), shard_uid, false, None); + let mut accounting_cache = TrieAccountingCache::new(None); let value = &values[0]; let key = hash(&value); @@ -327,39 +326,43 @@ mod trie_storage_tests { // Move to CachingChunk mode. Retrieval should increment the counter, because it is the first time we accessed // item while caching chunk. - trie_caching_storage.set_mode(TrieCacheMode::CachingChunk); - let count_before = trie_caching_storage.get_trie_nodes_count(); - let result = trie_caching_storage.retrieve_raw_bytes(&key); + accounting_cache.set_enabled(true); + let count_before = accounting_cache.get_trie_nodes_count(); + let result = + accounting_cache.retrieve_raw_bytes_with_accounting(&key, &trie_caching_storage); let count_delta = - trie_caching_storage.get_trie_nodes_count().checked_sub(&count_before).unwrap(); + accounting_cache.get_trie_nodes_count().checked_sub(&count_before).unwrap(); assert_eq!(result.unwrap().as_ref(), value); assert_eq!(count_delta.db_reads, 1); assert_eq!(count_delta.mem_reads, 0); - // After previous retrieval, item must be copied to chunk cache. Retrieval shouldn't increment the counter. - let count_before = trie_caching_storage.get_trie_nodes_count(); - let result = trie_caching_storage.retrieve_raw_bytes(&key); + // After previous retrieval, item must be copied to accounting cache. Retrieval shouldn't increment the counter. + let count_before = accounting_cache.get_trie_nodes_count(); + let result = + accounting_cache.retrieve_raw_bytes_with_accounting(&key, &trie_caching_storage); let count_delta = - trie_caching_storage.get_trie_nodes_count().checked_sub(&count_before).unwrap(); + accounting_cache.get_trie_nodes_count().checked_sub(&count_before).unwrap(); assert_eq!(result.unwrap().as_ref(), value); assert_eq!(count_delta.db_reads, 0); assert_eq!(count_delta.mem_reads, 1); - // Even if we switch to caching shard, retrieval shouldn't increment the counter. Chunk cache only grows and is + // Even if we switch to caching shard, retrieval shouldn't increment the counter. Accounting cache only grows and is // dropped only when trie caching storage is dropped. - trie_caching_storage.set_mode(TrieCacheMode::CachingShard); - let count_before = trie_caching_storage.get_trie_nodes_count(); - let result = trie_caching_storage.retrieve_raw_bytes(&key); + accounting_cache.set_enabled(true); + let count_before = accounting_cache.get_trie_nodes_count(); + let result = + accounting_cache.retrieve_raw_bytes_with_accounting(&key, &trie_caching_storage); let count_delta = - trie_caching_storage.get_trie_nodes_count().checked_sub(&count_before).unwrap(); + accounting_cache.get_trie_nodes_count().checked_sub(&count_before).unwrap(); assert_eq!(result.unwrap().as_ref(), value); assert_eq!(count_delta.db_reads, 0); assert_eq!(count_delta.mem_reads, 1); } - /// Check that if an item present in chunk cache gets evicted from the shard cache, it stays in the chunk cache. + /// Check that if an item present in accounting cache gets evicted from the shard cache, + /// it stays in the accounting cache. #[test] - fn test_chunk_cache_presence() { + fn test_accounting_cache_presence() { let shard_cache_size = 5; let values: Vec> = (0..shard_cache_size as u8 + 1).map(|i| vec![i]).collect(); let shard_uid = ShardUId::single_shard(); @@ -369,26 +372,30 @@ mod trie_storage_tests { let trie_cache = TrieCache::new(&trie_config, shard_uid, false); let trie_caching_storage = TrieCachingStorage::new(store, trie_cache.clone(), shard_uid, false, None); + let mut accounting_cache = TrieAccountingCache::new(None); let value = &values[0]; let key = hash(&value); - trie_caching_storage.set_mode(TrieCacheMode::CachingChunk); - let result = trie_caching_storage.retrieve_raw_bytes(&key); + accounting_cache.set_enabled(true); + let result = + accounting_cache.retrieve_raw_bytes_with_accounting(&key, &trie_caching_storage); assert_eq!(result.unwrap().as_ref(), value); - trie_caching_storage.set_mode(TrieCacheMode::CachingShard); + accounting_cache.set_enabled(true); for value in values[1..].iter() { - let result = trie_caching_storage.retrieve_raw_bytes(&hash(value)); + let result = accounting_cache + .retrieve_raw_bytes_with_accounting(&hash(value), &trie_caching_storage); assert_eq!(result.unwrap().as_ref(), value); } // Check that the first element gets evicted, but the counter is not incremented. assert_eq!(trie_cache.get(&key), None); - let count_before = trie_caching_storage.get_trie_nodes_count(); - let result = trie_caching_storage.retrieve_raw_bytes(&key); + let count_before = accounting_cache.get_trie_nodes_count(); + let result = + accounting_cache.retrieve_raw_bytes_with_accounting(&key, &trie_caching_storage); let count_delta = - trie_caching_storage.get_trie_nodes_count().checked_sub(&count_before).unwrap(); + accounting_cache.get_trie_nodes_count().checked_sub(&count_before).unwrap(); assert_eq!(result.unwrap().as_ref(), value); assert_eq!(count_delta.db_reads, 0); assert_eq!(count_delta.mem_reads, 1); diff --git a/core/store/src/trie/update.rs b/core/store/src/trie/update.rs index ada4b76b28c..28de5a37839 100644 --- a/core/store/src/trie/update.rs +++ b/core/store/src/trie/update.rs @@ -46,7 +46,7 @@ impl<'a> TrieUpdateValuePtr<'a> { match self { TrieUpdateValuePtr::MemoryRef(value) => Ok(value.to_vec()), TrieUpdateValuePtr::HashAndSize(trie, _, hash) => { - trie.storage.retrieve_raw_bytes(hash).map(|bytes| bytes.to_vec()) + trie.internal_retrieve_trie_node(hash, true).map(|bytes| bytes.to_vec()) } } } @@ -157,9 +157,7 @@ impl TrieUpdate { } pub fn set_trie_cache_mode(&self, state: TrieCacheMode) { - if let Some(storage) = self.trie.storage.as_caching_storage() { - storage.set_mode(state); - } + self.trie.accounting_cache.borrow_mut().set_enabled(state == TrieCacheMode::CachingChunk); } } diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 5e92f3c0b51..39ee2ff17ef 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -12,6 +12,7 @@ - [Transaction Routing](./architecture/how/tx_routing.md) - [Transactions And Receipts](./architecture/how/tx_receipts.md) - [Cross shard transactions - deep dive](./architecture/how/cross-shard.md) + - [Gas](./architecture/how/gas.md) - [Meta transactions](./architecture/how/meta-tx.md) - [Serialization: Borsh, Json, ProtoBuf](./architecture/how/serialization.md) - [Proofs](./architecture/how/proofs.md) diff --git a/docs/architecture/how/gas.md b/docs/architecture/how/gas.md new file mode 100644 index 00000000000..75e9011b2fb --- /dev/null +++ b/docs/architecture/how/gas.md @@ -0,0 +1,146 @@ +# Gas + +This page describes the technical details around gas during the lifecycle of a +_transaction_(*) while giving an intuition for why things are the way they are +from a technical perspective. For a more practical, user-oriented angle, please +refer to the [gas section in the official protocol +documentation](https://docs.near.org/concepts/basics/transactions/gas). + +(*) _For this page, a transaction shall refer to the set of all recursively +generated receipts by a `SignedTransaction`. When referring to only the original +transaction object, we write `SignedTransaction`._ + +The topic is split into several sections. + +1. [Buying Gas](#buying-gas-for-a-transaction): How are NEAR tokens converted to gas? +2. [Gas Price](#gas-price): + - [Block-Level Gas Price](#block-level-gas-price): How the block-level gas price is determined. + - [Pessimistic Gas Price](#pessimistic-gas-price): How worst-case gas pricing is estimated. + - [Effective Gas Purchase Cost](#effective-gas-purchase-cost): The cost paid for a receipt. +3. [Tracking Gas](#tracking-gas-in-receipts): How the system keeps track of purchased gas during the transaction execution. + + +## Buying Gas for a Transaction + +A signer pays all the gas required for a transaction upfront. However, there is +no explicit act of buying gas. Instead, the fee is subtracted directly in NEAR +tokens from the balance of the signer's account. If we ignore all the details +explained further down, the fee is calculated as `gas amount` * `gas price`. + +The `gas amount` is not a field of `SignedTransaction`, nor is it something the +signer can choose. It is only a virtual field that is computed on-chain following +the protocol's rules. + +The `gas price` is a variable that may change during the execution of the +transaction. The way it is implemented today, a single transaction can be +charged a different gas price for different receipts. + +Already we can see a fundamental problem: Gas is bought once at the beginning +but the gas price may change during execution. To solve this incompatibility, +the protocol calculates a pessimistic gas price for the initial purchase. Later +on, the delta between real and pessimistic gas prices is refunded at the end of +every receipt execution. + +An alternative implementation would instead charge the gas at every receipt, +instead of once at the start. However, remember that the execution may happen on +a different shard than the signer account. Therefore we cannot access the +signer's balance while executing. + +## Gas Price + +Gas pricing is a surprisingly deep and complicated topic. Usually, we only think +about the value of the `gas_price` field in the block header. However, to +understand the internals, this is not enough. + +### Block-Level Gas Price + +`gas_price` is a field in the block header. It determines how much it costs to +burn gas at the given block height. Confusingly, this is not the same price at +which gas is purchased. +(See [Effective Gas Purchase Price](#effective-gas-price).) + +The price is measured in NEAR tokens per unit of gas. It dynamically changes in +the range between 0.1 NEAR per Pgas and 2 NEAR per Pgas, based on demand. (1 +Pgas = 1000 Tgas corresponds to a full chunk.) + +The block producer has to set this field following the exact formula as defined +by the protocol. Otherwise, the produced block is invalid. + +Intuitively, the formula checks how much gas was used compared to the total +capacity. If it exceeds 50%, the gas price increases exponentially within the +limits. When the demand is below 50%, it decreases exponentially. In practice, +it stays at the bottom most of the time. + +Note that all shards share the same gas price. Hence, if one out of four shards +is at 100% capacity, this will not cause the price to increase. The 50% capacity +is calculated as an average across all shards. + +Going slightly off-topic, it should also be mentioned that chunk capacity is not +constant. Chunk producers can change it by 0.1% per chunk. The nearcore client +does not currently make use of this option, so it really is a nitpick only +relevant in theory. However, any client implementation such as nearcore must +compute the total capacity as the sum of gas limits stored in the chunk headers +to be compliant. Using a hard-coded `1000 Tgas * num_shards` would lead to +incorrect block header validation. + + +### Pessimistic Gas Price + +The pessimistic gas price calculation uses the fact that any transaction can +only have a limited depth in the generated receipt DAG. For most actions, the +depth is a constant 1 or 2. For function call actions, it is limited to a +hand-wavy `attached_gas` / `min gas per function call`. (Note: `attached_gas` is +a property of a single action and is only a part of the total gas costs of a +receipt.) + +Once the maximum depth is known, the protocol assumes that the gas price will +not change more than 3% per receipt. This is not a guarantee since receipts can +be delayed for virtually unlimited blocks. + +The final formula for the pessimistic gas price is the following. + +```txt +pessimistic(current_gas_price, max_depth) = current_gas_price × 1.03^max_depth +``` + +This still is not the price at which gas is purchased. But we are very close. + + +### Effective Gas Purchase Cost + +When a transaction is converted to its root action receipt, the gas costs are +calculated in two parts. + +Part one contains all the gas which is burnt immediately. Namely, the `send` +costs for a receipt and all the actions it includes. This is charged at the +current block-level gas price. + +Part two is everything else, from execution costs of actions that are statically +known such as `CreateAccount` all the way to `attached_gas` for function calls. +All of this is purchased at the same pessimistic gas price, even if some actions +inside might have a lower maximum call depth than others. + +The deducted tokens are the sum of these two parts. If the account has +insufficient balance to pay for this pessimistic pricing, it will fail with a +`NotEnoughBalance` error, with the required balance included in the error +message. + +## Tracking Gas in Receipts + +The previous section explained how gas is bought and what determines its price. +This section details the tracking that enables correct refunds. + +First, when a `SignedTransaction` is converted to a receipt, the pessimistic gas +price is written to the receipt's `gas_price` field. + +Later on, when the receipt has been executed, a gas refund is created at the +value of `receipt.gas_burnt` * (`block_header.gas_price` - `receipt.gas_price`). + +Some gas goes attaches to outgoing receipts. We commonly refer to this as used +gas that was not burnt, yet. The refund excludes this gas. But it includes the +receipt send cost. + +Finally, unspent gas is refunded at the full `receipt.gas_price`. This refund is +merged with the refund for burnt gas of the same receipt outcome to reduce the +number of spawned system receipts. But it makes it a bit harder to interpret +refunds when backtracking for how much gas a specific refund receipt covers. diff --git a/docs/architecture/how/sync.md b/docs/architecture/how/sync.md index f0a585d67ae..a14d6aba926 100644 --- a/docs/architecture/how/sync.md +++ b/docs/architecture/how/sync.md @@ -63,11 +63,11 @@ each epoch. For the ‘current’ epoch, we still need to get all the headers. ### Step 2: State sync [normal node] -After header sync - if you notice that you’re too far behind (controlled by -`block_fetch_horizon` config option) **AND** that the chain head is in a different -epoch than your local head - the node will try to do the ‘state sync’. +After header sync - if you notice that you’re too far behind, i.e. the chain +head is at least two epochs ahead of your local head - the node will try to do +the ‘state sync’. -The idea of the state sync - is rather than trying to process all the blocks - +The idea of the state sync is - rather than trying to process all the blocks - try to ‘jump’ ahead by downloading the freshest state instead - and continue processing blocks from that place in the chain. As a side effect, it is going to create a ‘gap’ in the chunks/state on this node (which is fine - as the data @@ -81,6 +81,8 @@ history and cannot have any gaps. In this case, we can skip processing transactions that are in the blocks 124 - 128, and start from 129 (after sync state finishes) +See [how-to](../../misc/state_sync_from_external_storage.md) to learn how to configure your node to state sync. + ### Step 3: Block sync (a.k.a Body sync) [archival node, normal node] (“downloading blocks”) The final step is to start requesting and processing blocks as soon as possible, diff --git a/docs/architecture/storage/trie.md b/docs/architecture/storage/trie.md index 83667fbcf0c..0b5c13e7636 100644 --- a/docs/architecture/storage/trie.md +++ b/docs/architecture/storage/trie.md @@ -61,12 +61,9 @@ when `ShardTries::apply_insertions` is called, which puts new values to Stores all `Trie` nodes and allows to get serialized nodes by `TrieKey` hash using the `retrieve_raw_bytes` method. -There are three implementations of `TrieStorage`: +There are two major implementations of `TrieStorage`: * `TrieCachingStorage` - caches all big values ever read by `retrieve_raw_bytes`. -* `TrieRecordingStorage` - records all key-value pairs ever read by - `retrieve_raw_bytes`. Used for obtaining state parts (and challenges in the - future). * `TrieMemoryPartialStorage` - used for validating recorded partial storage. Note that these storages use database keys, which are retrieved using hashes of diff --git a/docs/misc/state_sync_dump.md b/docs/misc/state_sync_dump.md index 7af09e0c2a5..3e5ee4fa2a0 100644 --- a/docs/misc/state_sync_dump.md +++ b/docs/misc/state_sync_dump.md @@ -1,71 +1,77 @@ -# Experimental: Dump of state to External Storage +# Experimental: Dump State to External Storage ## Purpose -Current implementation of state sync (see -https://github.com/near/nearcore/blob/master/docs/architecture/how/sync.md for -details) doesn't allow the nodes to reliably perform state sync for testnet or -mainnet. +[State Sync](../architecture/how/sync.md#step-2-state-sync-normal-node) is being +reworked. -That's why a new solution for state sync is being designed. -The experimental code is likely going to be a part of solution to greatly -improve both reliability and speed of state sync. +A new version is available for experimental use. This version gets state parts +from external storage. The following kinds of external storage are supported: +* Local filesystem +* Google Cloud Storage +* Amazon S3 -The new solution will probably involve making the state available on external -storage, making downloading the state both low latency and reliable process, -thanks to the robust infrastructure of external storage such as S3. +A new version of decentralized state sync is work in progress. ## How-to -[#8661](https://github.com/near/nearcore/pull/8661) adds an experimental option -to dump state of every epoch to external storage. +neard release `1.36.0-rc.1` adds an experimental option to sync state from +external storage. -### S3 -To enable S3 as your external storage, add this to your `config.json` file: +See [how-to](state_sync_from_external_storage.md) how to configure your node to +State Sync from External Storage. + +In case you would like to manage your own dumps of State, keep reading. + +### Google Cloud Storage +To enable Google Cloud Storage as your external storage, add this to your +`config.json` file: ```json "state_sync": { "dump": { "location": { - "S3": { - "bucket": "my-aws-bucket", - "region": "my-aws-region" + "GCS": { + "bucket": "my-gcs-bucket", } - } + } } } ``` -And run your node with environment variables `AWS_ACCESS_KEY_ID` and -`AWS_SECRET_ACCESS_KEY`: +And run your node with an environment variable `SERVICE_ACCOUNT` or +`GOOGLE_APPLICATION_CREDENTIALS` pointing to the credentials json file ```shell -AWS_ACCESS_KEY_ID="MY_ACCESS_KEY" AWS_SECRET_ACCESS_KEY="MY_AWS_SECRET_ACCESS_KEY" ./neard run +SERVICE_ACCOUNT=/path/to/file ./neard run ``` -### Google Cloud Storage -To enable Google Cloud Storage as your external storage, add this to your `config.json` file: +### Amazon S3 +To enable Amazon S3 as your external storage, add this to your `config.json` +file: ```json "state_sync": { "dump": { "location": { - "GCS": { - "bucket": "my-gcs-bucket", + "S3": { + "bucket": "my-aws-bucket", + "region": "my-aws-region" } - } + } } } ``` -And run your node with an environment variable `SERVICE_ACCOUNT` or `GOOGLE_APPLICATION_CREDENTIALS` -pointing to the credentials json file +And run your node with environment variables `AWS_ACCESS_KEY_ID` and +`AWS_SECRET_ACCESS_KEY`: ```shell -SERVICE_ACCOUNT=/path/to/file ./neard run +AWS_ACCESS_KEY_ID="MY_ACCESS_KEY" AWS_SECRET_ACCESS_KEY="MY_AWS_SECRET_ACCESS_KEY" ./neard run ``` ## Dump to a local filesystem -Add this to your `config.json` file to dump state of every epoch to local filesystem: +Add this to your `config.json` file to dump state of every epoch to local +filesystem: ```json "state_sync": { @@ -79,42 +85,8 @@ Add this to your `config.json` file to dump state of every epoch to local filesy } ``` -In this case you don't need any extra environment variables. Simply run your node: +In this case you don't need any extra environment variables. Simply run your +node: ```shell ./neard run ``` - -## Implementation Details - -The experimental option spawns a thread for each of the shards tracked by a node. -Each of the threads acts independently. Each thread determines the last -complete epoch, and starts the process of dumping the state. - -To dump the state a thread does the following: -* Get the size of the trie to determine the number of state parts -* Obtain each state part -* Upload each state part to S3 - -State parts are uploaded as individual objects. Location of those objects is -computed as follows: -``` -"chain_id={chain_id}/epoch_height={epoch_height}/shard_id={shard_id}/state_part_{part_id:06}_of_{num_parts:06}", -``` -for example `chain_id=testnet/epoch_height=1790/shard_id=2/state_part_032642_of_065402` - -Currently, using multiple nodes for dumping state doesn't make the process go -any faster. The nodes will simpler duplicate the work overwriting files created -by each other. - -Future improvement can be to make the nodes cooperate. To avoid introducing a -complicated consensus process, we can suggest the following simple process: -* Get a list of state parts already dumped for an epoch -* Pick 100 random state parts that are not yet random -* Obtain and upload that 100 state parts -* Repeat until all state parts are complete - -The process of dumping state parts is managed as a state machine with 2 -possible states. The state is stored in the `BlockMisc` column with row key -`STATE_SYNC_DUMP:X` for shard X. Note that epoch id is not included in the row -key, because epoch id is not needed for managing the state machine, because only -one epoch per shard can be dumped at a time. diff --git a/docs/misc/state_sync_from_external_storage.md b/docs/misc/state_sync_from_external_storage.md new file mode 100644 index 00000000000..aafd6eaa8f1 --- /dev/null +++ b/docs/misc/state_sync_from_external_storage.md @@ -0,0 +1,128 @@ +# Experimental: State Sync from External Storage + +## Purpose + +[State Sync](../architecture/how/sync.md#step-2-state-sync-normal-node) is being +reworked. + +A new version is available for experimental use. This version gets state parts +from external storage. The following kinds of external storage are supported: +* Local filesystem +* Google Cloud Storage +* Amazon S3 + +A new version of decentralized state sync is work in progress. + +## How-to + +neard release `1.36.0-rc.1` adds an experimental option to sync state from +external storage. + +The reference `config.json` file by default enables state sync from external +storage and configures it to get state parts from a location managed by Pagoda. + +Note: to obtain the reference configuration file, download it by running the +following command: +```shell +neard init --chain-id --download-config --download-genesis +``` + +To create your own State dumps to external storage, see the corresponding [how-to](state_sync_dump.md). + +### Google Cloud Storage + +To enable Google Cloud Storage as your external storage, add the following to +your `config.json` file: + +```json +"state_sync_enabled": true, +"state_sync": { + "sync": { + "ExternalStorage": { + "location": { + "GCS": { + "bucket": "my-gcs-bucket", + } + }, + "num_concurrent_requests": 4, + "num_concurrent_requests_during_catchup": 4, + } + } +}, +"consensus": { + "state_sync_timeout": { + "secs": 30, + "nanos": 0 + } +} +``` + +Then run the `neard` binary and it will access GCS anonymously: +```shell +./neard run +``` + +#### Extra Options + +The options suggested above will most likely work fine. + +* `num_concurrent_requests` determines the number of state parts across all +shards that can be downloaded in parallel during state sync. +* `num_concurrent_requests_during_catchup` determines the number of state parts +across all shards that can be downloaded in parallel during catchup. Generally, +this number should not be higher than `num_concurrent_requests`. Keep it +reasonably low to allow the node to process chunks of other shards. +* `consensus.state_sync_timeout` determines the max duration of an attempt to download a +state part. Setting it too low may cause too many unsuccessful attempts. + +### Amazon S3 + +To enable Amazon S3 as your external storage, add the following to your +`config.json` file. +You may add the other mentioned options too. + +```json +"state_sync_enabled": true, +"state_sync": { + "sync": { + "ExternalStorage": { + "location": { + "S3": { + "bucket": "my-aws-bucket", + "region": "my-aws-region" + } + } + } + } +}, +``` + +Then run the `neard` binary and it will access Amazon S3 anonymously: +```shell +./neard run +``` + +## Sync from a local filesystem + +To enable, add the following to your `config.json` file. +You may add the other mentioned options too. + +```json +"state_sync_enabled": true, +"state_sync": { + "sync": { + "ExternalStorage": { + "location": { + "Filesystem": { + "root_dir": "/tmp/state-parts" + } + } + } + } +} +``` + +Then run the `neard` binary: +```shell +./neard run +``` diff --git a/docs/misc/state_sync_from_s3.md b/docs/misc/state_sync_from_s3.md deleted file mode 100644 index 3d54eff79b6..00000000000 --- a/docs/misc/state_sync_from_s3.md +++ /dev/null @@ -1,112 +0,0 @@ -# Experimental: Sync state from External Storage - -## Purpose - -Current implementation of state sync (see -https://github.com/near/nearcore/blob/master/docs/architecture/how/sync.md for -details) doesn't allow the nodes to reliably perform state sync for testnet or -mainnet. - -That's why a new solution for state sync is being designed. -This is a short-term solution that is needed to let nodes sync and let chunk -only producers to switch tracked shards. -The experimental code is will not be kept for long and will be replaced with a -decentralized solution. - -## How-to - -[#8789](https://github.com/near/nearcore/pull/8789) adds an experimental option -to sync state from external storage. - -### S3 - -To enable S3 as your external storage, add this to your `config.json` file: - -```json -"state_sync_enabled": true, -"state_sync": { - "sync": { - "ExternalStorage": { - "location": { - "S3": { - "bucket": "my-aws-bucket", - "region": "my-aws-region" - } - } - } - } -} -``` - -Then run the `neard` binary and it will access S3 anonymously: -```shell -./neard run -``` - -### Google Cloud Storage - -To enable Google Cloud Storage as your external storage, add this to your `config.json` file: - -```json -"state_sync_enabled": true, -"state_sync": { - "sync": { - "ExternalStorage": { - "location": { - "GCS": { - "bucket": "my-gcs-bucket", - } - } - } - } -} -``` - -Then run the `neard` binary and it will access GCS anonymously: -```shell -./neard run -``` - -## Sync from a local filesystem - -To enable, add this to your `config.json` file: - -```json -"state_sync_enabled": true, -"state_sync": { - "sync": { - "ExternalStorage": { - "location": { - "Filesystem": { - "root_dir": "/tmp/state-parts" - } - } - } - } -} -``` - -Then run the `neard` binary: -```shell -./neard run -``` - -## Implementation Details - -The experimental option replaces how a node fetches state parts. -The legacy implementation asks peer nodes to create and share a state part over network. -The new implementation expects to find state parts as files on an S3 storage. - -The sync mechanism proceeds to download state parts mostly-sequentially from S3. -In case the state part is not available, the request will be retried after a -delay defined by `state_sync_timeout`, which by default is 1 minute. - -State parts are location on S3 at the following location: -``` -"chain_id={chain_id}/epoch_height={epoch_height}/shard_id={shard_id}/state_part_{part_id:06}_of_{num_parts:06}", -``` -for example `chain_id=testnet/epoch_height=1790/shard_id=2/state_part_032642_of_065402` - -After all state parts are downloaded, the node applies them, which replaces the existing State of the node. - -Currently, both downloading and applying state parts work rather quickly. diff --git a/docs/practices/workflows/io_trace.md b/docs/practices/workflows/io_trace.md index 8aba4b5f872..b295c42f074 100644 --- a/docs/practices/workflows/io_trace.md +++ b/docs/practices/workflows/io_trace.md @@ -216,7 +216,7 @@ memory. Then the SDK reads the serialized contract state from the hardcoded key `"STATE"`. Note that we charge 20 `tn_db_reads` for it, since we missed the -chunk cache, but we hit everything in the shard cache. Thus, there are no DB +accounting cache, but we hit everything in the shard cache. Thus, there are no DB requests. If there were DB requests for this `tn_db_reads`, you would see them listed. @@ -234,10 +234,10 @@ The `sha256` call here is used to shorten implicit account ids. Afterwards, a value with 16 bytes (a `u128`) is fetched from the trie state. To serve this, it required reading 30 trie nodes, 19 of them were cached in the -chunk cache and were not charged the full gas cost. And the remaining 11 missed -the chunk cache but they hit the shard cache. Nothing needed to be fetched from -DB because the Sweatcoin specific prefetcher has already loaded everything into -the shard cache. +accounting cache and were not charged the full gas cost. And the remaining 11 +missed the accounting cache but they hit the shard cache. Nothing needed to be +fetched from DB because the Sweatcoin specific prefetcher has already loaded +everything into the shard cache. *Note: We see trie node requests despite flat state being used. This is because the trace was collected with a binary that performed a read on both the trie and diff --git a/genesis-tools/genesis-csv-to-json/src/csv_parser.rs b/genesis-tools/genesis-csv-to-json/src/csv_parser.rs index 4b42298f699..17da184bddb 100644 --- a/genesis-tools/genesis-csv-to-json/src/csv_parser.rs +++ b/genesis-tools/genesis-csv-to-json/src/csv_parser.rs @@ -258,12 +258,12 @@ fn account_records(row: &Row, gas_price: Balance) -> Vec { gas_price, output_data_receivers: vec![], input_data_ids: vec![], - actions: vec![Action::FunctionCall(FunctionCallAction { + actions: vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "init".to_string(), args, gas: INIT_GAS, deposit: 0, - })], + }))], }), }; res.push(StateRecord::PostponedReceipt(Box::new(receipt))); diff --git a/genesis-tools/genesis-populate/Cargo.toml b/genesis-tools/genesis-populate/Cargo.toml index c42163181d7..10903d9145d 100644 --- a/genesis-tools/genesis-populate/Cargo.toml +++ b/genesis-tools/genesis-populate/Cargo.toml @@ -22,6 +22,7 @@ near-primitives.workspace = true near-store.workspace = true near-chain.workspace = true near-test-contracts.workspace = true +near-vm-runner.workspace = true [features] nightly_protocol = [ @@ -30,6 +31,7 @@ nightly_protocol = [ "near-epoch-manager/nightly_protocol", "near-primitives/nightly_protocol", "near-store/nightly_protocol", + "near-vm-runner/nightly_protocol", "nearcore/nightly_protocol", ] nightly = [ @@ -39,5 +41,6 @@ nightly = [ "near-epoch-manager/nightly", "near-primitives/nightly", "near-store/nightly", + "near-vm-runner/nightly", "nearcore/nightly", ] diff --git a/genesis-tools/genesis-populate/src/lib.rs b/genesis-tools/genesis-populate/src/lib.rs index cfe071a5046..eb41cfdbbce 100644 --- a/genesis-tools/genesis-populate/src/lib.rs +++ b/genesis-tools/genesis-populate/src/lib.rs @@ -12,7 +12,6 @@ use near_epoch_manager::types::BlockHeaderInfo; use near_epoch_manager::{EpochManager, EpochManagerAdapter, EpochManagerHandle}; use near_primitives::account::{AccessKey, Account}; use near_primitives::block::{genesis_chunks, Tip}; -use near_primitives::contract::ContractCode; use near_primitives::hash::{hash, CryptoHash}; use near_primitives::shard_layout::{account_id_to_shard_id, ShardUId}; use near_primitives::state_record::StateRecord; @@ -22,6 +21,7 @@ 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, }; +use near_vm_runner::ContractCode; use nearcore::{NearConfig, NightshadeRuntime}; use std::collections::BTreeMap; use std::hash::{Hash, Hasher}; diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 89c67d7755f..b0db8c4b57f 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -17,6 +17,7 @@ chrono.workspace = true clap.workspace = true futures.workspace = true hex.workspace = true +itertools.workspace = true once_cell.workspace = true parking_lot.workspace = true primitive-types.workspace = true @@ -72,9 +73,6 @@ test_features = ["nearcore/test_features", "near-store/test_features"] protocol_feature_fix_contract_loading_cost = [ "nearcore/protocol_feature_fix_contract_loading_cost", ] -protocol_feature_block_header_v4 = [ - "near-primitives/protocol_feature_block_header_v4", -] protocol_feature_reject_blocks_with_outdated_protocol_version = [ "near-primitives/protocol_feature_reject_blocks_with_outdated_protocol_version", "near-chain/protocol_feature_reject_blocks_with_outdated_protocol_version", @@ -86,9 +84,9 @@ protocol_feature_restrict_tla = ["nearcore/protocol_feature_restrict_tla"] nightly = [ "nightly_protocol", - "protocol_feature_block_header_v4", "protocol_feature_fix_contract_loading_cost", "protocol_feature_reject_blocks_with_outdated_protocol_version", + "protocol_feature_restrict_tla", "protocol_feature_simple_nightshade_v2", "near-actix-test-utils/nightly", "near-async/nightly", @@ -142,3 +140,4 @@ nightly_protocol = [ sandbox = ["near-chain/sandbox", "node-runtime/sandbox", "near-client/sandbox"] no_cache = ["nearcore/no_cache"] calimero_zero_storage = [] +new_epoch_sync = ["nearcore/new_epoch_sync"] diff --git a/integration-tests/src/node/mod.rs b/integration-tests/src/node/mod.rs index 012af7eee2f..80b92c1005f 100644 --- a/integration-tests/src/node/mod.rs +++ b/integration-tests/src/node/mod.rs @@ -8,13 +8,13 @@ use crate::user::{AsyncUser, User}; use near_chain_configs::Genesis; use near_crypto::{InMemorySigner, Signer}; use near_jsonrpc_primitives::errors::ServerError; -use near_primitives::contract::ContractCode; use near_primitives::num_rational::Ratio; use near_primitives::state_record::StateRecord; use near_primitives::transaction::SignedTransaction; use near_primitives::types::{AccountId, Balance, NumSeats}; use near_primitives::validator_signer::InMemoryValidatorSigner; use near_primitives::views::AccountView; +use near_vm_runner::ContractCode; use nearcore::config::{ create_testnet_configs, create_testnet_configs_from_seeds, Config, GenesisExt, }; diff --git a/integration-tests/src/node/runtime_node.rs b/integration-tests/src/node/runtime_node.rs index d7c25ab5023..980ae626c3f 100644 --- a/integration-tests/src/node/runtime_node.rs +++ b/integration-tests/src/node/runtime_node.rs @@ -109,7 +109,7 @@ mod tests { let (alice1, bob1) = (node.view_balance(&alice).unwrap(), node.view_balance(&bob).unwrap()); node_user.send_money(alice.clone(), bob.clone(), 1).unwrap(); let runtime_config = node.client.as_ref().read().unwrap().runtime_config.clone(); - let fee_helper = FeeHelper::new(runtime_config.fees, node.genesis().config.min_gas_price); + let fee_helper = FeeHelper::new(runtime_config, node.genesis().config.min_gas_price); let transfer_cost = fee_helper.transfer_cost(); let (alice2, bob2) = (node.view_balance(&alice).unwrap(), node.view_balance(&bob).unwrap()); assert_eq!(alice2, alice1 - 1 - transfer_cost); diff --git a/integration-tests/src/tests/client/challenges.rs b/integration-tests/src/tests/client/challenges.rs index bb9d8a2da88..03c070ac51a 100644 --- a/integration-tests/src/tests/client/challenges.rs +++ b/integration-tests/src/tests/client/challenges.rs @@ -63,11 +63,8 @@ fn test_block_with_challenges() { body.body.challenges = challenges.clone(); } }; - #[cfg(feature = "protocol_feature_block_header_v4")] - { - let block_body_hash = block.compute_block_body_hash().unwrap(); - block.mut_header().get_mut().inner_rest.block_body_hash = block_body_hash; - } + let block_body_hash = block.compute_block_body_hash().unwrap(); + block.mut_header().get_mut().inner_rest.block_body_hash = block_body_hash; block.mut_header().get_mut().inner_rest.challenges_root = Block::compute_challenges_root(&challenges); block.mut_header().resign(&*signer); diff --git a/integration-tests/src/tests/client/cold_storage.rs b/integration-tests/src/tests/client/cold_storage.rs index 466b5047692..c8733f57f72 100644 --- a/integration-tests/src/tests/client/cold_storage.rs +++ b/integration-tests/src/tests/client/cold_storage.rs @@ -122,12 +122,12 @@ fn test_storage_after_commit_of_cold_update() { "test0".parse().unwrap(), "test0".parse().unwrap(), &signer, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "write_random_value".to_string(), args: vec![], gas: 100_000_000_000_000, deposit: 0, - })], + }))], last_hash, ); assert_eq!(env.clients[0].process_tx(tx, false, false), ProcessTxResponse::ValidTx); diff --git a/integration-tests/src/tests/client/epoch_sync.rs b/integration-tests/src/tests/client/epoch_sync.rs new file mode 100644 index 00000000000..2d684b041b9 --- /dev/null +++ b/integration-tests/src/tests/client/epoch_sync.rs @@ -0,0 +1,98 @@ +use super::utils::TestEnvNightshadeSetupExt; +use near_chain::{ChainGenesis, Provenance}; +use near_chain_configs::Genesis; +use near_client::test_utils::TestEnv; +use near_client::ProcessTxResponse; +use near_crypto::{InMemorySigner, KeyType}; +use near_o11y::testonly::init_test_logger; +use near_primitives::transaction::{ + Action, DeployContractAction, FunctionCallAction, SignedTransaction, +}; +use near_primitives_core::hash::CryptoHash; +use near_primitives_core::types::BlockHeight; +use nearcore::config::GenesisExt; + +fn generate_transactions(last_hash: &CryptoHash, h: BlockHeight) -> Vec { + let mut txs = vec![]; + let signer = InMemorySigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + if h == 1 { + txs.push(SignedTransaction::from_actions( + h, + "test0".parse().unwrap(), + "test0".parse().unwrap(), + &signer, + vec![Action::DeployContract(DeployContractAction { + code: near_test_contracts::rs_contract().to_vec(), + })], + last_hash.clone(), + )); + } + + for i in 0..5 { + txs.push(SignedTransaction::from_actions( + h * 10 + i, + "test0".parse().unwrap(), + "test0".parse().unwrap(), + &signer, + vec![Action::FunctionCall(Box::new(FunctionCallAction { + method_name: "write_random_value".to_string(), + args: vec![], + gas: 100_000_000_000_000, + deposit: 0, + }))], + last_hash.clone(), + )); + } + + for i in 0..5 { + txs.push(SignedTransaction::send_money( + h * 10 + i, + "test0".parse().unwrap(), + "test1".parse().unwrap(), + &signer, + 1, + last_hash.clone(), + )); + } + txs +} + +/// Produce 4 epochs with some transactions. +/// At the end of each epoch check that `EpochSyncInfo` has been recorded. +#[test] +fn test_continuous_epoch_sync_info_population() { + init_test_logger(); + + let epoch_length = 5; + let max_height = epoch_length * 4 + 1; + + let mut genesis = Genesis::test(vec!["test0".parse().unwrap(), "test1".parse().unwrap()], 1); + + genesis.config.epoch_length = epoch_length; + let mut chain_genesis = ChainGenesis::test(); + chain_genesis.epoch_length = epoch_length; + let mut env = TestEnv::builder(chain_genesis) + .real_epoch_managers(&genesis.config) + .nightshade_runtimes(&genesis) + .build(); + + let mut last_hash = *env.clients[0].chain.genesis().hash(); + + for h in 1..max_height { + for tx in generate_transactions(&last_hash, h) { + assert_eq!(env.clients[0].process_tx(tx, false, false), ProcessTxResponse::ValidTx); + } + + let block = env.clients[0].produce_block(h).unwrap().unwrap(); + env.process_block(0, block.clone(), Provenance::PRODUCED); + last_hash = *block.hash(); + + if env.clients[0].epoch_manager.is_next_block_epoch_start(&last_hash).unwrap() { + let epoch_id = block.header().epoch_id().clone(); + + tracing::debug!("Checking epoch: {:?}", &epoch_id); + assert!(env.clients[0].chain.store().get_epoch_sync_info(&epoch_id).is_ok()); + tracing::debug!("OK"); + } + } +} diff --git a/integration-tests/src/tests/client/features/account_id_in_function_call_permission.rs b/integration-tests/src/tests/client/features/account_id_in_function_call_permission.rs index 9348b6cf3d5..564095170ea 100644 --- a/integration-tests/src/tests/client/features/account_id_in_function_call_permission.rs +++ b/integration-tests/src/tests/client/features/account_id_in_function_call_permission.rs @@ -46,7 +46,7 @@ fn test_account_id_in_function_call_permission_upgrade() { signer_id: "test0".parse().unwrap(), receiver_id: "test0".parse().unwrap(), public_key: signer.public_key(), - actions: vec![Action::AddKey(AddKeyAction { + actions: vec![Action::AddKey(Box::new(AddKeyAction { public_key: signer.public_key(), access_key: AccessKey { nonce: 1, @@ -56,7 +56,7 @@ fn test_account_id_in_function_call_permission_upgrade() { method_names: vec![], }), }, - })], + }))], nonce: 0, block_hash: CryptoHash::default(), }; @@ -111,7 +111,7 @@ fn test_very_long_account_id() { signer_id: "test0".parse().unwrap(), receiver_id: "test0".parse().unwrap(), public_key: signer.public_key(), - actions: vec![Action::AddKey(AddKeyAction { + actions: vec![Action::AddKey(Box::new(AddKeyAction { public_key: signer.public_key(), access_key: AccessKey { nonce: 1, @@ -121,7 +121,7 @@ fn test_very_long_account_id() { method_names: vec![], }), }, - })], + }))], nonce: 0, block_hash: tip.last_block_hash, } diff --git a/integration-tests/src/tests/client/features/chunk_nodes_cache.rs b/integration-tests/src/tests/client/features/chunk_nodes_cache.rs index 7e32e167a98..bd8ee2f8975 100644 --- a/integration-tests/src/tests/client/features/chunk_nodes_cache.rs +++ b/integration-tests/src/tests/client/features/chunk_nodes_cache.rs @@ -38,18 +38,18 @@ fn process_transaction( "test0".parse().unwrap(), signer, vec![ - Action::FunctionCall(FunctionCallAction { + Action::FunctionCall(Box::new(FunctionCallAction { args: encode(&[0u64, 10u64]), method_name: "write_key_value".to_string(), gas, deposit: 0, - }), - Action::FunctionCall(FunctionCallAction { + })), + Action::FunctionCall(Box::new(FunctionCallAction { args: encode(&[1u64, 20u64]), method_name: "write_key_value".to_string(), gas, deposit: 0, - }), + })), ], last_block_hash, ); @@ -66,7 +66,8 @@ fn process_transaction( /// Compare charged node accesses before and after protocol upgrade to the protocol version of `ChunkNodesCache`. /// This upgrade during chunk processing saves each node for which we charge touching trie node cost to a special -/// chunk cache, and such cost is charged only once on the first access. This effect doesn't persist across chunks. +/// accounting cache (used to be called "chunk cache"), and such cost is charged only once on the first access. +/// This effect doesn't persist across chunks. /// /// We run the same transaction 4 times and compare resulting costs. This transaction writes two different key-value /// pairs to the contract storage. @@ -78,8 +79,8 @@ fn process_transaction( /// /// 2nd run should count 12 regular db reads - for 6 nodes per each value, because protocol is not upgraded yet. /// 3nd run follows the upgraded protocol and it should count 8 db and 4 memory reads, which comes from 6 db reads -/// for `Value 1` and only 2 db reads for `Value 2`, because first 4 nodes were already put into the chunk cache. -/// 4nd run should give the same results, because caching must not affect different chunks. +/// for `Value 1` and only 2 db reads for `Value 2`, because first 4 nodes were already put into the accounting +/// cache. 4nd run should give the same results, because caching must not affect different chunks. #[test] fn compare_node_counts() { let mut genesis = Genesis::test(vec!["test0".parse().unwrap(), "test1".parse().unwrap()], 1); diff --git a/integration-tests/src/tests/client/features/delegate_action.rs b/integration-tests/src/tests/client/features/delegate_action.rs index 6bcd84ddf14..44e5e6653e2 100644 --- a/integration-tests/src/tests/client/features/delegate_action.rs +++ b/integration-tests/src/tests/client/features/delegate_action.rs @@ -228,14 +228,15 @@ fn check_meta_tx_fn_call( // dynamic cost. The contract reward can be inferred from that. // static send gas is paid and burnt upfront - let static_send_gas = fee_helper.cfg.fee(ActionCosts::new_action_receipt).send_fee(false) - + num_fn_calls as u64 * fee_helper.cfg.fee(ActionCosts::function_call_base).send_fee(false) - + msg_len * fee_helper.cfg.fee(ActionCosts::function_call_byte).send_fee(false); + let static_send_gas = fee_helper.cfg().fee(ActionCosts::new_action_receipt).send_fee(false) + + num_fn_calls as u64 + * fee_helper.cfg().fee(ActionCosts::function_call_base).send_fee(false) + + msg_len * fee_helper.cfg().fee(ActionCosts::function_call_byte).send_fee(false); // static execution gas burnt in the same receipt as the function calls but // it doesn't contribute to the contract reward - let static_exec_gas = fee_helper.cfg.fee(ActionCosts::new_action_receipt).exec_fee() - + num_fn_calls as u64 * fee_helper.cfg.fee(ActionCosts::function_call_base).exec_fee() - + msg_len * fee_helper.cfg.fee(ActionCosts::function_call_byte).exec_fee(); + let static_exec_gas = fee_helper.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + + num_fn_calls as u64 * fee_helper.cfg().fee(ActionCosts::function_call_base).exec_fee() + + msg_len * fee_helper.cfg().fee(ActionCosts::function_call_byte).exec_fee(); // calculate contract rewards as reward("gas burnt in fn call receipt" - "static exec costs") let gas_burnt_for_function_call = @@ -313,7 +314,7 @@ fn meta_tx_fn_call_access_key() { // Check previous allowance is set as expected let key = node.user().get_access_key(&sender, &public_key).expect("failed looking up fn access key"); - let AccessKeyPermissionView::FunctionCall { allowance, ..} = key.permission else { + let AccessKeyPermissionView::FunctionCall { allowance, .. } = key.permission else { panic!("should be function access key") }; assert_eq!(allowance.unwrap(), INITIAL_ALLOWANCE); @@ -338,7 +339,7 @@ fn meta_tx_fn_call_access_key() { .user() .get_access_key(&sender, &signer.public_key()) .expect("failed looking up fn access key"); - let AccessKeyPermissionView::FunctionCall { allowance, ..} = key.permission else { + let AccessKeyPermissionView::FunctionCall { allowance, .. } = key.permission else { panic!("should be function access key") }; assert_eq!( @@ -448,7 +449,7 @@ fn meta_tx_stake() { let tx_cost = fee_helper.stake_cost(); let public_key = create_user_test_signer(&sender).public_key; - let actions = vec![Action::Stake(StakeAction { public_key, stake: 0 })]; + let actions = vec![Action::Stake(Box::new(StakeAction { public_key, stake: 0 }))]; check_meta_tx_no_fn_call(&node, actions, tx_cost, 0, sender, relayer, receiver); } @@ -465,10 +466,10 @@ fn meta_tx_add_key() { // any public key works as long as it doesn't exists on the receiver, the // relayer public key is just handy let public_key = node.signer().public_key(); - let actions = vec![Action::AddKey(AddKeyAction { + let actions = vec![Action::AddKey(Box::new(AddKeyAction { public_key: public_key.clone(), access_key: AccessKey::full_access(), - })]; + }))]; check_meta_tx_no_fn_call(&node, actions, tx_cost, 0, sender, relayer, receiver.clone()); let key_view = node @@ -493,7 +494,8 @@ fn meta_tx_delete_key() { let tx_cost = fee_helper.delete_key_cost(); let public_key = PublicKey::from_seed(KeyType::ED25519, &receiver); - let actions = vec![Action::DeleteKey(DeleteKeyAction { public_key: public_key.clone() })]; + let actions = + vec![Action::DeleteKey(Box::new(DeleteKeyAction { public_key: public_key.clone() }))]; check_meta_tx_no_fn_call(&node, actions, tx_cost, 0, sender, relayer, receiver.clone()); let err = node @@ -627,12 +629,12 @@ fn meta_tx_ft_transfer() { /// Call the function "log_something" in the test contract. fn log_something_fn_call() -> Action { - Action::FunctionCall(FunctionCallAction { + Action::FunctionCall(Box::new(FunctionCallAction { method_name: TEST_METHOD.to_owned(), args: vec![], gas: 30_000_000_000_000, deposit: 0, - }) + })) } /// Construct an function call action with a FT transfer. @@ -649,12 +651,12 @@ fn ft_transfer_action(receiver: &str, amount: u128) -> (Action, u64) { .collect(); let method_name = "ft_transfer".to_owned(); let num_bytes = method_name.len() + args.len(); - let action = Action::FunctionCall(FunctionCallAction { + let action = Action::FunctionCall(Box::new(FunctionCallAction { method_name, args, gas: 20_000_000_000_000, deposit: 1, - }); + })); (action, num_bytes as u64) } @@ -669,12 +671,12 @@ fn ft_register_action(receiver: &str) -> Action { ) .bytes() .collect(); - Action::FunctionCall(FunctionCallAction { + Action::FunctionCall(Box::new(FunctionCallAction { method_name: "storage_deposit".to_owned(), args, gas: 20_000_000_000_000, deposit: NEAR_BASE, - }) + })) } /// Format a NEP-141 event for an ft transfer @@ -762,7 +764,7 @@ fn meta_tx_create_named_account() { let actions = vec![ Action::CreateAccount(CreateAccountAction {}), Action::Transfer(TransferAction { deposit: amount }), - Action::AddKey(AddKeyAction { public_key, access_key: AccessKey::full_access() }), + Action::AddKey(Box::new(AddKeyAction { public_key, access_key: AccessKey::full_access() })), ]; // Check the account doesn't exist, yet. We want to create it. diff --git a/integration-tests/src/tests/client/features/fix_contract_loading_cost.rs b/integration-tests/src/tests/client/features/fix_contract_loading_cost.rs index 8ced76eefc4..b1b77120fab 100644 --- a/integration-tests/src/tests/client/features/fix_contract_loading_cost.rs +++ b/integration-tests/src/tests/client/features/fix_contract_loading_cost.rs @@ -18,9 +18,10 @@ fn prepare_env_with_contract( let mut genesis = Genesis::test(vec![account.clone()], 1); genesis.config.epoch_length = epoch_length; genesis.config.protocol_version = protocol_version; + let runtime_config = near_primitives::runtime::config_store::RuntimeConfigStore::new(None); let mut env = TestEnv::builder(ChainGenesis::new(&genesis)) .real_epoch_managers(&genesis.config) - .nightshade_runtimes(&genesis) + .nightshade_runtimes_with_runtime_config_store(&genesis, vec![runtime_config]) .build(); deploy_test_contract(&mut env, account, &contract, epoch_length, 1); env diff --git a/integration-tests/src/tests/client/features/flat_storage.rs b/integration-tests/src/tests/client/features/flat_storage.rs index 8739218c393..7fa1cffb9d1 100644 --- a/integration-tests/src/tests/client/features/flat_storage.rs +++ b/integration-tests/src/tests/client/features/flat_storage.rs @@ -29,9 +29,10 @@ fn test_flat_storage_upgrade() { genesis.config.epoch_length = epoch_length; genesis.config.protocol_version = old_protocol_version; let chain_genesis = ChainGenesis::new(&genesis); + let runtime_config = near_primitives::runtime::config_store::RuntimeConfigStore::new(None); let mut env = TestEnv::builder(chain_genesis) .real_epoch_managers(&genesis.config) - .nightshade_runtimes(&genesis) + .nightshade_runtimes_with_runtime_config_store(&genesis, vec![runtime_config]) .build(); // We assume that it is enough to process 4 blocks to get a single txn included and processed. @@ -65,12 +66,12 @@ fn test_flat_storage_upgrade() { // Write key-value pair to state. { - let write_value_action = vec![Action::FunctionCall(FunctionCallAction { + let write_value_action = vec![Action::FunctionCall(Box::new(FunctionCallAction { args: encode(&[1u64, 10u64]), method_name: "write_key_value".to_string(), gas, deposit: 0, - })]; + }))]; let tip = env.clients[0].chain.head().unwrap(); let signed_transaction = Transaction { nonce: 10, @@ -93,12 +94,12 @@ fn test_flat_storage_upgrade() { let touching_trie_node_costs: Vec<_> = (0..2) .map(|i| { - let read_value_action = vec![Action::FunctionCall(FunctionCallAction { + let read_value_action = vec![Action::FunctionCall(Box::new(FunctionCallAction { args: encode(&[1u64]), method_name: "read_value".to_string(), gas, deposit: 0, - })]; + }))]; let tip = env.clients[0].chain.head().unwrap(); let signed_transaction = Transaction { nonce: 20 + i, diff --git a/integration-tests/src/tests/client/features/increase_deployment_cost.rs b/integration-tests/src/tests/client/features/increase_deployment_cost.rs index ca877e5830c..73b871bd4e5 100644 --- a/integration-tests/src/tests/client/features/increase_deployment_cost.rs +++ b/integration-tests/src/tests/client/features/increase_deployment_cost.rs @@ -3,12 +3,12 @@ use near_chain::ChainGenesis; use near_chain_configs::Genesis; use near_client::test_utils::TestEnv; use near_crypto::{InMemorySigner, KeyType}; -use near_primitives::config::VMConfig; use near_primitives::runtime::config_store::RuntimeConfigStore; use near_primitives::transaction::{Action, DeployContractAction}; use near_primitives::version::ProtocolFeature; use near_primitives::views::FinalExecutionStatus; use near_vm_runner::internal::VMKind; +use near_vm_runner::logic::Config as VMConfig; use nearcore::config::GenesisExt; use crate::tests::client::utils::TestEnvNightshadeSetupExt; diff --git a/integration-tests/src/tests/client/features/increase_storage_compute_cost.rs b/integration-tests/src/tests/client/features/increase_storage_compute_cost.rs index 4cf6fbb4d89..3e682a808d3 100644 --- a/integration-tests/src/tests/client/features/increase_storage_compute_cost.rs +++ b/integration-tests/src/tests/client/features/increase_storage_compute_cost.rs @@ -295,8 +295,12 @@ fn produce_saturated_chunk( ) -> std::sync::Arc { let msg_len = (method_name.len() + args.len()) as u64; // needed for gas computation later let gas = 300_000_000_000_000; - let actions = - vec![Action::FunctionCall(FunctionCallAction { method_name, args, gas, deposit: 0 })]; + let actions = vec![Action::FunctionCall(Box::new(FunctionCallAction { + method_name, + args, + gas, + deposit: 0, + }))]; let signer = InMemorySigner::from_seed(user_account.clone(), KeyType::ED25519, user_account); let tip = env.clients[0].chain.head().unwrap(); diff --git a/integration-tests/src/tests/client/features/lower_storage_key_limit.rs b/integration-tests/src/tests/client/features/lower_storage_key_limit.rs index c69507d6301..47e2dcdafd0 100644 --- a/integration-tests/src/tests/client/features/lower_storage_key_limit.rs +++ b/integration-tests/src/tests/client/features/lower_storage_key_limit.rs @@ -69,12 +69,12 @@ fn protocol_upgrade() { signer_id: "test0".parse().unwrap(), receiver_id: "test0".parse().unwrap(), public_key: signer.public_key(), - actions: vec![Action::FunctionCall(FunctionCallAction { + actions: vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "write_key_value".to_string(), args, gas: 10u64.pow(14), deposit: 0, - })], + }))], nonce: 0, block_hash: CryptoHash::default(), @@ -129,12 +129,12 @@ fn protocol_upgrade() { signer_id: "test0".parse().unwrap(), receiver_id: "test0".parse().unwrap(), public_key: signer.public_key(), - actions: vec![Action::FunctionCall(FunctionCallAction { + actions: vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "write_key_value".to_string(), args, gas: 10u64.pow(14), deposit: 0, - })], + }))], nonce: 0, block_hash: CryptoHash::default(), diff --git a/integration-tests/src/tests/client/features/nearvm.rs b/integration-tests/src/tests/client/features/nearvm.rs index ab9fd3d7d89..7ffaa315c03 100644 --- a/integration-tests/src/tests/client/features/nearvm.rs +++ b/integration-tests/src/tests/client/features/nearvm.rs @@ -48,12 +48,12 @@ fn test_nearvm_upgrade() { signer_id: "test0".parse().unwrap(), receiver_id: "test0".parse().unwrap(), public_key: signer.public_key(), - actions: vec![Action::FunctionCall(FunctionCallAction { + actions: vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "log_something".to_string(), args: Vec::new(), gas: 100_000_000_000_000, deposit: 0, - })], + }))], nonce: 0, block_hash: CryptoHash::default(), diff --git a/integration-tests/src/tests/client/features/zero_balance_account.rs b/integration-tests/src/tests/client/features/zero_balance_account.rs index 16628898b5e..7721d7de046 100644 --- a/integration-tests/src/tests/client/features/zero_balance_account.rs +++ b/integration-tests/src/tests/client/features/zero_balance_account.rs @@ -175,14 +175,14 @@ fn test_zero_balance_account_add_key() { for i in 1..5 { let new_key = PublicKey::from_seed(KeyType::ED25519, format!("{}", i).as_str()); keys.push(new_key.clone()); - actions.push(AddKey(AddKeyAction { + actions.push(AddKey(Box::new(AddKeyAction { public_key: new_key, access_key: AccessKey::full_access(), - })); + }))); } for i in 0..2 { let new_key = PublicKey::from_seed(KeyType::ED25519, format!("{}", i + 5).as_str()); - actions.push(AddKey(AddKeyAction { + actions.push(AddKey(Box::new(AddKeyAction { public_key: new_key, access_key: AccessKey { nonce: 0, @@ -192,7 +192,7 @@ fn test_zero_balance_account_add_key() { method_names: vec![], }), }, - })); + }))); } let head = env.clients[0].chain.head().unwrap(); @@ -230,7 +230,9 @@ fn test_zero_balance_account_add_key() { new_account_id.clone(), new_account_id.clone(), &new_signer, - vec![Action::DeleteKey(DeleteKeyAction { public_key: keys.last().unwrap().clone() })], + vec![Action::DeleteKey(Box::new(DeleteKeyAction { + public_key: keys.last().unwrap().clone(), + }))], *genesis_block.hash(), ); assert_eq!(env.clients[0].process_tx(delete_key_tx, false, false), ProcessTxResponse::ValidTx); diff --git a/integration-tests/src/tests/client/flat_storage.rs b/integration-tests/src/tests/client/flat_storage.rs index 1a2df88c022..789e0861cab 100644 --- a/integration-tests/src/tests/client/flat_storage.rs +++ b/integration-tests/src/tests/client/flat_storage.rs @@ -3,9 +3,12 @@ use assert_matches::assert_matches; use near_chain::{ChainGenesis, Provenance}; use near_chain_configs::Genesis; use near_client::test_utils::TestEnv; +use near_client::ProcessTxResponse; +use near_crypto::{InMemorySigner, KeyType}; use near_o11y::testonly::init_test_logger; use near_primitives::errors::StorageError; use near_primitives::shard_layout::{ShardLayout, ShardUId}; +use near_primitives::transaction::SignedTransaction; use near_primitives::trie_key::TrieKey; use near_primitives::types::AccountId; use near_primitives_core::types::BlockHeight; @@ -15,6 +18,7 @@ use near_store::flat::{ }; use near_store::test_utils::create_test_store; use near_store::{KeyLookupMode, Store, TrieTraversalItem}; +use near_vm_runner::logic::TrieNodesCount; use nearcore::config::GenesisExt; use std::str::FromStr; use std::thread; @@ -23,7 +27,7 @@ use std::time::Duration; use super::utils::TestEnvNightshadeSetupExt; /// Height on which we start flat storage background creation. -const START_HEIGHT: BlockHeight = 4; +const START_HEIGHT: BlockHeight = 7; /// Number of steps which should be enough to create flat storage. const CREATION_TIMEOUT: BlockHeight = 30; @@ -129,13 +133,25 @@ fn test_flat_storage_creation_sanity() { // Process some blocks with flat storage. Then remove flat storage data from disk. { let mut env = setup_env(&genesis, store.clone()); + let signer = InMemorySigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let genesis_hash = *env.clients[0].chain.genesis().hash(); for height in 1..START_HEIGHT { env.produce_block(0, height); + + let tx = SignedTransaction::send_money( + height, + "test0".parse().unwrap(), + "test0".parse().unwrap(), + &signer, + 1, + genesis_hash, + ); + assert_eq!(env.clients[0].process_tx(tx, false, false), ProcessTxResponse::ValidTx); } // If chain was initialized from scratch, flat storage state should be created. During block processing, flat - // storage head should be moved to block `START_HEIGHT - 3`. - let flat_head_height = START_HEIGHT - 3; + // storage head should be moved to block `START_HEIGHT - 4`. + 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); @@ -146,18 +162,19 @@ fn test_flat_storage_creation_sanity() { panic!("expected FlatStorageStatus::Ready status, got {status:?}"); } - // Deltas for blocks until `START_HEIGHT - 2` should not exist. - for height in 0..START_HEIGHT - 2 { + // 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)); } // Deltas for blocks until `START_HEIGHT` should still exist, // because they come after flat storage head. - for height in START_HEIGHT - 2..START_HEIGHT { + 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), - Ok(Some(_)) + Ok(Some(_)), + "height: {height}" ); } @@ -235,8 +252,20 @@ fn test_flat_storage_creation_two_shards() { // 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 signer = InMemorySigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let genesis_hash = *env.clients[0].chain.genesis().hash(); for height in 1..START_HEIGHT { env.produce_block(0, height); + + let tx = SignedTransaction::send_money( + height, + "test0".parse().unwrap(), + "test0".parse().unwrap(), + &signer, + 1, + genesis_hash, + ); + assert_eq!(env.clients[0].process_tx(tx, false, false), ProcessTxResponse::ValidTx); } for &shard_uid in &shard_uids { @@ -360,12 +389,12 @@ fn test_flat_storage_creation_start_from_state_part() { .runtime_adapter .get_trie_for_shard(0, &block_hash, state_root, true) .unwrap(); - let chunk_view = trie.flat_storage_chunk_view.unwrap(); for part_trie_keys in trie_keys.iter() { for trie_key in part_trie_keys.iter() { - assert_matches!(chunk_view.get_value(trie_key), Ok(Some(_))); + assert_matches!(trie.get_ref(&trie_key, KeyLookupMode::FlatStorage), Ok(Some(_))); } } + assert_eq!(trie.get_trie_nodes_count(), TrieNodesCount { db_reads: 0, mem_reads: 0 }); } } @@ -460,6 +489,10 @@ fn test_flat_storage_iter() { } #[test] +/// Initializes flat storage, then creates a Trie to read the flat storage +/// exactly at the flat head block. +/// Add another block to the flat state, which moves flat head and makes the +/// state of the previous flat head inaccessible. fn test_not_supported_block() { init_test_logger(); let genesis = Genesis::test(vec!["test0".parse().unwrap()], 1); @@ -468,11 +501,24 @@ fn test_not_supported_block() { let store = create_test_store(); let mut env = setup_env(&genesis, store); + let signer = InMemorySigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let genesis_hash = *env.clients[0].chain.genesis().hash(); + // Produce blocks up to `START_HEIGHT`. for height in 1..START_HEIGHT { env.produce_block(0, height); + let tx = SignedTransaction::send_money( + height, + "test0".parse().unwrap(), + "test0".parse().unwrap(), + &signer, + 1, + genesis_hash, + ); + assert_eq!(env.clients[0].process_tx(tx, false, false), ProcessTxResponse::ValidTx); } + let flat_head_height = START_HEIGHT - 4; // Trie key which must exist in the storage. let trie_key_bytes = near_primitives::trie_key::TrieKey::Account { account_id: "test0".parse().unwrap() } @@ -482,7 +528,7 @@ fn test_not_supported_block() { // After creating the first trie, produce block `START_HEIGHT` which moves flat storage // head 1 block further and invalidates it. let mut get_ref_results = vec![]; - for height in START_HEIGHT - 3..START_HEIGHT - 1 { + for height in flat_head_height..START_HEIGHT - 1 { let block_hash = env.clients[0].chain.get_block_hash_by_height(height).unwrap(); let state_root = *env.clients[0] .chain @@ -494,7 +540,7 @@ fn test_not_supported_block() { .runtime_adapter .get_trie_for_shard(shard_uid.shard_id(), &block_hash, state_root, true) .unwrap(); - if height == START_HEIGHT - 3 { + if height == flat_head_height { env.produce_block(0, START_HEIGHT); } get_ref_results.push(trie.get_ref(&trie_key_bytes, KeyLookupMode::FlatStorage)); diff --git a/integration-tests/src/tests/client/mod.rs b/integration-tests/src/tests/client/mod.rs index 04b2b24c238..17a65afa890 100644 --- a/integration-tests/src/tests/client/mod.rs +++ b/integration-tests/src/tests/client/mod.rs @@ -2,6 +2,8 @@ mod benchmarks; mod challenges; mod chunks_management; mod cold_storage; +#[cfg(feature = "new_epoch_sync")] +mod epoch_sync; mod features; mod flat_storage; mod process_blocks; diff --git a/integration-tests/src/tests/client/process_blocks.rs b/integration-tests/src/tests/client/process_blocks.rs index 8aee765977d..f39b25d42c3 100644 --- a/integration-tests/src/tests/client/process_blocks.rs +++ b/integration-tests/src/tests/client/process_blocks.rs @@ -46,8 +46,8 @@ use near_o11y::WithSpanContextExt; use near_primitives::block::{Approval, ApprovalInner}; use near_primitives::block_header::BlockHeader; use near_primitives::epoch_manager::RngSeed; -use near_primitives::errors::InvalidTxError; use near_primitives::errors::TxExecutionError; +use near_primitives::errors::{ActionError, ActionErrorKind, InvalidTxError}; use near_primitives::hash::{hash, CryptoHash}; use near_primitives::merkle::{verify_hash, PartialMerkleTree}; use near_primitives::receipt::DelayedReceiptIndices; @@ -231,12 +231,12 @@ pub(crate) fn prepare_env_with_congestion( "test0".parse().unwrap(), "test0".parse().unwrap(), &signer, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "call_promise".to_string(), args: serde_json::to_vec(&data).unwrap(), gas: gas_1, deposit: 0, - })], + }))], *genesis_block.hash(), ); tx_hashes.push(signed_transaction.get_hash()); @@ -590,7 +590,7 @@ fn produce_block_with_approvals_arrived_early() { let block = block_holder.read().unwrap().clone().unwrap(); conns[0].client_actor.do_send( BlockResponse { - block: block, + block, peer_id: PeerInfo::random().id, was_requested: false, } @@ -1174,10 +1174,10 @@ fn test_invalid_approvals() { b1.mut_header().get_mut().inner_rest.approvals = (0..100) .map(|i| { let account_id = AccountId::try_from(format!("test{}", i)).unwrap(); - Some( + Some(Box::new( create_test_signer(account_id.as_str()) .sign_approval(&ApprovalInner::Endorsement(*genesis.hash()), 1), - ) + )) }) .collect(); b1.mut_header().resign(&*signer); @@ -1290,10 +1290,7 @@ fn test_bad_orphan() { { let block = block.get_mut(); // Change the chunk in any way, chunk_headers_root won't match - #[cfg(feature = "protocol_feature_block_header_v4")] let chunk = &mut block.body.chunks[0].get_mut(); - #[cfg(not(feature = "protocol_feature_block_header_v4"))] - let chunk = &mut block.chunks[0].get_mut(); match &mut chunk.inner { ShardChunkHeaderInner::V1(inner) => inner.outcome_root = CryptoHash([1; 32]), @@ -1310,7 +1307,7 @@ fn test_bad_orphan() { // Orphan block with invalid approvals. Allowed for now. let mut block = env.clients[0].produce_block(9).unwrap().unwrap(); let some_signature = Signature::from_parts(KeyType::ED25519, &[1; 64]).unwrap(); - block.mut_header().get_mut().inner_rest.approvals = vec![Some(some_signature)]; + block.mut_header().get_mut().inner_rest.approvals = vec![Some(Box::new(some_signature))]; block.mut_header().get_mut().prev_hash = CryptoHash([3; 32]); block.mut_header().resign(&*signer); let res = env.clients[0].process_block_test(block.into(), Provenance::NONE); @@ -1323,10 +1320,7 @@ fn test_bad_orphan() { let some_signature = Signature::from_parts(KeyType::ED25519, &[1; 64]).unwrap(); { // Change the chunk in any way, chunk_headers_root won't match - #[cfg(feature = "protocol_feature_block_header_v4")] let chunk = block.get_mut().body.chunks[0].get_mut(); - #[cfg(not(feature = "protocol_feature_block_header_v4"))] - let chunk = block.get_mut().chunks[0].get_mut(); chunk.signature = some_signature; chunk.hash = ShardChunkHeaderV3::compute_hash(&chunk.inner); } @@ -2437,12 +2431,12 @@ fn test_validate_chunk_extra() { "test0".parse().unwrap(), "test0".parse().unwrap(), &signer, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "write_block_height".to_string(), args: vec![], gas: 100000000000000, deposit: 0, - })], + }))], *last_block.hash(), ); assert_eq!( @@ -2485,11 +2479,8 @@ fn test_validate_chunk_extra() { block.mut_header().get_mut().inner_rest.chunk_mask = vec![true]; block.mut_header().get_mut().inner_lite.outcome_root = Block::compute_outcome_root(block.chunks().iter()); - #[cfg(feature = "protocol_feature_block_header_v4")] - { - block.mut_header().get_mut().inner_rest.block_body_hash = - block.compute_block_body_hash().unwrap(); - } + block.mut_header().get_mut().inner_rest.block_body_hash = + block.compute_block_body_hash().unwrap(); block.mut_header().resign(&validator_signer); let res = env.clients[0].process_block_test(block.clone().into(), Provenance::NONE); assert_matches!(res.unwrap_err(), near_chain::Error::ChunksMissing(_)); @@ -2775,6 +2766,8 @@ fn test_block_execution_outcomes() { assert!(execution_outcomes_from_block[0].outcome_with_id.id == delayed_receipt_id[0]); } +// This test verifies that gas consumed for processing refund receipts is taken into account +// for the purpose of limiting the size of the chunk. #[test] fn test_refund_receipts_processing() { init_test_logger(); @@ -2788,8 +2781,8 @@ fn test_refund_receipts_processing() { ); genesis.config.epoch_length = epoch_length; genesis.config.min_gas_price = min_gas_price; - // Set gas limit to be small enough to produce some delay receipts, but - // large enough for transactions to get through. + // Set gas limit to be small enough to produce some delayed receipts, but large enough for + // transactions to get through. genesis.config.gas_limit = 100_000_000; let chain_genesis = ChainGenesis::new(&genesis); let mut env = TestEnv::builder(chain_genesis) @@ -2799,9 +2792,9 @@ fn test_refund_receipts_processing() { let genesis_block = env.clients[0].chain.get_block_by_height(0).unwrap(); let signer = InMemorySigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); let mut tx_hashes = vec![]; - // send transactions to a non-existing account to generate refund + // Send transactions to a non-existing account to generate refunds. for i in 0..3 { - // send transaction to the same account to generate local receipts + // Send transaction from the same account to generate local receipts. let tx = SignedTransaction::send_money( i + 1, "test0".parse().unwrap(), @@ -2814,84 +2807,102 @@ fn test_refund_receipts_processing() { assert_eq!(env.clients[0].process_tx(tx, false, false), ProcessTxResponse::ValidTx); } - env.produce_block(0, 3); - env.produce_block(0, 4); - let mut block_height = 5; + // Make sure all transactions are processed. + for i in 3..16 { + env.produce_block(0, i); + } + let test_shard_uid = ShardUId { version: 1, shard_id: 0 }; - loop { - env.produce_block(0, block_height); - let block = env.clients[0].chain.get_block_by_height(block_height).unwrap().clone(); - let prev_block = - env.clients[0].chain.get_block_by_height(block_height - 1).unwrap().clone(); - let chunk_extra = env.clients[0] - .chain - .get_chunk_extra(prev_block.hash(), &test_shard_uid) - .unwrap() - .clone(); - let state_update = env.clients[0] - .runtime_adapter - .get_tries() - .new_trie_update(test_shard_uid, *chunk_extra.state_root()); - let delayed_indices: Option = - get(&state_update, &TrieKey::DelayedReceiptIndices).unwrap(); - let finished_all_delayed_receipts = match delayed_indices { - None => false, - Some(delayed_indices) => { - delayed_indices.next_available_index > 0 - && delayed_indices.first_index == delayed_indices.next_available_index - } - }; - let chunk = - env.clients[0].chain.get_chunk(&block.chunks()[0].chunk_hash()).unwrap().clone(); - if chunk.receipts().is_empty() - && chunk.transactions().is_empty() - && finished_all_delayed_receipts - { - break; + for tx_hash in tx_hashes { + let tx_outcome = env.clients[0].chain.get_execution_outcome(&tx_hash).unwrap(); + assert_eq!(tx_outcome.outcome_with_id.outcome.receipt_ids.len(), 1); + if let ExecutionStatus::SuccessReceiptId(id) = tx_outcome.outcome_with_id.outcome.status { + let receipt_outcome = env.clients[0].chain.get_execution_outcome(&id).unwrap(); + assert_matches!( + receipt_outcome.outcome_with_id.outcome.status, + ExecutionStatus::Failure(TxExecutionError::ActionError(ActionError { + kind: ActionErrorKind::AccountDoesNotExist { .. }, + .. + })) + ); + let execution_outcomes_from_block = env.clients[0] + .chain + .store() + .get_block_execution_outcomes(&receipt_outcome.block_hash) + .unwrap() + .remove(&0) + .unwrap(); + assert_eq!(execution_outcomes_from_block.len(), 1); + let chunk_extra = env.clients[0] + .chain + .get_chunk_extra(&receipt_outcome.block_hash, &test_shard_uid) + .unwrap() + .clone(); + assert!(chunk_extra.gas_used() >= chunk_extra.gas_limit()); + } else { + unreachable!("Transaction must succeed"); } - block_height += 1; } +} - let mut refund_receipt_ids = HashSet::new(); - for (_, id) in tx_hashes.into_iter().enumerate() { - let execution_outcome = env.clients[0].chain.get_execution_outcome(&id).unwrap(); - assert_eq!(execution_outcome.outcome_with_id.outcome.receipt_ids.len(), 1); - match execution_outcome.outcome_with_id.outcome.status { - ExecutionStatus::SuccessReceiptId(id) => { - let receipt_outcome = env.clients[0].chain.get_execution_outcome(&id).unwrap(); - assert_matches!( - receipt_outcome.outcome_with_id.outcome.status, - ExecutionStatus::Failure(TxExecutionError::ActionError(_)) - ); - for id in receipt_outcome.outcome_with_id.outcome.receipt_ids.iter() { - refund_receipt_ids.insert(*id); - } - } - _ => assert!(false), - }; +// Tests that the number of delayed receipts in each shard is bounded based on the gas limit of +// the chunk and any new receipts are not included if there are too many delayed receipts. +#[test] +fn test_delayed_receipt_count_limit() { + init_test_logger(); + + let epoch_length = 5; + let min_gas_price = 10000; + let mut genesis = Genesis::test_sharded_new_version(vec!["test0".parse().unwrap()], 1, vec![1]); + genesis.config.epoch_length = epoch_length; + genesis.config.min_gas_price = min_gas_price; + // Set gas limit to be small enough to produce some delayed receipts, but large enough for + // transactions to get through. + // This will result in delayed receipt count limit of 20. + let transaction_costs = RuntimeConfig::test().fees; + let chunk_gas_limit = 10 * transaction_costs.fee(ActionCosts::new_action_receipt).exec_fee(); + genesis.config.gas_limit = chunk_gas_limit; + let chain_genesis = ChainGenesis::new(&genesis); + let mut env = TestEnv::builder(chain_genesis) + .real_epoch_managers(&genesis.config) + .nightshade_runtimes(&genesis) + .build(); + let genesis_block = env.clients[0].chain.get_block_by_height(0).unwrap(); + + let signer = InMemorySigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + // Send enough transactions to saturate delayed receipts capacity. + let total_tx_count = 200usize; + for i in 0..total_tx_count { + let tx = SignedTransaction::from_actions( + (i + 1) as u64, + "test0".parse().unwrap(), + "test0".parse().unwrap(), + &signer, + vec![Action::DeployContract(DeployContractAction { code: vec![92; 10000] })], + *genesis_block.hash(), + ); + assert_eq!(env.clients[0].process_tx(tx, false, false), ProcessTxResponse::ValidTx); } - let ending_block_height = block_height - 1; - let begin_block_height = ending_block_height - refund_receipt_ids.len() as u64 + 1; - let mut processed_refund_receipt_ids = HashSet::new(); - for i in begin_block_height..=ending_block_height { - let block = env.clients[0].chain.get_block_by_height(i).unwrap().clone(); - let execution_outcomes_from_block = env.clients[0] - .chain - .store() - .get_block_execution_outcomes(block.hash()) - .unwrap() - .remove(&0) - .unwrap(); - for outcome in execution_outcomes_from_block.iter() { - processed_refund_receipt_ids.insert(outcome.outcome_with_id.id); + let mut included_tx_count = 0; + let mut height = 1; + while included_tx_count < total_tx_count { + env.produce_block(0, height); + let block = env.clients[0].chain.get_block_by_height(height).unwrap(); + let chunk = env.clients[0].chain.get_chunk(&block.chunks()[0].chunk_hash()).unwrap(); + // These checks are useful to ensure that we didn't mess up the test setup. + assert!(chunk.receipts().len() <= 1); + assert!(chunk.transactions().len() <= 5); + + // Because all transactions are in the transactions pool, this means we have not included + // some transactions due to the delayed receipt count limit. + if included_tx_count > 0 && chunk.transactions().is_empty() { + break; } - let chunk_extra = - env.clients[0].chain.get_chunk_extra(block.hash(), &test_shard_uid).unwrap().clone(); - assert_eq!(execution_outcomes_from_block.len(), 1); - assert!(chunk_extra.gas_used() >= chunk_extra.gas_limit()); + included_tx_count += chunk.transactions().len(); + height += 1; } - assert_eq!(processed_refund_receipt_ids, refund_receipt_ids); + assert!(included_tx_count < total_tx_count); } #[test] @@ -2936,11 +2947,11 @@ fn test_execution_metadata() { + config.fees.fee(ActionCosts::function_call_byte).exec_fee() * "main".len() as u64; let expected_wasm_ops = match config.wasm_config.limit_config.contract_prepare_version { - near_primitives::config::ContractPrepareVersion::V0 => 2, - near_primitives::config::ContractPrepareVersion::V1 => 2, + near_vm_runner::logic::ContractPrepareVersion::V0 => 2, + near_vm_runner::logic::ContractPrepareVersion::V1 => 2, // We spend two wasm instructions (call & drop), plus 8 ops for initializing function // operand stack (8 bytes worth to hold the return value.) - near_primitives::config::ContractPrepareVersion::V2 => 10, + near_vm_runner::logic::ContractPrepareVersion::V2 => 10, }; // Profile for what's happening *inside* wasm vm during function call. @@ -3340,7 +3351,6 @@ fn test_not_broadcast_block_on_accept() { } #[test] -#[cfg_attr(not(feature = "protocol_feature_block_header_v4"), should_panic)] fn test_header_version_downgrade() { init_test_logger(); use borsh::ser::BorshSerialize; @@ -3546,12 +3556,12 @@ fn test_validator_stake_host_function() { "test0".parse().unwrap(), "test0".parse().unwrap(), &signer, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "ext_validator_stake".to_string(), args: b"test0".to_vec(), gas: 100_000_000_000_000, deposit: 0, - })], + }))], *genesis_block.hash(), ); assert_eq!( @@ -3594,13 +3604,12 @@ fn test_catchup_no_sharding_change() { /// These tests fail on aarch because the WasmtimeVM::precompile method doesn't populate the cache. mod contract_precompilation_tests { use super::*; - use near_primitives::contract::ContractCode; use near_primitives::test_utils::MockEpochInfoProvider; use near_primitives::views::ViewApplyState; use near_store::{Store, StoreCompiledContractCache, TrieUpdate}; - use near_vm_runner::get_contract_cache_key; use near_vm_runner::internal::VMKind; use near_vm_runner::logic::CompiledContractCache; + use near_vm_runner::{get_contract_cache_key, ContractCode}; use node_runtime::state_viewer::TrieViewer; const EPOCH_LENGTH: u64 = 25; diff --git a/integration-tests/src/tests/client/sandbox.rs b/integration-tests/src/tests/client/sandbox.rs index 476ea997d85..1c80dfd2bc5 100644 --- a/integration-tests/src/tests/client/sandbox.rs +++ b/integration-tests/src/tests/client/sandbox.rs @@ -44,12 +44,12 @@ fn test_setup() -> (TestEnv, InMemorySigner) { "test0".parse().unwrap(), "test0".parse().unwrap(), &signer, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "write_random_value".to_string(), args: vec![], gas: 100000000000000, deposit: 0, - })], + }))], ), ProcessTxResponse::ValidTx ); diff --git a/integration-tests/src/tests/client/sharding_upgrade.rs b/integration-tests/src/tests/client/sharding_upgrade.rs index 8982ab97e60..0ecb406466c 100644 --- a/integration-tests/src/tests/client/sharding_upgrade.rs +++ b/integration-tests/src/tests/client/sharding_upgrade.rs @@ -1,8 +1,12 @@ use borsh::BorshSerialize; -use near_client::ProcessTxResponse; +use near_client::{Client, ProcessTxResponse}; +use near_primitives::epoch_manager::{AllEpochConfig, EpochConfig}; +use near_primitives_core::num_rational::Rational32; use crate::tests::client::process_blocks::set_block_protocol_version; +use assert_matches::assert_matches; use near_chain::near_chain_primitives::Error; +use near_chain::test_utils::wait_for_all_blocks_in_processing; use near_chain::{ChainGenesis, ChainStoreAccess, Provenance}; use near_chain_configs::Genesis; use near_client::test_utils::{run_catchup, TestEnv}; @@ -19,20 +23,17 @@ use near_primitives::transaction::{ use near_primitives::types::{BlockHeight, NumShards, ProtocolVersion, ShardId}; use near_primitives::utils::MaybeValidated; use near_primitives::version::ProtocolFeature; -use near_primitives::views::QueryRequest; -use near_primitives::views::{ExecutionStatusView, FinalExecutionStatus}; -use near_primitives_core::version::PROTOCOL_VERSION; +#[cfg(not(feature = "protocol_feature_simple_nightshade_v2"))] +use near_primitives::version::PROTOCOL_VERSION; +use near_primitives::views::{ExecutionStatusView, FinalExecutionStatus, QueryRequest}; use near_store::test_utils::{gen_account, gen_unique_accounts}; use nearcore::config::GenesisExt; use nearcore::NEAR_BASE; -use tracing::debug; - -use assert_matches::assert_matches; -use near_chain::test_utils::wait_for_all_blocks_in_processing; use rand::seq::{IteratorRandom, SliceRandom}; use rand::{thread_rng, Rng}; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::sync::Arc; +use tracing::debug; use super::utils::TestEnvNightshadeSetupExt; @@ -48,32 +49,43 @@ const SIMPLE_NIGHTSHADE_V2_PROTOCOL_VERSION: ProtocolVersion = PROTOCOL_VERSION const P_CATCHUP: f64 = 0.2; +enum ReshardingType { + // In the V0->V1 resharding outgoing receipts are reassigned to receiver. + V1, + // In the V1->V2 resharding outgoing receipts are reassigned to lowest index child. + #[allow(unused)] + V2, +} + // Return the expected number of shards. -// The number of shards depends on height and whether shard layout V2 is enabled. -fn get_expected_shards_num(epoch_length: u64, height: BlockHeight, testing_v2: bool) -> u64 { - if height < 2 * epoch_length { - return 1; - } - if height < 4 * epoch_length { - return 4; - } - if testing_v2 && PROTOCOL_VERSION >= SIMPLE_NIGHTSHADE_V2_PROTOCOL_VERSION { - // V2 is enabled, there should be 5 shards. - return 5; - } else { - // V2 is not enabled we stay at 4 shards forever. - return 4; - } +fn get_expected_shards_num( + epoch_length: u64, + height: BlockHeight, + resharding_type: &ReshardingType, +) -> u64 { + match resharding_type { + ReshardingType::V1 => { + if height < 2 * epoch_length { + return 1; + } else { + return 4; + } + } + ReshardingType::V2 => { + if height < 2 * epoch_length { + return 4; + } else { + return 5; + } + } + }; } /// Test environment prepared for testing the sharding upgrade. -/// Epoch 0, blocks 1-5 : 1 shard -/// Epoch 1, blocks 6-10 : 1 shard, state split happens -/// Epoch 2: blocks 10-15: 4 shards, shard layout upgrades to simple_nightshade_layout, -/// -/// If V2 is enabled the test continues: -/// Epoch 3: blocks 16-20: 4 shards, state split happens -/// Epoch 4: blocks 21-25: 5 shards, shard layout upgrades to simple_nightshade_layout_v2 +/// Epoch 0, blocks 1-5 : genesis shard layout +/// Epoch 1, blocks 6-10 : genesis shard layout, state split happens +/// Epoch 2: blocks 10-15: target shard layout, shard layout is upgraded +/// Epoch 3: blocks 16-20: target shard layout, /// /// Note: if the test is extended to more epochs, garbage collection will /// kick in and delete data that is checked at the end of the test. @@ -81,9 +93,8 @@ struct TestShardUpgradeEnv { env: TestEnv, initial_accounts: Vec, init_txs: Vec, - txs_by_height: HashMap>, + txs_by_height: BTreeMap>, epoch_length: u64, - num_validators: usize, num_clients: usize, } @@ -94,30 +105,36 @@ impl TestShardUpgradeEnv { num_clients: usize, num_init_accounts: usize, gas_limit: Option, + genesis_protocol_version: ProtocolVersion, ) -> Self { let mut rng = thread_rng(); let validators: Vec = (0..num_validators).map(|i| format!("test{}", i).parse().unwrap()).collect(); - let initial_accounts = - [validators, gen_unique_accounts(&mut rng, num_init_accounts, num_init_accounts)] - .concat(); - let genesis = - setup_genesis(epoch_length, num_validators as u64, initial_accounts.clone(), gas_limit); + let other_accounts = gen_unique_accounts(&mut rng, num_init_accounts, num_init_accounts); + let initial_accounts = [validators, other_accounts].concat(); + let genesis = setup_genesis( + epoch_length, + num_validators as u64, + initial_accounts.clone(), + gas_limit, + genesis_protocol_version, + ); let chain_genesis = ChainGenesis::new(&genesis); let env = TestEnv::builder(chain_genesis) .clients_count(num_clients) .validator_seats(num_validators) .real_epoch_managers(&genesis.config) .nightshade_runtimes(&genesis) + .track_all_shards() .build(); + assert_eq!(env.validators.len(), num_validators); Self { env, initial_accounts, epoch_length, - num_validators, num_clients, init_txs: vec![], - txs_by_height: HashMap::new(), + txs_by_height: BTreeMap::new(), } } @@ -129,7 +146,7 @@ impl TestShardUpgradeEnv { /// `txs_by_height` is a hashmap from block height to transactions to be /// included at block at that height fn set_tx_at_height(&mut self, height: u64, txs: Vec) { - debug!(target:"test", "adding txs at height {} txs: {:?}", height, txs.iter().map(|x|x.get_hash()).collect::>()); + debug!(target: "test", "setting txs at height {} txs: {:?}", height, txs.iter().map(|x|x.get_hash()).collect::>()); self.txs_by_height.insert(height, txs); } @@ -138,7 +155,7 @@ impl TestShardUpgradeEnv { /// /// please also see the step_impl for changing the protocol version fn step(&mut self, p_drop_chunk: f64) { - self.step_impl(p_drop_chunk, SIMPLE_NIGHTSHADE_PROTOCOL_VERSION, false); + self.step_impl(p_drop_chunk, SIMPLE_NIGHTSHADE_PROTOCOL_VERSION, &ReshardingType::V1); } /// produces and processes the next block also checks that all accounts in @@ -151,23 +168,21 @@ impl TestShardUpgradeEnv { &mut self, p_drop_chunk: f64, protocol_version: ProtocolVersion, - testing_v2: bool, + resharding_type: &ReshardingType, ) { let env = &mut self.env; let mut rng = thread_rng(); let head = env.clients[0].chain.head().unwrap(); let height = head.height + 1; - let expected_num_shards = get_expected_shards_num(self.epoch_length, height, testing_v2); + let expected_num_shards = + get_expected_shards_num(self.epoch_length, height, resharding_type); + + tracing::debug!(target: "test", height, expected_num_shards, "step"); // add transactions for the next block if height == 1 { for tx in self.init_txs.iter() { - for j in 0..self.num_validators { - assert_eq!( - env.clients[j].process_tx(tx.clone(), false, false), - ProcessTxResponse::ValidTx - ); - } + Self::process_tx(env, tx); } } @@ -177,26 +192,25 @@ impl TestShardUpgradeEnv { // it when we are producing the block at `height` if let Some(txs) = self.txs_by_height.get(&(height + 1)) { for tx in txs { - for j in 0..self.num_validators { - assert_eq!( - env.clients[j].process_tx(tx.clone(), false, false), - ProcessTxResponse::ValidTx - ); - } + Self::process_tx(env, tx); } } // produce block - let block_producer = { - let epoch_id = env.clients[0] - .epoch_manager - .get_epoch_id_from_prev_block(&head.last_block_hash) - .unwrap(); - env.clients[0].epoch_manager.get_block_producer(&epoch_id, height).unwrap() + let block = { + let client = &env.clients[0]; + let epoch_id = + client.epoch_manager.get_epoch_id_from_prev_block(&head.last_block_hash).unwrap(); + let block_producer = + client.epoch_manager.get_block_producer(&epoch_id, height).unwrap(); + let _span = tracing::debug_span!(target: "test", "", client=?block_producer).entered(); + let block_producer_client = env.client(&block_producer); + let mut block = block_producer_client.produce_block(height).unwrap().unwrap(); + set_block_protocol_version(&mut block, block_producer.clone(), protocol_version); + + block }; - let block_producer_client = env.client(&block_producer); - let mut block = block_producer_client.produce_block(height).unwrap().unwrap(); - set_block_protocol_version(&mut block, block_producer.clone(), protocol_version); + // Make sure that catchup is done before the end of each epoch, but when it is done is // by chance. This simulates when catchup takes a long time to be done // Note: if the catchup happens only at the last block of an epoch then @@ -204,11 +218,13 @@ impl TestShardUpgradeEnv { let should_catchup = rng.gen_bool(P_CATCHUP) || height % self.epoch_length == 0; // process block, this also triggers chunk producers for the next block to produce chunks for j in 0..self.num_clients { + let client = &mut env.clients[j]; + let _span = tracing::debug_span!(target: "test", "process block", client=j).entered(); let produce_chunks = !rng.gen_bool(p_drop_chunk); // Here we don't just call self.clients[i].process_block_sync_with_produce_chunk_options // because we want to call run_catchup before finish processing this block. This simulates // that catchup and block processing run in parallel. - env.clients[j] + client .start_process_block( MaybeValidated::from(block.clone()), Provenance::NONE, @@ -216,11 +232,10 @@ impl TestShardUpgradeEnv { ) .unwrap(); if should_catchup { - run_catchup(&mut env.clients[j], &[]).unwrap(); + run_catchup(client, &[]).unwrap(); } - while wait_for_all_blocks_in_processing(&mut env.clients[j].chain) { - let (_, errors) = - env.clients[j].postprocess_ready_blocks(Arc::new(|_| {}), produce_chunks); + while wait_for_all_blocks_in_processing(&mut client.chain) { + let (_, errors) = client.postprocess_ready_blocks(Arc::new(|_| {}), produce_chunks); assert!(errors.is_empty(), "unexpected errors: {:?}", errors); } if should_catchup { @@ -248,8 +263,32 @@ impl TestShardUpgradeEnv { } } + // Submit the tx to all clients for processing and checks: + // Clients that track the relevant shard should return ValidTx + // Clients that do not track the relevenat shard should return RequestRouted + // At least one client should process it and return ValidTx. + fn process_tx(env: &mut TestEnv, tx: &SignedTransaction) { + let mut response_valid_count = 0; + let mut response_routed_count = 0; + for j in 0..env.validators.len() { + let response = env.clients[j].process_tx(tx.clone(), false, false); + tracing::trace!(target: "test", client=j, tx=?tx.get_hash(), ?response, "process tx"); + match response { + ProcessTxResponse::ValidTx => response_valid_count += 1, + ProcessTxResponse::RequestRouted => response_routed_count += 1, + response => { + panic!("invalid tx response {response:?} {tx:?}"); + } + } + } + assert_ne!(response_valid_count, 0); + assert_eq!(response_valid_count + response_routed_count, env.validators.len()); + } + /// check that all accounts in `accounts` exist in the current state fn check_accounts(&mut self, accounts: Vec<&AccountId>) { + tracing::debug!(target: "test", "checking accounts"); + let head = self.env.clients[0].chain.head().unwrap(); let block = self.env.clients[0].chain.get_block(&head.last_block_hash).unwrap(); for account_id in accounts { @@ -332,6 +371,7 @@ impl TestShardUpgradeEnv { allow_not_started: bool, skip_heights: Vec, ) -> Vec { + tracing::debug!(target: "test", "checking tx outcomes"); let env = &mut self.env; let head = env.clients[0].chain.head().unwrap(); let block = env.clients[0].chain.get_block(&head.last_block_hash).unwrap(); @@ -351,37 +391,41 @@ impl TestShardUpgradeEnv { let mut successful_txs = Vec::new(); for tx in txs_to_check { let id = &tx.get_hash(); - let account_id = &tx.transaction.signer_id; - let shard_uid = account_id_to_shard_uid(account_id, &shard_layout); - for (i, account_id) in env.validators.iter().enumerate() { - let cares_about_shard = env.clients[i].shard_tracker.care_about_shard( - Some(account_id), + + let signer_account_id = &tx.transaction.signer_id; + let shard_uid = account_id_to_shard_uid(signer_account_id, &shard_layout); + + tracing::trace!(target: "test", tx=?id, ?signer_account_id, ?shard_uid, "checking tx"); + + for (i, validator_account_id) in env.validators.iter().enumerate() { + let client = &env.clients[i]; + + let cares_about_shard = client.shard_tracker.care_about_shard( + Some(validator_account_id), block.header().prev_hash(), shard_uid.shard_id(), true, ); - if cares_about_shard { - let execution_outcomes = - env.clients[i].chain.get_transaction_execution_result(id).unwrap(); - if execution_outcomes.is_empty() { - assert!(allow_not_started, "transaction {:?} not processed", id); - } else { - let final_outcome = - env.clients[i].chain.get_final_transaction_result(id).unwrap(); - - let outcome_status = final_outcome.status.clone(); - if matches!(outcome_status, FinalExecutionStatus::SuccessValue(_)) { - successful_txs.push(tx.get_hash()); - } else { - panic!("tx failed {:?}", final_outcome); - } - for outcome in final_outcome.receipts_outcome { - assert_matches!( - outcome.outcome.status, - ExecutionStatusView::SuccessValue(_) - ); - } - } + if !cares_about_shard { + continue; + } + let execution_outcomes = client.chain.get_transaction_execution_result(id).unwrap(); + if execution_outcomes.is_empty() { + tracing::error!(target: "test", tx=?id, client=i, "tx not processed"); + assert!(allow_not_started, "tx {:?} not processed", id); + continue; + } + let final_outcome = client.chain.get_final_transaction_result(id).unwrap(); + + let outcome_status = final_outcome.status.clone(); + if matches!(outcome_status, FinalExecutionStatus::SuccessValue(_)) { + successful_txs.push(tx.get_hash()); + } else { + tracing::error!(target: "test", tx=?id, client=i, "tx failed"); + panic!("tx failed {:?}", final_outcome); + } + for outcome in final_outcome.receipts_outcome { + assert_matches!(outcome.outcome.status, ExecutionStatusView::SuccessValue(_)); } } } @@ -398,19 +442,34 @@ impl TestShardUpgradeEnv { .get_shard_layout_from_prev_block(&head.last_block_hash) .unwrap(); let block = env.clients[0].chain.get_block(&head.last_block_hash).unwrap(); + for (shard_id, chunk_header) in block.chunks().iter().enumerate() { - if chunk_header.height_included() == block.header().height() { - let outgoing_receipts = env.clients[0] + if chunk_header.height_included() != block.header().height() { + continue; + } + let shard_id = shard_id as ShardId; + + for (i, me) in env.validators.iter().enumerate() { + let client = &mut env.clients[i]; + let care_about_shard = client.shard_tracker.care_about_shard( + Some(me), + &head.prev_block_hash, + shard_id, + true, + ); + if !care_about_shard { + continue; + } + + let outgoing_receipts = client .chain .mut_store() - .get_outgoing_receipts(&head.last_block_hash, shard_id as ShardId) + .get_outgoing_receipts(&head.last_block_hash, shard_id) .unwrap() .clone(); for receipt in outgoing_receipts.iter() { - let target_shard_id = env.clients[0] - .chain - .get_shard_id_for_receipt_id(&receipt.receipt_id) - .unwrap(); + let target_shard_id = + client.chain.get_shard_id_for_receipt_id(&receipt.receipt_id).unwrap(); assert_eq!( target_shard_id, account_id_to_shard_id(&receipt.receiver_id, &shard_layout) @@ -422,6 +481,8 @@ impl TestShardUpgradeEnv { /// Check that after split state is finished, the artifacts stored in storage is removed fn check_split_states_artifacts(&mut self) { + tracing::debug!(target: "test", "checking split states artifacts"); + let env = &mut self.env; let head = env.clients[0].chain.head().unwrap(); for height in 0..head.height { @@ -440,54 +501,116 @@ impl TestShardUpgradeEnv { } } } + + fn check_outgoing_receipts_reassigned(&self, resharding_type: &ReshardingType) { + tracing::debug!(target: "test", "checking outgoing receipts reassigned"); + let env = &self.env; + + // height 20 is after the resharding is finished + let num_shards = get_expected_shards_num(self.epoch_length, 20, resharding_type); + + // last height before resharding took place + let last_height_included = 10; + for client in &env.clients { + for shard_id in 0..num_shards { + check_outgoing_receipts_reassigned_impl( + client, + shard_id, + last_height_included, + resharding_type, + ); + } + } + } +} + +fn check_outgoing_receipts_reassigned_impl( + client: &Client, + shard_id: u64, + last_height_included: u64, + resharding_type: &ReshardingType, +) { + let chain = &client.chain; + let prev_block = chain.get_block_by_height(last_height_included).unwrap(); + let prev_block_hash = *prev_block.hash(); + + let outgoing_receipts = chain + .get_outgoing_receipts_for_shard(prev_block_hash, shard_id, last_height_included) + .unwrap(); + let shard_layout = + client.epoch_manager.get_shard_layout_from_prev_block(&prev_block_hash).unwrap(); + + match resharding_type { + ReshardingType::V1 => { + // In V0->V1 resharding the outgoing receipts should be reassigned + // to the receipt receiver's shard id. + for receipt in outgoing_receipts { + let receiver = receipt.receiver_id; + let receiver_shard_id = account_id_to_shard_id(&receiver, &shard_layout); + assert_eq!(receiver_shard_id, shard_id); + } + } + ReshardingType::V2 => { + // In V1->V2 resharding the outgoing receipts should be reassigned + // to the lowest index child of the parent shard. + // We can't directly check that here but we can check that the + // non-lowest-index shards are not assigned any receipts. + // We check elsewhere that no receipts are lost so this should be sufficient. + if shard_id == 4 { + assert!(outgoing_receipts.is_empty()); + } + } + } } /// Checks that account exists in the state after `block` is processed /// This function checks both state_root from chunk extra and state root from chunk header, if /// the corresponding chunk is included in the block fn check_account(env: &mut TestEnv, account_id: &AccountId, block: &Block) { + tracing::trace!(target: "test", ?account_id, block_height=block.header().height(), "checking account"); let prev_hash = block.header().prev_hash(); let shard_layout = env.clients[0].epoch_manager.get_shard_layout_from_prev_block(prev_hash).unwrap(); let shard_uid = account_id_to_shard_uid(account_id, &shard_layout); let shard_id = shard_uid.shard_id(); for (i, me) in env.validators.iter().enumerate() { - if env.clients[i].shard_tracker.care_about_shard(Some(me), prev_hash, shard_id, true) { - let state_root = *env.clients[i] - .chain - .get_chunk_extra(block.hash(), &shard_uid) - .unwrap() - .state_root(); - env.clients[i] + let client = &env.clients[i]; + let care_about_shard = + client.shard_tracker.care_about_shard(Some(me), prev_hash, shard_id, true); + if !care_about_shard { + continue; + } + let chunk_extra = &client.chain.get_chunk_extra(block.hash(), &shard_uid).unwrap(); + let state_root = *chunk_extra.state_root(); + client + .runtime_adapter + .query( + shard_uid, + &state_root, + block.header().height(), + 0, + prev_hash, + block.hash(), + block.header().epoch_id(), + &QueryRequest::ViewAccount { account_id: account_id.clone() }, + ) + .unwrap(); + + let chunk = &block.chunks()[shard_id as usize]; + if chunk.height_included() == block.header().height() { + client .runtime_adapter .query( shard_uid, - &state_root, + &chunk.prev_state_root(), block.header().height(), 0, - prev_hash, + block.header().prev_hash(), block.hash(), block.header().epoch_id(), &QueryRequest::ViewAccount { account_id: account_id.clone() }, ) .unwrap(); - - let chunk = &block.chunks()[shard_id as usize]; - if chunk.height_included() == block.header().height() { - env.clients[i] - .runtime_adapter - .query( - shard_uid, - &chunk.prev_state_root(), - block.header().height(), - 0, - block.header().prev_hash(), - block.hash(), - block.header().epoch_id(), - &QueryRequest::ViewAccount { account_id: account_id.clone() }, - ) - .unwrap(); - } } } } @@ -497,31 +620,53 @@ fn setup_genesis( num_validators: u64, initial_accounts: Vec, gas_limit: Option, + genesis_protocol_version: ProtocolVersion, ) -> Genesis { let mut genesis = Genesis::test(initial_accounts, num_validators); // No kickout, since we are going to test missing chunks genesis.config.chunk_producer_kickout_threshold = 0; genesis.config.epoch_length = epoch_length; - genesis.config.protocol_version = SIMPLE_NIGHTSHADE_PROTOCOL_VERSION - 1; + genesis.config.protocol_version = genesis_protocol_version; genesis.config.use_production_config = true; - if let Some(gas_limit) = gas_limit { genesis.config.gas_limit = gas_limit; } + // The block producer assignment often happens to be unlucky enough to not + // include one of the validators in the first epoch. When that happens the + // new protocol version gets only 75% of the votes which is lower that the + // default 80% threshold for upgrading. Delaying the upgrade also delays + // resharding and makes it harder to predict. The threshold is set slightly + // lower here to always ensure that upgrade takes place as soon as possible. + // This was not fun to debug. + genesis.config.protocol_upgrade_stake_threshold = Rational32::new(7, 10); + + let default_epoch_config = EpochConfig::from(&genesis.config); + let all_epoch_config = AllEpochConfig::new(true, default_epoch_config); + let epoch_config = all_epoch_config.for_protocol_version(genesis_protocol_version); + + genesis.config.shard_layout = epoch_config.shard_layout; + genesis.config.num_block_producer_seats_per_shard = + epoch_config.num_block_producer_seats_per_shard; + genesis.config.avg_hidden_validator_seats_per_shard = + epoch_config.avg_hidden_validator_seats_per_shard; + genesis } -// test some shard layout upgrade with some simple transactions to create accounts -#[test] -fn test_shard_layout_upgrade_simple() { +fn test_shard_layout_upgrade_simple_impl(resharding_type: ReshardingType) { init_test_logger(); + tracing::info!(target: "test", "test_shard_layout_upgrade_simple_impl starting"); + + let genesis_protocol_version = get_genesis_protocol_version(&resharding_type); + let target_protocol_version = get_target_protocol_version(&resharding_type); let mut rng = thread_rng(); // setup let epoch_length = 5; - let mut test_env = TestShardUpgradeEnv::new(epoch_length, 2, 2, 100, None); + let mut test_env = + TestShardUpgradeEnv::new(epoch_length, 2, 2, 100, None, genesis_protocol_version); test_env.set_init_tx(vec![]); let mut nonce = 100; @@ -543,45 +688,55 @@ fn test_shard_layout_upgrade_simple() { ) }; + // Transactions added for the first block with new shard layout will not be + // processed, that's a known issue for the shard upgrade implementation. It + // is because transaction pools are stored by shard id and we do not migrate + // transactions that are still in the pool at the end of the sharding + // upgrade. + let skip_heights = vec![2 * epoch_length + 1]; + // add transactions until after sharding upgrade finishes - // TODO(resharding) fix handling transactions in V1->V2 and add transactions - // for heights 3 * epoch_length to 6 * epoch_length for height in 2..3 * epoch_length { - test_env.set_tx_at_height( - height, - generate_create_accounts_txs(10, height != 2 * epoch_length + 1), - ); + let check_accounts = !skip_heights.contains(&height); + let txs = generate_create_accounts_txs(10, check_accounts); + test_env.set_tx_at_height(height, txs); } - let mut protocol_version = SIMPLE_NIGHTSHADE_PROTOCOL_VERSION; - for height in 1..2 * epoch_length { - debug!(target:"test", height, "step"); - test_env.step_impl(0., protocol_version, true); + for _ in 1..4 * epoch_length { + test_env.step_impl(0., target_protocol_version, &resharding_type); test_env.check_receipt_id_to_shard_id(); } - // If V2 is enabled then test resharding from V1 to V2 as well. - // This condition will be true: - // - in nightly build - always - V2 is enabled - // - in default build - once V2 is stabilized and rolled out - if SIMPLE_NIGHTSHADE_V2_PROTOCOL_VERSION <= PROTOCOL_VERSION { - protocol_version = SIMPLE_NIGHTSHADE_V2_PROTOCOL_VERSION; + test_env.check_tx_outcomes(false, skip_heights); + test_env.check_accounts(accounts_to_check.iter().collect()); + test_env.check_split_states_artifacts(); + test_env.check_outgoing_receipts_reassigned(&resharding_type); + tracing::info!(target: "test", "test_shard_layout_upgrade_simple_impl finished"); +} + +fn get_target_protocol_version(resharding_type: &ReshardingType) -> u32 { + match resharding_type { + ReshardingType::V1 => SIMPLE_NIGHTSHADE_PROTOCOL_VERSION, + ReshardingType::V2 => SIMPLE_NIGHTSHADE_V2_PROTOCOL_VERSION, } +} - for height in 2 * epoch_length..5 * epoch_length + 1 { - debug!(target:"test", height, "step"); - test_env.step_impl(0., protocol_version, true); - test_env.check_receipt_id_to_shard_id(); +fn get_genesis_protocol_version(resharding_type: &ReshardingType) -> u32 { + match resharding_type { + ReshardingType::V1 => SIMPLE_NIGHTSHADE_PROTOCOL_VERSION - 1, + ReshardingType::V2 => SIMPLE_NIGHTSHADE_V2_PROTOCOL_VERSION - 1, } +} - // transactions added for height = 2 * epoch_length + 1 will not be processed, that's a known - // issue for the shard upgrade implementation. It is because transaction pools are stored by - // shard id and we do not migrate transactions that are still in the pool at the end of the - // sharding upgrade - test_env.check_tx_outcomes(false, vec![2 * epoch_length + 1]); - test_env.check_accounts(accounts_to_check.iter().collect()); +#[test] +fn test_shard_layout_upgrade_simple_v1() { + test_shard_layout_upgrade_simple_impl(ReshardingType::V1); +} - test_env.check_split_states_artifacts(); +#[cfg(feature = "protocol_feature_simple_nightshade_v2")] +#[test] +fn test_shard_layout_upgrade_simple_v2() { + test_shard_layout_upgrade_simple_impl(ReshardingType::V2); } fn generate_create_accounts_txs( @@ -619,9 +774,10 @@ fn generate_create_accounts_txs( genesis_hash, ); if check_accounts { - accounts_to_check.push(account_id); + accounts_to_check.push(account_id.clone()); } *nonce += 1; + tracing::trace!(target: "test", ?account_id, tx=?tx.get_hash(), "adding create account tx"); return tx; } }) @@ -685,12 +841,12 @@ fn gen_cross_contract_transaction( account0.clone(), account1.clone(), &signer0, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "call_promise".to_string(), args: serde_json::to_vec(&data).unwrap(), gas: GAS_1, deposit: 0, - })], + }))], *block_hash, ) } @@ -698,8 +854,16 @@ fn gen_cross_contract_transaction( /// Return test_env and a map from tx hash to the new account that will be added by this transaction fn setup_test_env_with_cross_contract_txs( epoch_length: u64, + genesis_protocol_version: ProtocolVersion, ) -> (TestShardUpgradeEnv, HashMap) { - let mut test_env = TestShardUpgradeEnv::new(epoch_length, 4, 4, 100, Some(100_000_000_000_000)); + let mut test_env = TestShardUpgradeEnv::new( + epoch_length, + 4, + 4, + 100, + Some(100_000_000_000_000), + genesis_protocol_version, + ); let mut rng = thread_rng(); let genesis_hash = *test_env.env.clients[0].chain.genesis_block().hash(); @@ -793,37 +957,59 @@ fn setup_test_env_with_cross_contract_txs( // Test cross contract calls // This test case tests postponed receipts and delayed receipts -#[test] -fn test_shard_layout_upgrade_cross_contract_calls() { +fn test_shard_layout_upgrade_cross_contract_calls_impl(resharding_type: ReshardingType) { init_test_logger(); // setup let epoch_length = 5; + let genesis_protocol_version = get_genesis_protocol_version(&resharding_type); + let target_protocol_version = get_target_protocol_version(&resharding_type); - let (mut test_env, new_accounts) = setup_test_env_with_cross_contract_txs(epoch_length); + let (mut test_env, new_accounts) = + setup_test_env_with_cross_contract_txs(epoch_length, genesis_protocol_version); for _ in 1..5 * epoch_length { - test_env.step(0.); + test_env.step_impl(0., target_protocol_version, &resharding_type); test_env.check_receipt_id_to_shard_id(); } let successful_txs = test_env.check_tx_outcomes(false, vec![2 * epoch_length + 1]); - let new_accounts: Vec<_> = + let new_accounts = successful_txs.iter().flat_map(|tx_hash| new_accounts.get(tx_hash)).collect(); + test_env.check_accounts(new_accounts); test_env.check_split_states_artifacts(); } +// Test cross contract calls +// This test case tests postponed receipts and delayed receipts +#[test] +fn test_shard_layout_upgrade_cross_contract_calls_v1() { + test_shard_layout_upgrade_cross_contract_calls_impl(ReshardingType::V1); +} + +// Test cross contract calls +// This test case tests postponed receipts and delayed receipts +#[cfg(feature = "protocol_feature_simple_nightshade_v2")] +#[test] +fn test_shard_layout_upgrade_cross_contract_calls_v2() { + test_shard_layout_upgrade_cross_contract_calls_impl(ReshardingType::V2); +} + // Test cross contract calls // This test case tests when there are missing chunks in the produced blocks // This is to test that all the chunk management logic in sharding split is correct fn test_shard_layout_upgrade_missing_chunks(p_missing: f64) { init_test_logger(); + let resharding_type = ReshardingType::V1; + let genesis_protocol_version = get_genesis_protocol_version(&resharding_type); + // setup let epoch_length = 10; - let (mut test_env, new_accounts) = setup_test_env_with_cross_contract_txs(epoch_length); + let (mut test_env, new_accounts) = + setup_test_env_with_cross_contract_txs(epoch_length, genesis_protocol_version); // randomly dropping chunks at the first few epochs when sharding splits happens // make sure initial txs (deploy smart contracts) are processed succesfully diff --git a/integration-tests/src/tests/nearcore/mod.rs b/integration-tests/src/tests/nearcore/mod.rs index 19145d13676..80d5811916e 100644 --- a/integration-tests/src/tests/nearcore/mod.rs +++ b/integration-tests/src/tests/nearcore/mod.rs @@ -3,6 +3,7 @@ mod rpc_error_structs; mod rpc_nodes; mod run_nodes; mod stake_nodes; +mod state_snapshot; mod sync_nodes; mod sync_state_nodes; mod track_shards; diff --git a/integration-tests/src/tests/nearcore/state_snapshot.rs b/integration-tests/src/tests/nearcore/state_snapshot.rs new file mode 100644 index 00000000000..eaced459ca9 --- /dev/null +++ b/integration-tests/src/tests/nearcore/state_snapshot.rs @@ -0,0 +1,270 @@ +use near_chain::types::RuntimeAdapter; +use near_chain::{ChainGenesis, Provenance}; +use near_chain_configs::Genesis; +use near_client::test_utils::TestEnv; +use near_client::ProcessTxResponse; +use near_crypto::{InMemorySigner, KeyType, Signer}; +use near_epoch_manager::{EpochManager, EpochManagerHandle}; +use near_o11y::testonly::init_test_logger; +use near_primitives::block::Block; +use near_primitives::hash::CryptoHash; +use near_primitives::shard_layout::ShardUId; +use near_primitives::transaction::SignedTransaction; +use near_store::flat::FlatStorageManager; +use near_store::genesis::initialize_genesis_state; +use near_store::{ + config::TrieCacheConfig, test_utils::create_test_store, Mode, ShardTries, StateSnapshotConfig, + StoreConfig, TrieConfig, +}; +use near_store::{NodeStorage, Store}; +use nearcore::config::GenesisExt; +use nearcore::{NightshadeRuntime, NEAR_BASE}; +use std::path::PathBuf; +use std::sync::Arc; + +struct StateSnaptshotTestEnv { + home_dir: PathBuf, + hot_store_path: PathBuf, + state_snapshot_subdir: PathBuf, + shard_tries: ShardTries, +} + +impl StateSnaptshotTestEnv { + fn new( + home_dir: PathBuf, + hot_store_path: PathBuf, + state_snapshot_subdir: PathBuf, + store: &Store, + ) -> Self { + let trie_cache_config = TrieCacheConfig { + default_max_bytes: 50_000_000, + per_shard_max_bytes: Default::default(), + shard_cache_deletions_queue_capacity: 0, + }; + let trie_config = TrieConfig { + shard_cache_config: trie_cache_config.clone(), + view_shard_cache_config: trie_cache_config, + enable_receipt_prefetching: false, + sweat_prefetch_receivers: Vec::new(), + sweat_prefetch_senders: Vec::new(), + }; + let flat_storage_manager = FlatStorageManager::new(store.clone()); + let shard_uids = [ShardUId::single_shard()]; + let state_snapshot_config = StateSnapshotConfig::Enabled { + home_dir: home_dir.clone(), + hot_store_path: hot_store_path.clone(), + state_snapshot_subdir: state_snapshot_subdir.clone(), + compaction_enabled: true, + }; + let shard_tries = ShardTries::new_with_state_snapshot( + store.clone(), + trie_config, + &shard_uids, + flat_storage_manager, + state_snapshot_config, + ); + Self { home_dir, hot_store_path, state_snapshot_subdir, shard_tries } + } +} + +fn set_up_test_env_for_state_snapshots(store: &Store) -> StateSnaptshotTestEnv { + let home_dir = + tempfile::Builder::new().prefix("storage").tempdir().unwrap().path().to_path_buf(); + let hot_store_path = PathBuf::from("data"); + let state_snapshot_subdir = PathBuf::from("state_snapshot"); + StateSnaptshotTestEnv::new(home_dir, hot_store_path, state_snapshot_subdir, store) +} + +#[test] +// there's no entry in rocksdb for STATE_SNAPSHOT_KEY, maybe_open_state_snapshot should return error instead of panic +fn test_maybe_open_state_snapshot_no_state_snapshot_key_entry() { + init_test_logger(); + let store = create_test_store(); + let test_env = set_up_test_env_for_state_snapshots(&store); + let result = + test_env.shard_tries.maybe_open_state_snapshot(|_| Ok(vec![ShardUId::single_shard()])); + assert!(result.is_err()); +} + +#[test] +// there's no file present in the path for state snapshot, maybe_open_state_snapshot should return error instead of panic +fn test_maybe_open_state_snapshot_file_not_exist() { + init_test_logger(); + 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 result = + test_env.shard_tries.maybe_open_state_snapshot(|_| Ok(vec![ShardUId::single_shard()])); + assert!(result.is_err()); +} + +#[test] +// there's garbage in the path for state snapshot, maybe_open_state_snapshot should return error instead of panic +fn test_maybe_open_state_snapshot_garbage_snapshot() { + use std::fs::{create_dir_all, File}; + use std::io::Write; + use std::path::Path; + init_test_logger(); + 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 snapshot_path = ShardTries::get_state_snapshot_base_dir( + &snapshot_hash, + &test_env.home_dir, + &test_env.hot_store_path, + &test_env.state_snapshot_subdir, + ); + if let Some(parent) = Path::new(&snapshot_path).parent() { + create_dir_all(parent).unwrap(); + } + let mut file = File::create(snapshot_path).unwrap(); + // write some garbage + let data: Vec = vec![1, 2, 3, 4]; + file.write_all(&data).unwrap(); + + let result = + test_env.shard_tries.maybe_open_state_snapshot(|_| Ok(vec![ShardUId::single_shard()])); + assert!(result.is_err()); +} + +fn verify_make_snapshot( + state_snapshot_test_env: &StateSnaptshotTestEnv, + block_hash: CryptoHash, + block: &Block, +) -> Result<(), anyhow::Error> { + state_snapshot_test_env.shard_tries.make_state_snapshot( + &block_hash, + &vec![ShardUId::single_shard()], + block, + )?; + // check that make_state_snapshot does not panic or err out + // assert!(res.is_ok()); + let snapshot_path = ShardTries::get_state_snapshot_base_dir( + &block_hash, + &state_snapshot_test_env.home_dir, + &state_snapshot_test_env.hot_store_path, + &state_snapshot_test_env.state_snapshot_subdir, + ); + // check that the snapshot just made can be opened + state_snapshot_test_env + .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()?; + 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", + )); + } + // check that the stored snapshot in file system is an actual snapshot + let store_config = StoreConfig::default(); + let opener = NodeStorage::opener(&snapshot_path, false, &store_config, None); + let _storage = opener.open_in_mode(Mode::ReadOnly)?; + // check that there's only one snapshot at the parent directory of snapshot path + let parent_path = snapshot_path + .parent() + .ok_or(anyhow::anyhow!("{snapshot_path:?} needs to have a parent dir"))?; + let parent_path_result = std::fs::read_dir(parent_path)?; + if vec![parent_path_result.filter_map(Result::ok)].len() > 1 { + return Err(anyhow::Error::msg( + "there are more than 1 snapshot file in the snapshot parent directory", + )); + } + return Ok(()); +} + +fn delete_content_at_path(path: &str) -> std::io::Result<()> { + let metadata = std::fs::metadata(path)?; + if metadata.is_dir() { + std::fs::remove_dir_all(path)?; + } else { + std::fs::remove_file(path)?; + } + Ok(()) +} + +#[test] +fn test_make_state_snapshot() { + init_test_logger(); + let genesis = Genesis::test(vec!["test0".parse().unwrap()], 1); + let num_clients = 1; + let env_objects = (0..num_clients).map(|_|{ + let tmp_dir = tempfile::tempdir().unwrap(); + // Use default StoreConfig rather than NodeStorage::test_opener so we’re using the + // same configuration as in production. + let store = NodeStorage::opener(&tmp_dir.path(), false, &Default::default(), None) + .open() + .unwrap() + .get_hot_store(); + initialize_genesis_state(store.clone(), &genesis, Some(tmp_dir.path())); + let epoch_manager = EpochManager::new_arc_handle(store.clone(), &genesis.config); + let runtime = + NightshadeRuntime::test(tmp_dir.path(), store.clone(), &genesis.config, epoch_manager.clone()) + as Arc; + (tmp_dir, store, epoch_manager, runtime) + }).collect::, Arc)>>(); + + let stores = env_objects.iter().map(|x| x.1.clone()).collect::>(); + let epoch_managers = env_objects.iter().map(|x| x.2.clone()).collect::>(); + let runtimes = env_objects.iter().map(|x| x.3.clone()).collect::>(); + + let mut env = TestEnv::builder(ChainGenesis::test()) + .clients_count(env_objects.len()) + .stores(stores.clone()) + .epoch_managers(epoch_managers) + .runtimes(runtimes.clone()) + .use_state_snapshots() + .build(); + + let signer = InMemorySigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let genesis_block = env.clients[0].chain.get_block_by_height(0).unwrap(); + let genesis_hash = *genesis_block.hash(); + + let mut blocks = vec![]; + + let state_snapshot_test_env = set_up_test_env_for_state_snapshots(&stores[0]); + + for i in 1..=5 { + let new_account_id = format!("test_account_{i}"); + let nonce = i; + let tx = SignedTransaction::create_account( + nonce, + "test0".parse().unwrap(), + new_account_id.parse().unwrap(), + NEAR_BASE, + signer.public_key(), + &signer, + genesis_hash, + ); + assert_eq!(env.clients[0].process_tx(tx, false, false), ProcessTxResponse::ValidTx); + let block = env.clients[0].produce_block(i).unwrap().unwrap(); + blocks.push(block.clone()); + env.process_block(0, block.clone(), Provenance::PRODUCED); + assert!(verify_make_snapshot(&state_snapshot_test_env, *block.hash(), &block).is_ok()); + } + + // 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 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(); + assert!(verify_make_snapshot(&state_snapshot_test_env, head_block_hash, &head_block).is_ok()); + + // check that if the snapshot is deleted from file system while there's entry in DBCol::STATE_SNAPSHOT_KEY and write lock is nonempty, making a snpashot of the same hash will not write to the file system + let snapshot_hash = head.last_block_hash; + let snapshot_path = ShardTries::get_state_snapshot_base_dir( + &snapshot_hash, + &state_snapshot_test_env.home_dir, + &state_snapshot_test_env.hot_store_path, + &state_snapshot_test_env.state_snapshot_subdir, + ); + delete_content_at_path(snapshot_path.to_str().unwrap()).unwrap(); + assert!( + verify_make_snapshot(&state_snapshot_test_env, head.last_block_hash, &head_block).is_err() + ); + if let Ok(entries) = std::fs::read_dir(snapshot_path) { + assert_eq!(entries.count(), 0); + } +} diff --git a/integration-tests/src/tests/nearcore/sync_nodes.rs b/integration-tests/src/tests/nearcore/sync_nodes.rs index 8fe831801a7..bfb457ba805 100644 --- a/integration-tests/src/tests/nearcore/sync_nodes.rs +++ b/integration-tests/src/tests/nearcore/sync_nodes.rs @@ -66,7 +66,7 @@ fn add_blocks( epoch_id, next_epoch_id, None, - vec![Some( + vec![Some(Box::new( Approval::new( *prev.hash(), prev.header().height(), @@ -74,7 +74,7 @@ fn add_blocks( signer, ) .signature, - )], + ))], Ratio::from_integer(0), 0, 1000, diff --git a/integration-tests/src/tests/nearcore/sync_state_nodes.rs b/integration-tests/src/tests/nearcore/sync_state_nodes.rs index 47954f8805d..deb502709ab 100644 --- a/integration-tests/src/tests/nearcore/sync_state_nodes.rs +++ b/integration-tests/src/tests/nearcore/sync_state_nodes.rs @@ -2,6 +2,7 @@ use crate::test_helpers::heavy_test; use actix::{Actor, System}; use futures::{future, FutureExt}; use near_actix_test_utils::run_actix; +use near_chain::chain::ApplyStatePartsRequest; use near_chain::types::RuntimeAdapter; use near_chain::{ChainGenesis, Provenance}; use near_chain_configs::ExternalStorageLocation::Filesystem; @@ -14,12 +15,14 @@ use near_network::tcp; use near_network::test_utils::{convert_boot_nodes, wait_or_timeout, WaitOrTimeoutActor}; use near_o11y::testonly::{init_integration_logger, init_test_logger}; use near_o11y::WithSpanContextExt; +use near_primitives::shard_layout::ShardUId; use near_primitives::state_part::PartId; -use near_primitives::syncing::get_num_state_parts; +use near_primitives::syncing::{get_num_state_parts, StatePartKey}; use near_primitives::transaction::SignedTransaction; use near_primitives::utils::MaybeValidated; +use near_primitives_core::types::ShardId; use near_store::genesis::initialize_genesis_state; -use near_store::{NodeStorage, Store}; +use near_store::{DBCol, NodeStorage, Store}; use nearcore::{config::GenesisExt, load_test_config, start_with_config, NightshadeRuntime}; use std::ops::ControlFlow; use std::sync::{Arc, RwLock}; @@ -346,8 +349,6 @@ fn sync_empty_state() { Duration::from_millis(200); near2.client_config.max_block_production_delay = Duration::from_millis(400); - near2.client_config.state_fetch_horizon = - state_sync_horizon; near2.client_config.block_header_fetch_horizon = block_header_fetch_horizon; near2.client_config.block_fetch_horizon = @@ -479,7 +480,6 @@ fn sync_state_dump() { Duration::from_millis(300); near2.client_config.max_block_production_delay = Duration::from_millis(600); - near2.client_config.state_fetch_horizon = state_sync_horizon; near2.client_config.block_header_fetch_horizon = block_header_fetch_horizon; near2.client_config.block_fetch_horizon = block_fetch_horizon; @@ -544,7 +544,6 @@ fn sync_state_dump() { } #[test] -#[ignore] // Test that state sync behaves well when the chunks are absent at the end of the epoch. // The test actually fails and the code needs fixing. fn test_dump_epoch_missing_chunk_in_last_block() { @@ -552,7 +551,7 @@ fn test_dump_epoch_missing_chunk_in_last_block() { init_test_logger(); let epoch_length = 10; - for num_last_chunks_missing in 0..5 { + for num_last_chunks_missing in 0..6 { assert!(num_last_chunks_missing < epoch_length); let mut genesis = Genesis::test(vec!["test0".parse().unwrap(), "test1".parse().unwrap()], 1); @@ -659,7 +658,7 @@ fn test_dump_epoch_missing_chunk_in_last_block() { let state_root_node = state_sync_header.state_root_node(); let num_parts = get_num_state_parts(state_root_node.memory_usage); // Check that state parts can be obtained. - let state_parts: Vec<_> = (0..num_parts) + let state_sync_parts: Vec<_> = (0..num_parts) .map(|i| { // This should obviously not fail, aka succeed. env.clients[0].chain.get_state_response_part(0, i, sync_hash).unwrap() @@ -675,11 +674,69 @@ fn test_dump_epoch_missing_chunk_in_last_block() { 0, &state_root, PartId::new(i, num_parts), - &state_parts[i as usize], + &state_sync_parts[i as usize], &epoch_id, ) .unwrap(); } + + env.clients[1].chain.set_state_header(0, sync_hash, state_sync_header).unwrap(); + for i in 0..num_parts { + env.clients[1] + .chain + .set_state_part( + 0, + sync_hash, + PartId::new(i, num_parts), + &state_sync_parts[i as usize], + ) + .unwrap(); + } + let rt = Arc::clone(&env.clients[1].runtime_adapter); + let f = move |msg: ApplyStatePartsRequest| { + use borsh::BorshSerialize; + let store = rt.store(); + + let shard_id = msg.shard_uid.shard_id as ShardId; + for part_id in 0..msg.num_parts { + let key = StatePartKey(msg.sync_hash, shard_id, part_id).try_to_vec().unwrap(); + let part = store.get(DBCol::StateParts, &key).unwrap().unwrap(); + + rt.apply_state_part( + shard_id, + &msg.state_root, + PartId::new(part_id, msg.num_parts), + &part, + &msg.epoch_id, + ) + .unwrap(); + } + }; + env.clients[1].chain.schedule_apply_state_parts(0, sync_hash, num_parts, &f).unwrap(); + env.clients[1].chain.set_state_finalize(0, sync_hash, Ok(())).unwrap(); + let last_chunk_height = epoch_length - num_last_chunks_missing; + for height in 1..epoch_length { + if height < last_chunk_height { + assert!(env.clients[1] + .chain + .get_chunk_extra(blocks[height as usize].hash(), &ShardUId::single_shard()) + .is_err()); + } else { + let chunk_extra = env.clients[1] + .chain + .get_chunk_extra(blocks[height as usize].hash(), &ShardUId::single_shard()) + .unwrap(); + let expected_chunk_extra = env.clients[0] + .chain + .get_chunk_extra( + blocks[last_chunk_height as usize].hash(), + &ShardUId::single_shard(), + ) + .unwrap(); + // The chunk extra of the prev block of sync block should be the same as the node that it is syncing from + assert_eq!(chunk_extra, expected_chunk_extra); + } + } } }); } diff --git a/integration-tests/src/tests/runtime/sanity_checks.rs b/integration-tests/src/tests/runtime/sanity_checks.rs index 1475439cb3a..bce8e38dba5 100644 --- a/integration-tests/src/tests/runtime/sanity_checks.rs +++ b/integration-tests/src/tests/runtime/sanity_checks.rs @@ -251,11 +251,12 @@ fn test_sanity_used_gas() { // // In this test we account for this by setting `op_cost` to zero, but if future tests // change test WASM in significant ways, this approach may become incorrect. - near_primitives::config::ContractPrepareVersion::V0 - | near_primitives::config::ContractPrepareVersion::V1 => 0, + near_vm_runner::ContractPrepareVersion::V0 | near_vm_runner::ContractPrepareVersion::V1 => { + 0 + } // Gas accounting is precise and instructions executed between calls to the side-effectful // `used_gas` host function calls will be observbable. - near_primitives::config::ContractPrepareVersion::V2 => { + near_vm_runner::ContractPrepareVersion::V2 => { u64::from(runtime_config.wasm_config.regular_op_cost) } }; diff --git a/integration-tests/src/tests/standard_cases/mod.rs b/integration-tests/src/tests/standard_cases/mod.rs index 7218726efe3..d4100656990 100644 --- a/integration-tests/src/tests/standard_cases/mod.rs +++ b/integration-tests/src/tests/standard_cases/mod.rs @@ -35,7 +35,7 @@ use testlib::runtime_utils::{ const FUNCTION_CALL_AMOUNT: Balance = TESTING_INIT_BALANCE / 10; pub(crate) fn fee_helper(node: &impl Node) -> FeeHelper { - FeeHelper::new(RuntimeConfig::test().fees, node.genesis().config.min_gas_price) + FeeHelper::new(RuntimeConfig::test(), node.genesis().config.min_gas_price) } /// Adds given access key to the given account_id using signer2. @@ -404,9 +404,9 @@ pub fn trying_to_create_implicit_account(node: impl Node) { let cost = fee_helper.create_account_transfer_full_key_cost_fail_on_create_account() + fee_helper.gas_to_balance( - fee_helper.cfg.fee(ActionCosts::create_account).send_fee(false) + fee_helper.cfg().fee(ActionCosts::create_account).send_fee(false) + fee_helper - .cfg + .cfg() .fee(near_primitives::config::ActionCosts::add_full_access_key) .send_fee(false), ); @@ -1490,8 +1490,8 @@ fn check_trie_nodes_count( /// /// 1st receipt should count 6 db reads. /// 2nd and 3rd receipts should count 2 db and 4 memory reads, because for them first 4 nodes were already put into the -/// chunk cache. -pub fn test_chunk_nodes_cache_common_parent(node: impl Node, runtime_config: RuntimeConfig) { +/// accounting cache. +pub fn test_accounting_cache_common_parent(node: impl Node, runtime_config: RuntimeConfig) { let receipts: Vec = (0..3) .map(|i| { make_receipt( @@ -1510,14 +1510,14 @@ pub fn test_chunk_nodes_cache_common_parent(node: impl Node, runtime_config: Run check_trie_nodes_count(&node, &runtime_config, receipts, results, true); } -/// This test is similar to `test_chunk_nodes_cache_common_parent` but checks another trie structure: +/// This test is similar to `test_accounting_cache_common_parent` but checks another trie structure: /// /// --> (Value 1) /// (Extension) -> (Branch) -> (Extension) -> (Branch) |-> (Leaf) -> (Value 2) /// /// 1st receipt should count 5 db reads. /// 2nd receipt should count 2 db and 4 memory reads. -pub fn test_chunk_nodes_cache_branch_value(node: impl Node, runtime_config: RuntimeConfig) { +pub fn test_accounting_cache_branch_value(node: impl Node, runtime_config: RuntimeConfig) { let receipts: Vec = (0..2) .map(|i| { make_receipt( @@ -1535,23 +1535,23 @@ pub fn test_chunk_nodes_cache_branch_value(node: impl Node, runtime_config: Runt check_trie_nodes_count(&node, &runtime_config, receipts, results, true); } -/// This test is similar to `test_chunk_nodes_cache_common_parent` but checks another trie structure: +/// This test is similar to `test_accounting_cache_common_parent` but checks another trie structure: /// /// --> (Leaf) -> (Value 1) /// (Extension) -> (Branch) --> (Extension) -> (Branch) |-> (Leaf) -> (Value 2) /// |-> (Leaf) -> (Value 2) /// -/// Here we check that chunk cache is enabled *only during function calls execution*. +/// Here we check that accounting cache is enabled *only during function calls execution*. /// 1st receipt writes `Value 1` and should count 6 db reads. /// 2nd receipt deploys a new contract which *code* is the same as `Value 2`. But this value shouldn't be put into the -/// chunk cache. +/// accounting cache. /// 3rd receipt writes `Value 2` and should count 2 db and 4 memory reads. /// -/// We have checked manually that if chunk cache mode is not disabled, then the following scenario happens: -/// - 1st receipt enables chunk cache mode but doesn't disable it -/// - 2nd receipt triggers insertion of `Value 2` into the chunk cache -/// - 3rd receipt reads it from the chunk cache, so it incorrectly charges user for 1 db and 5 memory reads. -pub fn test_chunk_nodes_cache_mode(node: impl Node, runtime_config: RuntimeConfig) { +/// We have checked manually that if accounting cache mode is not disabled, then the following scenario happens: +/// - 1st receipt enables accounting cache mode but doesn't disable it +/// - 2nd receipt triggers insertion of `Value 2` into the accounting cache +/// - 3rd receipt reads it from the accounting cache, so it incorrectly charges user for 1 db and 5 memory reads. +pub fn test_accounting_cache_mode(node: impl Node, runtime_config: RuntimeConfig) { let receipts: Vec = vec![ make_receipt(&node, vec![make_write_key_value_action(vec![1], vec![1])], bob_account()), make_receipt( diff --git a/integration-tests/src/tests/standard_cases/runtime.rs b/integration-tests/src/tests/standard_cases/runtime.rs index becfe588e52..dc7c7750b31 100644 --- a/integration-tests/src/tests/standard_cases/runtime.rs +++ b/integration-tests/src/tests/standard_cases/runtime.rs @@ -318,24 +318,24 @@ fn test_contract_write_key_value_cost_runtime() { } #[test] -fn test_chunk_nodes_cache_same_common_parent() { +fn test_accounting_cache_same_common_parent() { let node = create_runtime_node(); let runtime_config = node.client.as_ref().read().unwrap().runtime_config.clone(); - test_chunk_nodes_cache_common_parent(node, runtime_config); + test_accounting_cache_common_parent(node, runtime_config); } #[test] -fn test_chunk_nodes_cache_branch_value_runtime() { +fn test_accounting_cache_branch_value_runtime() { let node = create_runtime_node(); let runtime_config = node.client.as_ref().read().unwrap().runtime_config.clone(); - test_chunk_nodes_cache_branch_value(node, runtime_config); + test_accounting_cache_branch_value(node, runtime_config); } #[test] -fn test_chunk_nodes_cache_mode_runtime() { +fn test_accounting_cache_mode_runtime() { let node = create_runtime_node(); let runtime_config = node.client.as_ref().read().unwrap().runtime_config.clone(); - test_chunk_nodes_cache_mode(node, runtime_config); + test_accounting_cache_mode(node, runtime_config); } #[test] diff --git a/integration-tests/src/tests/test_errors.rs b/integration-tests/src/tests/test_errors.rs index 8ee81b2edb7..3ebeec84ee8 100644 --- a/integration-tests/src/tests/test_errors.rs +++ b/integration-tests/src/tests/test_errors.rs @@ -15,6 +15,7 @@ use near_primitives::transaction::{ use near_primitives::version::PROTOCOL_VERSION; use nearcore::config::{GenesisExt, TESTING_INIT_BALANCE, TESTING_INIT_STAKE}; use nearcore::load_test_config; +use node_runtime::config::RuntimeConfig; use testlib::runtime_utils::{alice_account, bob_account}; fn start_node() -> ThreadNode { @@ -43,10 +44,10 @@ fn test_check_tx_error_log() { vec![ Action::CreateAccount(CreateAccountAction {}), Action::Transfer(TransferAction { deposit: 1_000 }), - Action::AddKey(AddKeyAction { + Action::AddKey(Box::new(AddKeyAction { public_key: signer.public_key.clone(), access_key: AccessKey::full_access(), - }), + })), ], block_hash, ); @@ -68,7 +69,7 @@ fn test_deliver_tx_error_log() { let runtime_config_store = RuntimeConfigStore::new(None); let runtime_config = runtime_config_store.get_config(PROTOCOL_VERSION); let fee_helper = testlib::fees_utils::FeeHelper::new( - runtime_config.fees.clone(), + RuntimeConfig::clone(&runtime_config), node.genesis().config.min_gas_price, ); let signer = @@ -83,10 +84,10 @@ fn test_deliver_tx_error_log() { vec![ Action::CreateAccount(CreateAccountAction {}), Action::Transfer(TransferAction { deposit: TESTING_INIT_BALANCE + 1 }), - Action::AddKey(AddKeyAction { + Action::AddKey(Box::new(AddKeyAction { public_key: signer.public_key.clone(), access_key: AccessKey::full_access(), - }), + })), ], block_hash, ); diff --git a/integration-tests/src/user/mod.rs b/integration-tests/src/user/mod.rs index d9095de9553..cc1da5b1a8f 100644 --- a/integration-tests/src/user/mod.rs +++ b/integration-tests/src/user/mod.rs @@ -5,7 +5,7 @@ use futures::{future::LocalBoxFuture, FutureExt}; use near_crypto::{PublicKey, Signer}; use near_jsonrpc_primitives::errors::ServerError; use near_primitives::account::AccessKey; -use near_primitives::delegate_action::{DelegateAction, NonDelegateAction, SignedDelegateAction}; +use near_primitives::action::delegate::{DelegateAction, NonDelegateAction, SignedDelegateAction}; use near_primitives::hash::CryptoHash; use near_primitives::receipt::Receipt; use near_primitives::test_utils::create_user_test_signer; @@ -144,12 +144,12 @@ pub trait User { self.sign_and_commit_actions( signer_id, contract_id, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: method_name.to_string(), args, gas, deposit, - })], + }))], ) } @@ -166,7 +166,10 @@ pub trait User { vec![ Action::CreateAccount(CreateAccountAction {}), Action::Transfer(TransferAction { deposit: amount }), - Action::AddKey(AddKeyAction { public_key, access_key: AccessKey::full_access() }), + Action::AddKey(Box::new(AddKeyAction { + public_key, + access_key: AccessKey::full_access(), + })), ], ) } @@ -180,7 +183,7 @@ pub trait User { self.sign_and_commit_actions( signer_id.clone(), signer_id, - vec![Action::AddKey(AddKeyAction { public_key, access_key })], + vec![Action::AddKey(Box::new(AddKeyAction { public_key, access_key }))], ) } @@ -192,7 +195,7 @@ pub trait User { self.sign_and_commit_actions( signer_id.clone(), signer_id, - vec![Action::DeleteKey(DeleteKeyAction { public_key })], + vec![Action::DeleteKey(Box::new(DeleteKeyAction { public_key }))], ) } @@ -207,8 +210,8 @@ pub trait User { signer_id.clone(), signer_id, vec![ - Action::DeleteKey(DeleteKeyAction { public_key: old_public_key }), - Action::AddKey(AddKeyAction { public_key: new_public_key, access_key }), + Action::DeleteKey(Box::new(DeleteKeyAction { public_key: old_public_key })), + Action::AddKey(Box::new(AddKeyAction { public_key: new_public_key, access_key })), ], ) } @@ -243,7 +246,7 @@ pub trait User { self.sign_and_commit_actions( signer_id.clone(), signer_id, - vec![Action::Stake(StakeAction { stake, public_key })], + vec![Action::Stake(Box::new(StakeAction { stake, public_key }))], ) } @@ -280,7 +283,7 @@ pub trait User { self.sign_and_commit_actions( relayer_id, signer_id, - vec![Action::Delegate(signed_delegate_action)], + vec![Action::Delegate(Box::new(signed_delegate_action))], ) } } diff --git a/nearcore/Cargo.toml b/nearcore/Cargo.toml index 5c4b3b3cf6d..9a2330304c2 100644 --- a/nearcore/Cargo.toml +++ b/nearcore/Cargo.toml @@ -120,13 +120,16 @@ protocol_feature_restrict_tla = [ "near-primitives/protocol_feature_restrict_tla", "node-runtime/protocol_feature_restrict_tla", ] +new_epoch_sync = [ + "near-client/new_epoch_sync" +] serialize_all_state_changes = ["near-store/serialize_all_state_changes"] nightly = [ "nightly_protocol", - "protocol_feature_restrict_tla", "protocol_feature_fix_contract_loading_cost", "protocol_feature_fix_staking_threshold", + "protocol_feature_restrict_tla", "protocol_feature_simple_nightshade_v2", "serialize_all_state_changes", "near-async/nightly", diff --git a/nearcore/res/example-config-gc.json b/nearcore/res/example-config-gc.json index 251e054aa1b..97addfeebee 100644 --- a/nearcore/res/example-config-gc.json +++ b/nearcore/res/example-config-gc.json @@ -78,7 +78,6 @@ }, "produce_empty_blocks": true, "block_fetch_horizon": 50, - "state_fetch_horizon": 5, "block_header_fetch_horizon": 50, "catchup_step_period": { "secs": 0, diff --git a/nearcore/res/example-config-no-gc.json b/nearcore/res/example-config-no-gc.json index 3239f65a91a..64b0ea07631 100644 --- a/nearcore/res/example-config-no-gc.json +++ b/nearcore/res/example-config-no-gc.json @@ -78,7 +78,6 @@ }, "produce_empty_blocks": true, "block_fetch_horizon": 50, - "state_fetch_horizon": 5, "block_header_fetch_horizon": 50, "catchup_step_period": { "secs": 0, diff --git a/nearcore/src/config.rs b/nearcore/src/config.rs index ae677c815e7..b963ff1f505 100644 --- a/nearcore/src/config.rs +++ b/nearcore/src/config.rs @@ -69,9 +69,6 @@ pub const MAX_BLOCK_WAIT_DELAY: u64 = 6_000; /// Horizon at which instead of fetching block, fetch full state. const BLOCK_FETCH_HORIZON: BlockHeightDelta = 50; -/// Horizon to step from the latest block when fetching state. -const STATE_FETCH_HORIZON: NumBlocks = 5; - /// Behind this horizon header fetch kicks in. const BLOCK_HEADER_FETCH_HORIZON: BlockHeightDelta = 50; @@ -181,6 +178,10 @@ fn default_view_client_threads() -> usize { 4 } +fn default_log_summary_period() -> Duration { + Duration::from_secs(10) +} + fn default_doomslug_step_period() -> Duration { Duration::from_millis(100) } @@ -213,8 +214,6 @@ pub struct Consensus { pub produce_empty_blocks: bool, /// Horizon at which instead of fetching block, fetch full state. pub block_fetch_horizon: BlockHeightDelta, - /// Horizon to step from the latest block when fetching state. - pub state_fetch_horizon: NumBlocks, /// Behind this horizon header fetch kicks in. pub block_header_fetch_horizon: BlockHeightDelta, /// Time between check to perform catchup. @@ -259,7 +258,6 @@ impl Default for Consensus { max_block_wait_delay: Duration::from_millis(MAX_BLOCK_WAIT_DELAY), produce_empty_blocks: true, block_fetch_horizon: BLOCK_FETCH_HORIZON, - state_fetch_horizon: STATE_FETCH_HORIZON, block_header_fetch_horizon: BLOCK_HEADER_FETCH_HORIZON, catchup_step_period: Duration::from_millis(CATCHUP_STEP_PERIOD), chunk_request_retry_period: Duration::from_millis(CHUNK_REQUEST_RETRY_PERIOD), @@ -308,6 +306,8 @@ pub struct Config { #[serde(skip_serializing_if = "Option::is_none")] pub save_trie_changes: Option, pub log_summary_style: LogSummaryStyle, + #[serde(default = "default_log_summary_period")] + pub log_summary_period: Duration, // Allows more detailed logging, for example a list of orphaned blocks. pub enable_multiline_logging: Option, /// Garbage collection configuration. @@ -377,6 +377,7 @@ impl Default for Config { archive: false, save_trie_changes: None, log_summary_style: LogSummaryStyle::Colored, + log_summary_period: default_log_summary_period(), gc: GCConfig::default(), epoch_sync_enabled: true, view_client_threads: default_view_client_threads(), @@ -502,7 +503,6 @@ impl Config { None } - #[allow(unused_variables)] pub fn set_rpc_addr(&mut self, addr: tcp::ListenerAddr) { #[cfg(feature = "json_rpc")] { @@ -658,14 +658,13 @@ impl NearConfig { .header_sync_expected_height_per_second, state_sync_timeout: config.consensus.state_sync_timeout, min_num_peers: config.consensus.min_num_peers, - log_summary_period: Duration::from_secs(10), + log_summary_period: config.log_summary_period, produce_empty_blocks: config.consensus.produce_empty_blocks, epoch_length: genesis.config.epoch_length, num_block_producer_seats: genesis.config.num_block_producer_seats, ttl_account_id_router: config.network.ttl_account_id_router, // TODO(1047): this should be adjusted depending on the speed of sync of state. block_fetch_horizon: config.consensus.block_fetch_horizon, - state_fetch_horizon: config.consensus.state_fetch_horizon, block_header_fetch_horizon: config.consensus.block_header_fetch_horizon, catchup_step_period: config.consensus.catchup_step_period, chunk_request_retry_period: config.consensus.chunk_request_retry_period, diff --git a/nearcore/src/entity_debug.rs b/nearcore/src/entity_debug.rs index 3dbd2b4c1bd..fb9ddbfccef 100644 --- a/nearcore/src/entity_debug.rs +++ b/nearcore/src/entity_debug.rs @@ -238,7 +238,7 @@ impl EntityDebugHandlerImpl { .copied() .chain(extension_nibbles.0.iter()) .collect::>(); - let data = trie.debug_get_value(&value)?; + let data = trie.retrieve_value(&value.hash)?; entity_data.entries.push(EntityDataEntry { name: "leaf_path".to_owned(), value: EntityDataValue::String(TriePath::nibbles_to_hex(&leaf_nibbles)), @@ -264,7 +264,7 @@ impl EntityDebugHandlerImpl { } } near_store::RawTrieNode::BranchWithValue(value, children) => { - let data = trie.debug_get_value(&value)?; + let data = trie.retrieve_value(&value.hash)?; entity_data.entries.push(EntityDataEntry { name: "leaf_path".to_owned(), value: EntityDataValue::String(TriePath::nibbles_to_hex( diff --git a/nearcore/src/lib.rs b/nearcore/src/lib.rs index a0c7ded01bf..95b4e8c37d9 100644 --- a/nearcore/src/lib.rs +++ b/nearcore/src/lib.rs @@ -375,7 +375,6 @@ pub fn start_with_config_and_synchronization( let hot_store = storage.get_hot_store(); - #[allow(unused_mut)] let mut rpc_servers = Vec::new(); let network_actor = PeerManagerActor::spawn( time::Clock::real(), diff --git a/nearcore/src/migrations.rs b/nearcore/src/migrations.rs index 43812efec3e..367f14fee74 100644 --- a/nearcore/src/migrations.rs +++ b/nearcore/src/migrations.rs @@ -112,6 +112,7 @@ impl<'a> near_store::StoreMigrator for Migrator<'a> { Ok(()) } 36 => near_store::migrations::migrate_36_to_37(store), + 37 => near_store::migrations::migrate_37_to_38(store), DB_VERSION.. => unreachable!(), } } diff --git a/nearcore/src/runtime/mod.rs b/nearcore/src/runtime/mod.rs index 7391b95c729..5df4c279af3 100644 --- a/nearcore/src/runtime/mod.rs +++ b/nearcore/src/runtime/mod.rs @@ -14,11 +14,11 @@ use near_epoch_manager::{EpochManagerAdapter, EpochManagerHandle}; use near_pool::types::PoolIterator; use near_primitives::account::{AccessKey, Account}; use near_primitives::challenge::ChallengesResult; +use near_primitives::config::ActionCosts; use near_primitives::config::ExtCosts; -use near_primitives::contract::ContractCode; use near_primitives::errors::{InvalidTxError, RuntimeError, StorageError}; use near_primitives::hash::{hash, CryptoHash}; -use near_primitives::receipt::Receipt; +use near_primitives::receipt::{DelayedReceiptIndices, Receipt}; use near_primitives::runtime::config_store::RuntimeConfigStore; use near_primitives::runtime::migration_data::{MigrationData, MigrationFlags}; use near_primitives::sandbox::state_patch::SandboxStatePatch; @@ -27,6 +27,7 @@ use near_primitives::shard_layout::{ }; use near_primitives::state_part::PartId; use near_primitives::transaction::SignedTransaction; +use near_primitives::trie_key::TrieKey; use near_primitives::types::validator_stake::ValidatorStakeIter; use near_primitives::types::{ AccountId, Balance, BlockHeight, EpochHeight, EpochId, EpochInfoProvider, Gas, MerkleHash, @@ -45,6 +46,7 @@ use near_store::{ }; use near_vm_runner::logic::CompiledContractCache; use near_vm_runner::precompile_contract; +use near_vm_runner::ContractCode; use node_runtime::adapter::ViewRuntimeAdapter; use node_runtime::state_viewer::TrieViewer; use node_runtime::{ @@ -549,7 +551,7 @@ impl NightshadeRuntime { .tries .get_trie_with_block_hash_for_shard_from_snapshot(shard_uid, *state_root, &prev_hash) .map_err(|err| Error::Other(err.to_string()))?; - let state_part = match snapshot_trie.get_trie_nodes_for_part_with_flat_storage(part_id, partial_state, nibbles_begin, nibbles_end, trie_with_state.storage.clone()) { + let state_part = match snapshot_trie.get_trie_nodes_for_part_with_flat_storage(part_id, partial_state, nibbles_begin, nibbles_end, &trie_with_state) { Ok(partial_state) => partial_state, Err(err) => { error!(target: "runtime", ?err, part_id.idx, part_id.total, %prev_hash, %state_root, %shard_id, "Can't get trie nodes for state part"); @@ -689,6 +691,30 @@ impl RuntimeAdapter for NightshadeRuntime { let runtime_config = self.runtime_config_store.get_config(current_protocol_version); + // To avoid limiting the throughput of the network, we want to include enough receipts to + // saturate the capacity of the chunk even in case when all of these receipts end up using + // the smallest possible amount of gas, which is at least the cost of execution of action + // receipt. + // Currently, the min execution cost is ~100 GGas and the chunk capacity is 1 PGas, giving + // a bound of at most 10000 receipts processed in a chunk. + let delayed_receipts_indices: DelayedReceiptIndices = + near_store::get(&state_update, &TrieKey::DelayedReceiptIndices)?.unwrap_or_default(); + let min_fee = runtime_config.fees.fee(ActionCosts::new_action_receipt).exec_fee(); + let new_receipt_count_limit = if min_fee > 0 { + // Round up to include at least one receipt. + let max_processed_receipts_in_chunk = (gas_limit + min_fee - 1) / min_fee; + // Allow at most 2 chunks worth of delayed receipts. This way under congestion, + // after processing a single chunk, we will still have at least 1 chunk worth of + // delayed receipts, ensuring the high throughput even if the next chunk producer + // does not include any receipts. + // This buffer size is a trade-off between the max queue size and system efficiency + // under congestion. + let delayed_receipt_count_limit = max_processed_receipts_in_chunk * 2; + delayed_receipt_count_limit.saturating_sub(delayed_receipts_indices.len()) as usize + } else { + usize::MAX + }; + // In general, we limit the number of transactions via send_fees. // However, as a second line of defense, we want to limit the byte size // of transaction as well. Rather than introducing a separate config for @@ -700,7 +726,10 @@ impl RuntimeAdapter for NightshadeRuntime { / (runtime_config.wasm_config.ext_costs.gas_cost(ExtCosts::storage_write_value_byte) + runtime_config.wasm_config.ext_costs.gas_cost(ExtCosts::storage_read_value_byte)); - while total_gas_burnt < transactions_gas_limit && total_size < size_limit { + while total_gas_burnt < transactions_gas_limit + && total_size < size_limit + && transactions.len() < new_receipt_count_limit + { if let Some(iter) = pool_iterator.next() { while let Some(tx) = iter.next() { num_checked_transactions += 1; @@ -832,7 +861,7 @@ impl RuntimeAdapter for NightshadeRuntime { is_new_chunk: bool, is_first_block_with_chunk_of_version: bool, ) -> Result { - let trie = Trie::from_recorded_storage(partial_storage, *state_root); + let trie = Trie::from_recorded_storage(partial_storage, *state_root, true); self.process_state_update( trie, shard_id, @@ -1048,28 +1077,40 @@ impl RuntimeAdapter for NightshadeRuntime { block_hash: &CryptoHash, state_roots: HashMap, next_epoch_shard_layout: &ShardLayout, - state_changes: StateChangesForSplitStates, + state_changes_for_split_states: StateChangesForSplitStates, ) -> Result, Error> { - let trie_changes = self.tries.apply_state_changes_to_split_states( + let trie_updates = self.tries.apply_state_changes_to_split_states( &state_roots, - state_changes, + state_changes_for_split_states, &|account_id| account_id_to_shard_uid(account_id, next_epoch_shard_layout), )?; - Ok(trie_changes - .into_iter() - .map(|(shard_uid, trie_changes)| ApplySplitStateResult { + let mut applied_split_state_results: Vec<_> = vec![]; + for (shard_uid, trie_update) in trie_updates { + let (_, trie_changes, state_changes) = trie_update.finalize()?; + // All state changes that are related to split state should have StateChangeCause as Resharding + // We do not want to commit the state_changes from resharding as they are already handled while + // processing parent shard + debug_assert!(state_changes.iter().all(|raw_state_changes| raw_state_changes + .changes + .iter() + .all(|state_change| state_change.cause == StateChangeCause::Resharding))); + let new_root = trie_changes.new_root; + let wrapped_trie_changes = WrappedTrieChanges::new( + self.get_tries(), shard_uid, - new_root: trie_changes.new_root, - trie_changes: WrappedTrieChanges::new( - self.get_tries(), - shard_uid, - trie_changes, - vec![], - *block_hash, - ), - }) - .collect()) + trie_changes, + state_changes, + *block_hash, + ); + applied_split_state_results.push(ApplySplitStateResult { + shard_uid, + new_root, + trie_changes: wrapped_trie_changes, + }); + } + + Ok(applied_split_state_results) } fn apply_state_part( @@ -1298,7 +1339,7 @@ mod test { sender.validator_id().clone(), sender.validator_id().clone(), &*signer, - vec![Action::Stake(StakeAction { stake, public_key: sender.public_key() })], + vec![Action::Stake(Box::new(StakeAction { stake, public_key: sender.public_key() }))], // runtime does not validate block history CryptoHash::default(), ) @@ -1361,6 +1402,7 @@ mod test { height, prev_hash: *prev_block_hash, }, + prev_block_with_changes: None, }, }; let new_store_update = flat_storage.add_delta(delta).unwrap(); @@ -2667,13 +2709,13 @@ mod test { .runtime .get_trie_for_shard(0, &env.head.prev_block_hash, Trie::EMPTY_ROOT, true) .unwrap(); - assert!(trie.flat_storage_chunk_view.is_some()); + assert!(trie.has_flat_storage_chunk_view()); let trie = env .runtime .get_view_trie_for_shard(0, &env.head.prev_block_hash, Trie::EMPTY_ROOT) .unwrap(); - assert!(trie.flat_storage_chunk_view.is_none()); + assert!(!trie.has_flat_storage_chunk_view()); } /// Check that querying trie and flat state gives the same result. diff --git a/nearcore/tests/economics.rs b/nearcore/tests/economics.rs index 197b07ac3e4..e5e05051e92 100644 --- a/nearcore/tests/economics.rs +++ b/nearcore/tests/economics.rs @@ -64,13 +64,12 @@ fn calc_total_supply(env: &mut TestEnv) -> u128 { fn test_burn_mint() { let genesis = build_genesis(); let mut env = setup_env(&genesis); - let transaction_costs = env.clients[0] + let config = env.clients[0] .runtime_adapter .get_protocol_config(&EpochId::default()) .unwrap() - .runtime_config - .fees; - let fee_helper = FeeHelper::new(transaction_costs, genesis.config.min_gas_price); + .runtime_config; + let fee_helper = FeeHelper::new(config, genesis.config.min_gas_price); let signer = InMemorySigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); let initial_total_supply = env.chain_genesis.total_supply; let genesis_hash = *env.clients[0].chain.genesis().hash(); diff --git a/neard/Cargo.toml b/neard/Cargo.toml index 4f330cb2277..d2d035ad24b 100644 --- a/neard/Cargo.toml +++ b/neard/Cargo.toml @@ -72,6 +72,7 @@ json_rpc = ["nearcore/json_rpc"] protocol_feature_fix_staking_threshold = ["nearcore/protocol_feature_fix_staking_threshold"] protocol_feature_simple_nightshade_v2 = ["nearcore/protocol_feature_simple_nightshade_v2"] serialize_all_state_changes = ["nearcore/serialize_all_state_changes"] +new_epoch_sync = ["nearcore/new_epoch_sync"] nightly = [ "nightly_protocol", diff --git a/neard/src/cli.rs b/neard/src/cli.rs index 5eab29ea758..a89f67da8b3 100644 --- a/neard/src/cli.rs +++ b/neard/src/cli.rs @@ -121,7 +121,7 @@ impl NeardCmd { cmd.run()?; } NeardSubCommand::FlatStorage(cmd) => { - cmd.run(&home_dir)?; + cmd.run(&home_dir, genesis_validation)?; } NeardSubCommand::ValidateConfig(cmd) => { cmd.run(&home_dir)?; diff --git a/nightly/pytest-sanity.txt b/nightly/pytest-sanity.txt index c8f521b6fb0..e93ffddde45 100644 --- a/nightly/pytest-sanity.txt +++ b/nightly/pytest-sanity.txt @@ -61,6 +61,12 @@ pytest --timeout=240 sanity/block_sync.py pytest --timeout=240 sanity/block_sync.py --features nightly pytest --timeout=10m sanity/block_sync_archival.py pytest --timeout=10m sanity/block_sync_archival.py --features nightly +pytest --timeout=120 sanity/block_sync_flat_storage.py +pytest --timeout=120 sanity/block_sync_flat_storage.py --features nightly +pytest --timeout=240 sanity/state_sync_epoch_boundary.py +pytest --timeout=240 sanity/state_sync_epoch_boundary.py --features nightly +pytest --timeout=240 sanity/state_sync_then_catchup.py +pytest --timeout=240 sanity/state_sync_then_catchup.py --features nightly pytest --timeout=240 sanity/validator_switch.py pytest --timeout=240 sanity/validator_switch.py --features nightly pytest --timeout=240 sanity/rpc_state_changes.py @@ -144,4 +150,4 @@ 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 \ No newline at end of file +pytest --timeout=600 sanity/split_storage.py --features nightly diff --git a/pytest/lib/cluster.py b/pytest/lib/cluster.py index 05c30ead51e..9be0fa9178a 100644 --- a/pytest/lib/cluster.py +++ b/pytest/lib/cluster.py @@ -321,6 +321,9 @@ def get_block(self, block_id, **kwargs): def get_block_by_height(self, block_height, **kwargs): return self.json_rpc('block', {'block_id': block_height}, **kwargs) + def get_final_block(self, **kwargs): + return self.json_rpc('block', {'finality': 'final'}, **kwargs) + def get_chunk(self, chunk_id): return self.json_rpc('chunk', [chunk_id]) @@ -812,12 +815,16 @@ def apply_config_changes(node_dir, client_config_change): # when None. allowed_missing_configs = ( 'archive', + 'consensus.state_sync_timeout', + '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', ) for k, v in client_config_change.items(): @@ -965,4 +972,4 @@ def get_binary_protocol_version(config) -> typing.Optional[int]: for i in range(n): if tokens[i] == "protocol" and i + 1 < n: return int(tokens[i + 1]) - return None \ No newline at end of file + return None diff --git a/pytest/tests/loadtest/locust/README.md b/pytest/tests/loadtest/locust/README.md index 19cefbcbdd6..af085a93bc3 100644 --- a/pytest/tests/loadtest/locust/README.md +++ b/pytest/tests/loadtest/locust/README.md @@ -1,9 +1,11 @@ # Locust based load testing -WIP: This is work in progress, motivation described in https://github.com/near/nearcore/issues/8999 - TLDR: Use [locust](https://locust.io/) to generate transactions against a Near chain and produce statistics. +Locust is a python library which we are using to send load to an RPC node. How +to set up the network under test is outside the scope of this document. This is +only about generating the load. + ## Install ```sh pip3 install locust @@ -11,6 +13,12 @@ pip3 install locust pip3 install -r pytest/requirements.txt ``` +*Note: You will need a working python3 / pip3 environment. While the code is +written in a backwards compatible way, modern OSs with modern python are +preferred. Completely independent of locust, you may run into problems with +PyOpenSSL with an old OS, `pip3 install pyopenssl --upgrade` may help if you see +error messages involving `X509_V_FLAG_CB_ISSUER_CHECK`.* + ## Run a first load test The load generator needs access to an account key with plenty of tokens. @@ -43,13 +51,17 @@ Each locust process will only use a single core. This leads to unwanted throttling on the load generator side if you try to use more than a couple hundred of users. +In the Locust UI, check the "Workers" tab to see CPU and memory usage. If this +approaches anything close to 100%, you should use more workers. + Luckily, locust has the ability to swarm the load generation across many processes. To use it, start one process with the `--master` argument and as many as you like with `--worker`. (If they run on different machines, you also need to provide `--master-host` and `--master-port`, if running on the same machine it will work automagically.) -Start the master +Start the master: + ```sh locust -H 127.0.0.1:3030 \ -f locustfiles/ft.py \ @@ -57,7 +69,8 @@ locust -H 127.0.0.1:3030 \ --master ``` -On the worker +On the worker: + ```sh # Increase soft limit of open files for the current OS user # (each locust user opens a separate socket = file) @@ -69,7 +82,9 @@ locust -H 127.0.0.1:3030 \ --worker ``` -Spawning N workers in a single shell: +Note that you have to start each worker process individually. But you can, for +example, use the `&` operator to spawn N workers in a single shell. + ```sh for i in {1..16} do @@ -80,9 +95,26 @@ do done ``` -Use `pkill -P $$` to stop all workers. +Use `pkill -P $$` to stop all workers spawned in the current shell. Stopping the master will also terminate all workers. +### Funding Key + +The funding key (`--funding-key=$KEY`) and the account associated with it are +used to pay for all on-chain transactions. When running in the distributed mode, +it will first create a funding account for each worker instance and those are +used to pay for transactions. + +As the load generator is currently configured, it will consume 1M NEAR from the +master funding account for each worker instance spawned. So, if you run with 64 +workers with 1000 users, it will consume 64M Near. It's recommended to have a +funding key with billions of Near. + +Also, the account name associated with this account should not bee too long. +Keep it below 20 characters, and it should be fine. The reason is that we create +sub accounts on this account, which become Sweat users. If the parent account +name is too long, children names will breach the 64 bytes max length. + # Available load types Different locust files can be passed as an argument to `locust` to run a specific load test. @@ -92,12 +124,46 @@ Currently supported load types: | load type | file | args | description | |---|---|---|---| -| Fungible Token | ft.py | `--fungible-token-wasm $WASM_PATH`
(`--num-ft-contracts $N`) | Creates `$N` FT contracts per worker, registers each user in one of them. Users transfer FTs between each other. | -| Social DB | social.py | `--social-db-wasm $WASM_PATH` | Creates a single instance of SocialDB and registers users to it. Users post messages and follow other users. (More workload TBD) | -| Congestion | congestion.py | `--congestion-wasm $WASM_PATH` | Creates a single instance of Congestion contract. Users run large and long transactions. | +| Fungible Token | ft.py | (`--fungible-token-wasm $WASM_PATH`)
(`--num-ft-contracts $N`) | Creates `$N` FT contracts per worker, registers each user in one of them. Users transfer FTs between each other. | +| Social DB | social.py | (`--social-db-wasm $WASM_PATH`) | Creates a single instance of SocialDB and registers users to it. Users post messages and follow other users. (More workload TBD) | +| Congestion | congestion.py | (`--congestion-wasm $WASM_PATH`) | Creates a single instance of Congestion contract. Users run large and long transactions. | +| Sweat (normal load) | sweat.py | (`--sweat-wasm $WASM_PATH`) | Creates a single instance of the SWEAT contract. A mix of FT transfers and batch minting with batch sizes comparable to mainnet observations in summer 2023. | +| Sweat (storage stress test) | sweat.py | `--tags=storage-stress-test`
(`--sweat-wasm $WASM_PATH`) | Creates a single instance of the SWEAT contract. Sends maximally large batches to mint more tokens, thereby touching many storage nodes per receipt. This load will take a while to initialize enough Sweat users on chain. | + +## Notes on Storage Stress Test + +The Sweat based storage stress test is special. While the other workloads send +innocent traffic with many assumptions, this test pushes the storage accesses +per chunk to the limit. As such, it is a bit more fragile. + +### Slow Start + +First, you will notice that for several minutes after start, it will only be +registering new accounts to the Sweat contract on chain. You will see these +requests show up as "Init FT Account" in the statistics tab of the Locust UI. + +To make sure you are not waiting forever, you want to have enough locust users +per worker instance. For example, 100 users in one worker will be 100 times +faster than 100 users distributed over 100 workers. + +Once enough accounts have been registered, large batches of users get new steps +added, which translates to new tokens being minted for them. You will see them +as `Sweat record batch (stress test)` requests. + +When you restart your workers, they reset their known registered user accounts. +Hence on restart they add new accounts again. And you have to wait again. To +avoid this, you can stop and restart tests from within the UI. This way, they +will remember the account list and start the next test immediately, without long +setup. + + +### Master Key Requirements -In the future, we might have multiple users per load type but for now there is a -one-to-one mapping from users to load type (and tag). +The `--funding-key` provided must always have enough balance to fund many users. +But this is even more extreme for this load, as we are creating many accounts +per worker. -If the table above seems outdated, list available classes using `locust ---funding-key . --list` and send a PR to fix it, please. +Also, the account name limits are even tighter for this load. Other loads +probably work with lengths up to 40 character, here it really has to be below 20 +characters or else we hit the log output limit when writing all the JSON events +for updated Sweat balances. diff --git a/pytest/tests/loadtest/locust/common/base.py b/pytest/tests/loadtest/locust/common/base.py index 0f61d85d3cc..d18f370375b 100644 --- a/pytest/tests/loadtest/locust/common/base.py +++ b/pytest/tests/loadtest/locust/common/base.py @@ -1,4 +1,4 @@ -from datetime import timedelta +from datetime import datetime, timedelta from locust import User, events, runners from retrying import retry import abc @@ -122,9 +122,10 @@ def __init__(self, self.balance = int(balance) @abc.abstractmethod - def args(self) -> dict: + def args(self) -> typing.Union[dict, typing.List[dict]]: """ Function call arguments to be serialized and sent with the call. + Return a single dict for `FunctionCall` but a list of dict for `MultiFunctionCall`. """ def sign_and_serialize(self, block_hash) -> bytes: @@ -137,6 +138,34 @@ def sender_account(self) -> Account: return self.sender +class MultiFunctionCall(FunctionCall): + """ + Batches multiple function calls into a single transaction. + """ + + def __init__(self, + sender: Account, + receiver_id: str, + method: str, + balance: int = 0): + super().__init__(sender, receiver_id, method, balance=balance) + + def sign_and_serialize(self, block_hash) -> bytes: + all_args = self.args() + gas = 300 * TGAS // len(all_args) + + def create_action(args): + return transaction.create_function_call_action( + self.method, + json.dumps(args).encode('utf-8'), gas, int(self.balance)) + + actions = [create_action(args) for args in all_args] + return transaction.sign_and_serialize_transaction( + self.receiver_id, self.sender.use_nonce(), actions, block_hash, + self.sender.key.account_id, self.sender.key.decoded_pk(), + self.sender.key.decoded_sk()) + + class Deploy(Transaction): def __init__(self, account, contract, name): @@ -159,7 +188,7 @@ def sender_account(self) -> Account: class CreateSubAccount(Transaction): - def __init__(self, sender, sub_key, balance: int = 50): + def __init__(self, sender, sub_key, balance: float = 50): super().__init__() self.sender = sender self.sub_key = sub_key @@ -177,6 +206,25 @@ def sender_account(self) -> Account: return self.sender +class AddFullAccessKey(Transaction): + + def __init__(self, parent: Account, new_key: key.Key): + super().__init__() + self.sender = parent + self.new_key = new_key + + def sign_and_serialize(self, block_hash) -> bytes: + action = transaction.create_full_access_key_action( + self.new_key.decoded_pk()) + return transaction.sign_and_serialize_transaction( + self.sender.key.account_id, self.sender.use_nonce(), + [action], block_hash, self.sender.key.account_id, + self.sender.key.decoded_pk(), self.sender.key.decoded_sk()) + + def sender_account(self) -> Account: + return self.sender + + class NearNodeProxy: """ Wrapper around a RPC node connection that tracks requests on locust. @@ -215,23 +263,14 @@ def send_tx_retry(self, tx: Transaction, locust_name) -> dict: ) time.sleep(0.25) - def send_tx(self, tx: Transaction, locust_name) -> dict: + def send_tx(self, tx: Transaction, locust_name: str) -> dict: """ Send a transaction and return the result, no retry attempted. """ - block_hash = base58.b58decode(self.node.get_latest_block().hash) + block_hash = self.final_block_hash() signed_tx = tx.sign_and_serialize(block_hash) - meta = { - "request_type": "near-rpc", - "name": locust_name, - "start_time": time.time(), - "response_time": 0, # overwritten later with end-to-end time - "response_length": 0, # overwritten later - "response": None, # overwritten later - "context": {}, # not used right now - "exception": None, # maybe overwritten later - } + meta = self.new_locust_metadata(locust_name) start_perf_counter = time.perf_counter() try: @@ -252,8 +291,8 @@ def send_tx(self, tx: Transaction, locust_name) -> dict: submit_response = submit_raw_response.json() # extract transaction ID from response, it should be "{ "result": "id...." }" if not "result" in submit_response: - meta["exception"] = RpcError(submit_response, - message="Didn't get a TX ID") + meta["exception"] = RpcError(message="Didn't get a TX ID", + details=submit_response) meta["response"] = submit_response.content else: tx.transaction_id = submit_response["result"] @@ -270,6 +309,22 @@ def send_tx(self, tx: Transaction, locust_name) -> dict: self.request_event.fire(**meta) return meta + def final_block_hash(self): + return base58.b58decode( + self.node.get_final_block()['result']['header']['hash']) + + def new_locust_metadata(self, locust_name: str): + return { + "request_type": "near-rpc", + "name": locust_name, + "start_time": time.time(), + "response_time": 0, # overwritten later with end-to-end time + "response_length": 0, # overwritten later + "response": None, # overwritten later + "context": {}, # not used right now + "exception": None, # maybe overwritten later + } + def post_json(self, method: str, params: typing.List[str]): j = { "method": method, @@ -303,7 +358,7 @@ def poll_tx_result(self, meta: dict, tx: Transaction): def account_exists(self, account_id: str) -> bool: return "error" not in self.node.get_account(account_id, do_assert=False) - def prepare_account(self, account: Account, parent: Account, balance: int, + def prepare_account(self, account: Account, parent: Account, balance: float, msg: str) -> bool: """ Creates the account if it doesn't exist and refreshes the nonce. @@ -315,6 +370,98 @@ def prepare_account(self, account: Account, parent: Account, balance: int, account.refresh_nonce(self.node) return exists + def prepare_accounts(self, + accounts: typing.List[Account], + parent: Account, + balance: float, + msg: str, + timeout: timedelta = timedelta(minutes=3)): + """ + Creates accounts if they don't exist and refreshes their nonce. + Accounts must share the parent account. + + This implementation attempts on-chain parallelization, hence it should + be faster than calling `prepare_account` in a loop. + + Note that error-handling in this variant isn't quite as smooth. Errors + that are only reported by the sync API of RPC nodes will not be caught + here. Instead, we do a best-effort retry and stop after a fixed timeout. + """ + + # To avoid blocking, each account goes though a FSM independently. + # + # FSM outline: + # + # [INIT] -----------------(account exists already)------------- + # | | + # V V + # [TO_CREATE] --(post tx)--> [INFLIGHT] --(poll result)--> [TO_REFRESH] + # ^ | | + # | | | + # |--(fail to submit)<-- (refresh) + # | + # V + # [DONE] + # + to_create: typing.List[Account] = [] + inflight: typing.List[Transaction, dict, Account] = [] + to_refresh: typing.List[Account] = [] + + for account in accounts: + if self.account_exists(account.key.account_id): + to_refresh.append(account) + else: + to_create.append(account) + + block_hash = self.final_block_hash() + start = datetime.now() + while len(to_create) + len(to_refresh) + len(inflight) > 0: + logger.info( + f"preparing {len(accounts)} accounts, {len(to_create)} to create, {len(to_refresh)} to refresh, {len(inflight)} inflight" + ) + if start + timeout < datetime.now(): + raise SystemExit("Account preparation timed out") + try_again = [] + for account in to_create: + meta = self.new_locust_metadata(msg) + tx = CreateSubAccount(parent, account.key, balance=balance) + signed_tx = tx.sign_and_serialize(block_hash) + submit_raw_response = self.post_json( + "broadcast_tx_async", + [base64.b64encode(signed_tx).decode('utf8')]) + meta["response_length"] = len(submit_raw_response.text) + submit_response = submit_raw_response.json() + if not "result" in submit_response: + # something failed, let's not block, just try again later + logger.debug( + f"couldn't submit account creation TX, got {submit_raw_response.text}" + ) + try_again.append(account) + else: + tx.transaction_id = submit_response["result"] + inflight.append((tx, meta, account)) + to_create = try_again + + # while requests are processed on-chain, refresh nonces for existing accounts + for account in to_refresh: + account.refresh_nonce(self.node) + to_refresh.clear() + + # poll all pending requests + for tx, meta, account in inflight: + # Using retrying lib here to poll until a response is ready. + # This is blocking on a single request, but we expect requests + # to be processed in order so it shouldn't matter. + self.poll_tx_result(meta, tx) + meta["response_time"] = (time.time() - + meta["start_time"]) * 1000 + to_refresh.append(account) + # Locust tracking + self.request_event.fire(**meta) + inflight.clear() + + logger.info(f"done preparing {len(accounts)} accounts") + class NearUser(User): abstract = True @@ -371,6 +518,10 @@ def send_tx_retry(self, class NearError(Exception): def __init__(self, message, details): + """ + The `message` is used in locust to aggregate errors and is also displayed in the UI. + The `details` are logged as additional information in the console. + """ self.message = message self.details = details super().__init__(message) @@ -378,8 +529,8 @@ def __init__(self, message, details): class RpcError(NearError): - def __init__(self, error, message="RPC returned an error"): - super().__init__(message, error) + def __init__(self, message="RPC returned an error", details=None): + super().__init__(message, details) class TxUnknownError(RpcError): @@ -388,7 +539,7 @@ def __init__( self, message="RPC does not know the result of this TX, probably it is not executed yet" ): - super().__init__(message) + super().__init__(message=message) class InvalidNonceError(RpcError): @@ -399,6 +550,8 @@ def __init__( ak_nonce, ): super().__init__( + message="Nonce too small", + details= f"Tried to use nonce {used_nonce} but access key nonce is {ak_nonce}" ) self.ak_nonce = ak_nonce @@ -453,7 +606,7 @@ def evaluate_rpc_result(rpc_result): raise InvalidNonceError( err_description["InvalidNonce"]["tx_nonce"], err_description["InvalidNonce"]["ak_nonce"]) - raise RpcError(rpc_result["error"]) + raise RpcError(details=rpc_result["error"]) result = rpc_result["result"] transaction_outcome = result["transaction_outcome"] @@ -571,12 +724,16 @@ def do_on_locust_init(environment): if isinstance(environment.runner, runners.MasterRunner): num_funding_accounts = environment.parsed_options.max_workers funding_balance = 10000 * NearUser.INIT_BALANCE - # TODO: Create accounts in parallel - for id in range(num_funding_accounts): + + def create_account(id): account_id = f"funds_worker_{id}.{master_funding_account.key.account_id}" - worker_key = key.Key.from_seed_testonly(account_id) - node.prepare_account(Account(worker_key), master_funding_account, - funding_balance, "create funding account") + return Account(key.Key.from_seed_testonly(account_id)) + + funding_accounts = [ + create_account(id) for id in range(num_funding_accounts) + ] + node.prepare_accounts(funding_accounts, master_funding_account, + funding_balance, "create funding account") funding_account = master_funding_account elif isinstance(environment.runner, runners.WorkerRunner): worker_id = environment.runner.worker_index diff --git a/pytest/tests/loadtest/locust/common/ft.py b/pytest/tests/loadtest/locust/common/ft.py index 4572f1c30f8..9630638f5c1 100644 --- a/pytest/tests/loadtest/locust/common/ft.py +++ b/pytest/tests/loadtest/locust/common/ft.py @@ -1,4 +1,5 @@ import random +import string import sys import pathlib import typing @@ -26,10 +27,13 @@ def install(self, node: NearNodeProxy, parent: Account): Deploy and initialize the contract on chain. The account is created if it doesn't exist yet. """ - node.prepare_account(self.account, parent, FTContract.INIT_BALANCE, - "create contract account") - node.send_tx_retry(Deploy(self.account, self.code, "FT"), "deploy ft") - self.init_contract(node) + existed = node.prepare_account(self.account, parent, + FTContract.INIT_BALANCE, + "create contract account") + if not existed: + node.send_tx_retry(Deploy(self.account, self.code, "FT"), + "deploy ft") + self.init_contract(node) def init_contract(self, node: NearNodeProxy): node.send_tx_retry(InitFT(self.account), "init ft") @@ -44,6 +48,14 @@ def register_user(self, user: NearUser): locust_name="FT Funding") self.registered_users.append(user.account_id) + def register_passive_user(self, node: NearNodeProxy, account: Account): + """ + Passive users are only used as receiver, not as signer. + """ + node.send_tx_retry(InitFTAccount(self.account, account), + locust_name="Init FT Account") + self.registered_users.append(account.key.account_id) + def random_receiver(self, sender: str) -> str: return self.random_receivers(sender, 1)[0] @@ -57,6 +69,37 @@ def random_receivers(self, sender: str, num) -> typing.List[str]: map(lambda a: a.replace(sender, self.ft_distributor.key.account_id), receivers)) + def create_passive_users(self, + num: int, + node: NearNodeProxy, + parent: Account, + max_account_id_len=64): + """ + Create on-chain accounts and register them as FT users. + Note that these are not locust users and they are not able to sign + transactions. They are only used as targets of transactions and as a + side-effect they also increase the state size of the contract. + """ + prefix_len = max_account_id_len - len(parent.key.account_id) - 1 + assert prefix_len > 4, f"user key {parent.key.account_id} is too long" + chars = string.ascii_lowercase + string.digits + + def create_account(): + prefix = ''.join(random.choices(chars, k=prefix_len)) + account_id = f"{prefix}.{parent.key.account_id}" + return Account(key.Key.from_seed_testonly(account_id)) + + accounts = [create_account() for _ in range(num)] + node.prepare_accounts(accounts, + parent, + balance=0.3, + msg="create passive user") + # TODO: this could also be done in parallel, actually in very simple + # ways since there are no nonce conflicts (transactions are signed by + # different users) + for account in accounts: + self.register_passive_user(node, account) + class TransferFT(FunctionCall): diff --git a/pytest/tests/loadtest/locust/common/sweat.py b/pytest/tests/loadtest/locust/common/sweat.py index ff60c8cd434..b3b3335cdec 100644 --- a/pytest/tests/loadtest/locust/common/sweat.py +++ b/pytest/tests/loadtest/locust/common/sweat.py @@ -113,10 +113,11 @@ def args(self) -> dict: } -class SweatMintBatch(FunctionCall): +class SweatMintBatch(MultiFunctionCall): """ A call to `record_batch`. Mints new tokens for walked steps for a batch of users. + Might get split into multiple function calls to avoid log output limits. """ def __init__(self, sweat_id: str, oracle: Account, @@ -124,8 +125,16 @@ def __init__(self, sweat_id: str, oracle: Account, super().__init__(oracle, sweat_id, "record_batch") self.recipient_step_pairs = recipient_step_pairs - def args(self) -> dict: - return {"steps_batch": self.recipient_step_pairs} + def args(self) -> typing.List[dict]: + # above a threshold, we hit the log output limit of 16kB + # this depends a bit on the exact account id names + chunk_len = 150 + chunks = [ + self.recipient_step_pairs[s:s + chunk_len] + for s in range(0, len(self.recipient_step_pairs), chunk_len) + ] + + return [{"steps_batch": chunk} for chunk in chunks] @events.init.add_listener @@ -157,12 +166,18 @@ def on_locust_init(environment, **kwargs): # on master, register oracles for workers if isinstance(environment.runner, locust.runners.MasterRunner): num_oracles = int(environment.parsed_options.max_workers) - # TODO: Add oracles in parallel - for worker_id in range(num_oracles): - id = worker_oracle_id(worker_id, run_id, funding_account) - worker_oracle = Account(key.Key.from_seed_testonly(id)) - node.prepare_account(worker_oracle, funding_account, 100000, - "create contract account") + oracle_accounts = [ + Account( + key.Key.from_seed_testonly( + worker_oracle_id(id, run_id, + environment.master_funding_account))) + for id in range(num_oracles) + ] + node.prepare_accounts(oracle_accounts, + environment.master_funding_account, 100000, + "create contract account") + for oracle in oracle_accounts: + id = oracle.key.account_id environment.sweat.register_oracle(node, id) environment.sweat.top_up(node, id) diff --git a/pytest/tests/loadtest/locust/locustfiles/sweat.py b/pytest/tests/loadtest/locust/locustfiles/sweat.py index 2ef1809b16d..863950e2683 100644 --- a/pytest/tests/loadtest/locust/locustfiles/sweat.py +++ b/pytest/tests/loadtest/locust/locustfiles/sweat.py @@ -11,11 +11,11 @@ - Periodic batches that adds steps (mints new tokens) """ -from common.sweat import RecipientSteps, SweatMintBatch +from common.sweat import RecipientSteps, SweatContract, SweatMintBatch from common.ft import TransferFT -from common.base import NearUser -from locust import between, task -from configured_logger import new_logger +from common.base import Account, AddFullAccessKey, NearUser +from locust import between, tag, task +import copy import logging import pathlib import random @@ -23,6 +23,9 @@ sys.path.append(str(pathlib.Path(__file__).resolve().parents[4] / 'lib')) +from configured_logger import new_logger +import key + logger = new_logger(level=logging.WARN) @@ -35,28 +38,66 @@ class SweatUser(NearUser): """ wait_time = between(1, 3) # random pause between transactions - @task + @task(3) def ft_transfer(self): receiver = self.sweat.random_receiver(self.account_id) tx = TransferFT(self.sweat.account, self.account, receiver) self.send_tx(tx, locust_name="Sweat transfer") - @task - def record_batch(self): + @task(1) + def record_single_batch(self): rng = random.Random() + # just around the log limit batch_size = min(rng.randint(100, 150), len(self.sweat.registered_users)) receivers = self.sweat.random_receivers(self.account_id, batch_size) - tx = SweatMintBatch( - self.sweat.account.key.account_id, self.sweat.oracle, [ - RecipientSteps(account_id, steps=rng.randint(1000, 3000)) - for account_id in receivers - ]) + tx = SweatMintBatch(self.sweat.account.key.account_id, self.oracle, [ + RecipientSteps(account_id, steps=rng.randint(1000, 3000)) + for account_id in receivers + ]) self.send_tx(tx, locust_name="Sweat record batch") + @tag("storage-stress-test") + @task + def record_batch_of_large_batches(self): + # create more sweat users to allow for a decent record_batch size + while len(self.sweat.registered_users) < 1000: + # creating 20 accounts in parallel, about 200 should fit in a chunk + # but we don't want to assume we get to use all the chunk space for + # ourself. Also, other SweatUsers will be in the same loop and at + # some point the local CPU becomes a bottleneck, too. + self.sweat.create_passive_users( + 20, + self.node, + self.account, + # protocol enforced max length is 64 but we want shorter names to + # not hit the log limits too soon + max_account_id_len=48) + + rng = random.Random() + # just around 300Tgas + batch_size = rng.randint(700, 750) + receivers = self.sweat.random_receivers(self.account_id, batch_size) + tx = SweatMintBatch( + self.sweat.account.key.account_id, self.oracle, + [[account_id, rng.randint(1000, 3000)] for account_id in receivers]) + self.send_tx(tx, locust_name="Sweat record batch (stress test)") + def on_start(self): super().on_start() + # We have one oracle account per worker. Sharing a single access key + # means potential conflicts in nonces when we mint new tokens through + # batches. Hence, let's add a new access key to the oracle account for + # each sweat user. self.sweat = self.environment.sweat + oracle = self.environment.sweat.oracle + user_oracle_key = key.Key.from_random(oracle.key.account_id) + self.send_tx_retry(AddFullAccessKey(oracle, user_oracle_key), + "add user key to oracle") + + self.oracle = Account(user_oracle_key) + self.oracle.refresh_nonce(self.node.node) + self.sweat.register_user(self) logger.debug( f"{self.account_id} ready to use Sweat contract {self.sweat.account.key.account_id}" diff --git a/pytest/tests/mocknet/README.md b/pytest/tests/mocknet/README.md index 09faa1b71bf..bd816d03038 100644 --- a/pytest/tests/mocknet/README.md +++ b/pytest/tests/mocknet/README.md @@ -2,13 +2,10 @@ Mirror transactions from a given network into a custom mocktest network and add 1. Setup a custom mocknet network following the README in the `provisioning/terraform/network/mocknet/mirror/` directory of the [near-ops](https://github.com/near/near-ops) repo. - you'll need the `unique_id`, `chain_id`, and `start_height` from this setup when running the mirror.py test script in 2. -2. Run `python3 tests/mocknet/mirror.py --chain-id {chain_id} --start-height {start_height} --unique-id {unique_id} setup` replacing the `{}`s with appropriate values from the `nearcore/pytest` directory. -- This may take a bit to setup, so if you get a Broken pipe make sure to complete the following steps: -- Get the instances associated with your project: `gcloud --project near-mocknet compute instances list | grep {unique_id}` -- ssh into the instances and check `/home/ubuntu/.near/logs/amend-genesis.txt` on the nodes to make sure there's nothing bad in there -- then also run `du -sh /home/ubuntu/.near/records.json` on the validators and `du -sh /home/ubuntu/.near/target/records.json` on the traffic generator. If it's 27 GB (and there's no neard process when you run `ps -C neard`) it should be done. -- Run `python3 tests/mocknet/mirror.py --chain-id {chain_id} --start-height {start_height} --unique-id {unique_id} make-backups` replacing the `{}`s with appropriate values. This step will take >12 hours to run (shouldn't be >24 hours). -- If you get an ssh connection reset or other disconnection error while running the previous command, re-run the command or ssh into the instances and run `top` command to see if the neard is using a lot of CPU to make backups. -3. Run `python3 tests/mocknet/mirror.py --chain-id {chain_id} --start-height {start_height} --unique-id {unique_id} --start-traffic` replacing the `{}`s with appropriate values +2. Run `python3 tests/mocknet/mirror.py --chain-id {chain_id} --start-height {start_height} --unique-id {unique_id} init-neard-runner`, replacing the `{}`s with appropriate values from the `nearcore/pytest` directory. This starts a helper program on each node that will be in charge of the test state and neard process. +3. Run `python3 tests/mocknet/mirror.py --chain-id {chain_id} --start-height {start_height} --unique-id {unique_id} new-test`. This will take a few hours. +4. Run `python3 tests/mocknet/mirror.py --chain-id {chain_id} --start-height {start_height} --unique-id {unique_id} start-traffic` replacing the `{}`s with appropriate values 4. Monitoring - See metrics on grafana mocknet https://grafana.near.org/d/jHbiNgSnz/mocknet?orgId=1&refresh=30s&var-chain_id=All&var-node_id=.*unique_id.*&var-account_id=All replacing the "unique_id" with the value from earlier + +If there's ever a problem with the neard runners on each node, for example if you get a connection error running the `status` command, run the `restart-neard-runner` command to restart them, which should be safe to do. diff --git a/pytest/tests/mocknet/helpers/neard_runner.py b/pytest/tests/mocknet/helpers/neard_runner.py index 798b7a7a593..9f2704e9cae 100644 --- a/pytest/tests/mocknet/helpers/neard_runner.py +++ b/pytest/tests/mocknet/helpers/neard_runner.py @@ -1,4 +1,8 @@ +# TODO: reimplement this logic using standard tooling like systemd instead of relying on this +# python script to handle neard process management. + import argparse +from enum import Enum import fcntl import json import jsonrpc @@ -6,7 +10,9 @@ import os import psutil import requests +import shutil import signal +import socket import subprocess import sys import threading @@ -43,8 +49,16 @@ class JSONHandler(http.server.BaseHTTPRequestHandler): def __init__(self, request, client_address, server): self.dispatcher = jsonrpc.Dispatcher() + self.dispatcher.add_method(server.neard_runner.do_new_test, + name="new_test") + self.dispatcher.add_method(server.neard_runner.do_network_init, + name="network_init") + self.dispatcher.add_method(server.neard_runner.do_update_config, + name="update_config") + self.dispatcher.add_method(server.neard_runner.do_ready, name="ready") self.dispatcher.add_method(server.neard_runner.do_start, name="start") self.dispatcher.add_method(server.neard_runner.do_stop, name="stop") + self.dispatcher.add_method(server.neard_runner.do_reset, name="reset") super().__init__(request, client_address, server) def do_GET(self): @@ -83,51 +97,56 @@ def __init__(self, addr, neard_runner): super().__init__(addr, JSONHandler) +class TestState(Enum): + NONE = 1 + AWAITING_NETWORK_INIT = 2 + AMEND_GENESIS = 3 + STATE_ROOTS = 4 + RUNNING = 5 + STOPPED = 6 + RESETTING = 7 + + class NeardRunner: def __init__(self, args): self.home = args.home self.neard_home = args.neard_home self.neard_logs_dir = args.neard_logs_dir - with open(os.path.join(self.home, 'config.json'), 'r') as f: + try: + os.mkdir(self.neard_logs_dir) + except FileExistsError: + pass + with open(self.home_path('config.json'), 'r') as f: self.config = json.load(f) self.neard = None + self.num_restarts = 0 + self.last_start = time.time() # self.data will contain data that we want to persist so we can # start where we left off if this process is killed and restarted # for now we save info on the neard binaries (their paths and epoch # heights where we want to run them), a bool that tells whether neard # should be running, and info on the currently running neard process try: - with open(os.path.join(self.home, 'data.json'), 'r') as f: + with open(self.home_path('data.json'), 'r') as f: self.data = json.load(f) self.data['binaries'].sort(key=lambda x: x['epoch_height']) except FileNotFoundError: self.data = { 'binaries': [], - 'run': False, - 'neard_process': { - 'pid': None, - # we save the create_time so we can tell if the process with pid equal - # to the one we saved is the same process. It's not that likely, but - # if we don't do this (or maybe something else like it), then it's possble - # that if this process is killed and restarted and we see a process with that PID, - # it could actually be a different process that was started later after neard exited - 'create_time': 0, - 'path': None, - } + 'neard_process': None, + 'current_neard_path': None, + 'state': TestState.NONE.value, } - # here we assume the home dir has already been inited. In the future we will want to - # initialize it in this program - with open(os.path.join(self.neard_home, 'config.json'), 'r') as f: - config = json.load(f) - self.neard_addr = config['rpc']['addr'] - # protects self.data, and its representation on disk, because both the rpc server - # and the loop that checks if we should upgrade the binary after a new epoch touch - # it concurrently + # protects self.data, and its representation on disk, + # because both the rpc server and the main loop touch them concurrently self.lock = threading.Lock() + def is_traffic_generator(self): + return self.config.get('is_traffic_generator', False) + def save_data(self): - with open(os.path.join(self.home, 'data.json'), 'w') as f: + with open(self.home_path('data.json'), 'w') as f: json.dump(self.data, f) def parse_binaries_config(self): @@ -161,25 +180,25 @@ def parse_binaries_config(self): binaries.append({ 'url': b['url'], 'epoch_height': b['epoch_height'], - 'system_path': os.path.join(self.home, 'binaries', f'neard{i}') + 'system_path': self.home_path('binaries', f'neard{i}') }) return binaries + def reset_current_neard_path(self): + self.data['current_neard_path'] = self.data['binaries'][0][ + 'system_path'] + # tries to download the binaries specified in config.json, saving them in $home/binaries/ def download_binaries(self): binaries = self.parse_binaries_config() try: - os.mkdir(os.path.join(self.home, 'binaries')) + os.mkdir(self.home_path('binaries')) except FileExistsError: pass with self.lock: num_binaries_saved = len(self.data['binaries']) - neard_process = self.data['neard_process'] - if neard_process['path'] is None: - neard_process['path'] = binaries[0]['system_path'] - self.save_data() # for now we assume that the binaries recorded in data.json as having been # dowloaded are still valid and were not touched. Also this assumes that their @@ -197,29 +216,235 @@ def download_binaries(self): with self.lock: self.data['binaries'].append(b) + if self.data['current_neard_path'] is None: + self.reset_current_neard_path() self.save_data() - def do_start(self): + def target_near_home_path(self, *args): + if self.is_traffic_generator(): + args = ('target',) + args + return os.path.join(self.neard_home, *args) + + def home_path(self, *args): + return os.path.join(self.home, *args) + + def tmp_near_home_path(self, *args): + args = ('tmp-near-home',) + args + return os.path.join(self.home, *args) + + def neard_init(self): + # We make neard init save files to self.tmp_near_home_path() just to make it + # a bit cleaner, so we can init to a non-existent directory and then move + # the files we want to the real near home without having to remove it first + cmd = [ + self.data['binaries'][0]['system_path'], '--home', + self.tmp_near_home_path(), 'init' + ] + if not self.is_traffic_generator(): + cmd += ['--account-id', f'{socket.gethostname()}.near'] + subprocess.check_call(cmd) + + with open(self.tmp_near_home_path('config.json'), 'r') as f: + config = json.load(f) + self.data['neard_addr'] = config['rpc']['addr'] + config['tracked_shards'] = [0, 1, 2, 3] + config['log_summary_style'] = 'plain' + config['network']['skip_sync_wait'] = False + config['genesis_records_file'] = 'records.json' + config['rpc']['enable_debug_rpc'] = True + if self.is_traffic_generator(): + config['archive'] = True + with open(self.tmp_near_home_path('config.json'), 'w') as f: + json.dump(config, f, indent=2) + + def move_init_files(self): + try: + os.mkdir(self.target_near_home_path()) + except FileExistsError: + pass + for p in os.listdir(self.target_near_home_path()): + filename = self.target_near_home_path(p) + if os.path.isfile(filename): + os.remove(filename) + try: + shutil.rmtree(self.target_near_home_path('data')) + except FileNotFoundError: + pass + paths = ['config.json', 'node_key.json'] + if not self.is_traffic_generator(): + paths.append('validator_key.json') + for path in paths: + shutil.move(self.tmp_near_home_path(path), + self.target_near_home_path(path)) + + # This RPC method tells to stop neard and re-initialize its home dir. This returns the + # validator and node key that resulted from the initialization. We can't yet call amend-genesis + # and compute state roots, because the caller of this method needs to hear back from + # each node before it can build the list of initial validators. So after this RPC method returns, + # we'll be waiting for the network_init RPC. + # TODO: add a binaries argument that tells what binaries we want to use in the test. Before we do + # this, it is pretty mandatory to implement some sort of client authentication, because without it, + # anyone would be able to get us to download and run arbitrary code + def do_new_test(self): with self.lock: - # we should already have tried running it if this is true - if self.data['run']: - return + self.kill_neard() + try: + shutil.rmtree(self.tmp_near_home_path()) + except FileNotFoundError: + pass + try: + os.remove(self.home_path('validators.json')) + except FileNotFoundError: + pass + try: + os.remove(self.home_path('network_init.json')) + except FileNotFoundError: + pass + + self.neard_init() + self.move_init_files() + + with open(self.target_near_home_path('config.json'), 'r') as f: + config = json.load(f) + with open(self.target_near_home_path('node_key.json'), 'r') as f: + node_key = json.load(f) + if not self.is_traffic_generator(): + with open(self.target_near_home_path('validator_key.json'), + 'r') as f: + validator_key = json.load(f) + validator_account_id = validator_key['account_id'] + validator_public_key = validator_key['public_key'] + else: + validator_account_id = None + validator_public_key = None - # for now just set this and wait for the next iteration of the update loop to start it - self.data['run'] = True + self.set_state(TestState.AWAITING_NETWORK_INIT) self.save_data() + return { + 'validator_account_id': validator_account_id, + 'validator_public_key': validator_public_key, + 'node_key': node_key['public_key'], + 'listen_port': config['network']['addr'].split(':')[1], + } + + # After the new_test RPC, we wait to get this RPC that gives us the list of validators + # and boot nodes for the test network. After this RPC call, we run amend-genesis and + # start neard to compute genesis state roots. + def do_network_init(self, + validators, + boot_nodes, + epoch_length=1000, + num_seats=100, + protocol_version=None): + if not isinstance(validators, list): + raise jsonrpc.exceptions.JSONRPCDispatchException( + code=-32600, message='validators argument not a list') + if not isinstance(boot_nodes, list): + raise jsonrpc.exceptions.JSONRPCDispatchException( + code=-32600, message='boot_nodes argument not a list') + + # TODO: maybe also check validity of these arguments? + if len(validators) == 0: + raise jsonrpc.exceptions.JSONRPCDispatchException( + code=-32600, message='validators argument must not be empty') + if len(boot_nodes) == 0: + raise jsonrpc.exceptions.JSONRPCDispatchException( + code=-32600, message='boot_nodes argument must not be empty') + + with self.lock: + state = self.get_state() + if state != TestState.AWAITING_NETWORK_INIT: + raise jsonrpc.exceptions.JSONRPCDispatchException( + code=-32600, + message='Can only call network_init after a call to init') + + if len(validators) < 3: + with open(self.target_near_home_path('config.json'), 'r') as f: + config = json.load(f) + config['consensus']['min_num_peers'] = len(validators) - 1 + with open(self.target_near_home_path('config.json'), 'w') as f: + json.dump(config, f) + with open(self.home_path('validators.json'), 'w') as f: + json.dump(validators, f) + with open(self.home_path('network_init.json'), 'w') as f: + json.dump( + { + 'boot_nodes': boot_nodes, + 'epoch_length': epoch_length, + 'num_seats': num_seats, + 'protocol_version': protocol_version, + }, f) + + def do_update_config(self, state_cache_size_mb): + with self.lock: + logging.info(f'updating config with {state_cache_size_mb}') + with open(self.target_near_home_path('config.json'), 'r') as f: + config = json.load(f) + + config['store']['trie_cache']['per_shard_max_bytes'] = {} + if state_cache_size_mb is not None: + for i in range(4): + config['store']['trie_cache']['per_shard_max_bytes'][ + f's{i}.v1'] = state_cache_size_mb * 10**6 + + with open(self.target_near_home_path('config.json'), 'w') as f: + json.dump(config, f, indent=2) + + return True + + def do_start(self): + with self.lock: + state = self.get_state() + if state == TestState.STOPPED: + self.start_neard() + self.set_state(TestState.RUNNING) + self.save_data() + elif state != TestState.RUNNING: + raise jsonrpc.exceptions.JSONRPCDispatchException( + code=-32600, + message= + 'Cannot start node as test state has not been initialized yet' + ) + + # right now only has an effect if the test setup has been initialized. Should it also mean stop setting up + # the test if we're in the middle of initializing it? def do_stop(self): with self.lock: - if not self.data['run']: - return + state = self.get_state() + if state == TestState.RUNNING: + self.kill_neard() + self.set_state(TestState.STOPPED) + self.save_data() + + def do_reset(self): + with self.lock: + state = self.get_state() + if state == TestState.RUNNING: + self.kill_neard() + self.set_state(TestState.RESETTING) + self.reset_current_neard_path() + self.save_data() + elif state == TestState.STOPPED: + self.set_state(TestState.RESETTING) + self.reset_current_neard_path() + self.save_data() + else: + raise jsonrpc.exceptions.JSONRPCDispatchException( + code=-32600, + message= + 'Cannot reset node as test state has not been initialized yet' + ) - # for now just set this and wait for the next iteration of the update loop to stop it - self.data['run'] = False - self.save_data() + def do_ready(self): + with self.lock: + state = self.get_state() + return state == TestState.RUNNING or state == TestState.STOPPED # check the current epoch height, and return the binary path that we should # be running given the epoch heights specified in config.json + # TODO: should we update it at a random time in the middle of the + # epoch instead of the beginning? def wanted_neard_path(self): j = { 'method': 'validators', @@ -228,7 +453,9 @@ def wanted_neard_path(self): 'jsonrpc': '2.0' } try: - r = requests.post(f'http://{self.neard_addr}', json=j, timeout=5) + r = requests.post(f'http://{self.data["neard_addr"]}', + json=j, + timeout=5) r.raise_for_status() response = json.loads(r.content) epoch_height = response['result']['epoch_height'] @@ -241,128 +468,327 @@ def wanted_neard_path(self): break return path except (requests.exceptions.ConnectionError, KeyError): - return self.data['neard_process']['path'] - - def check_run_neard(self): - neard_path = self.wanted_neard_path() - start_neard = False + return self.data['current_neard_path'] + + def run_neard(self, cmd, out_file=None): + assert (self.neard is None) + assert (self.data['neard_process'] is None) + env = os.environ.copy() + if 'RUST_LOG' not in env: + env['RUST_LOG'] = 'actix_web=warn,mio=warn,tokio_util=warn,actix_server=warn,actix_http=warn,indexer=info,debug' + logging.info(f'running {" ".join(cmd)}') + self.neard = subprocess.Popen( + cmd, + stdin=subprocess.DEVNULL, + stdout=out_file, + stderr=out_file, + env=env, + ) + # we save the create_time so we can tell if the process with pid equal + # to the one we saved is the same process. It's not that likely, but + # if we don't do this (or maybe something else like it), then it's possble + # that if this process is killed and restarted and we see a process with that PID, + # it could actually be a different process that was started later after neard exited + try: + create_time = int(psutil.Process(self.neard.pid).create_time()) + except psutil.NoSuchProcess: + # not that likely, but if it already exited, catch the exception so + # at least this process doesn't die + create_time = 0 + + self.data['neard_process'] = { + 'pid': self.neard.pid, + 'create_time': create_time, + 'path': cmd[0], + } + self.save_data() + def poll_neard(self): if self.neard is not None: code = self.neard.poll() - if code is not None: - logging.info(f'neard exited with code {code}. restarting') - start_neard = True - else: - if self.data['neard_process']['path'] != neard_path: - logging.info('upgrading neard upon new epoch') - self.neard.send_signal(signal.SIGINT) - self.neard.wait() - start_neard = True - elif self.data['neard_process']['pid'] is not None: + path = self.data['neard_process']['path'] + running = code is None + if not running: + self.neard = None + self.data['neard_process'] = None + self.save_data() + return path, running, code + elif self.data['neard_process'] is not None: + path = self.data['neard_process']['path'] # we land here if this process previously died and is now restarted, # and the old neard process is still running try: p = psutil.Process(self.data['neard_process']['pid']) if int(p.create_time() ) == self.data['neard_process']['create_time']: - if self.data['neard_process']['path'] != neard_path: - logging.info('upgrading neard upon new epoch') - p.send_signal(signal.SIGINT) - p.wait() - start_neard = True - else: - start_neard = True + return path, True, None except psutil.NoSuchProcess: - start_neard = True + self.neard = None + self.data['neard_process'] = None + self.save_data() + return path, False, None else: - start_neard = True - - if start_neard: - try: - os.mkdir(self.neard_logs_dir) - except FileExistsError: - pass - for i in range(20, -1, -1): - old_log = os.path.join(self.neard_logs_dir, f'log-{i}.txt') - new_log = os.path.join(self.neard_logs_dir, f'log-{i+1}.txt') - try: - os.rename(old_log, new_log) - except FileNotFoundError: - pass + return None, False, None - with open(os.path.join(self.neard_logs_dir, 'log-0.txt'), - 'ab') as out: - cmd = [ - neard_path, '--home', self.neard_home, - '--unsafe-fast-startup', 'run' - ] - env = os.environ.copy() - if 'RUST_LOG' not in env: - env['RUST_LOG'] = 'actix_web=warn,mio=warn,tokio_util=warn,actix_server=warn,actix_http=warn,debug' - logging.info(f'running {" ".join(cmd)}') - self.neard = subprocess.Popen( - cmd, - stdin=subprocess.DEVNULL, - stdout=out, - stderr=out, - env=env, - ) - - try: - create_time = int(psutil.Process(self.neard.pid).create_time()) - except psutil.NoSuchProcess: - # not that likely, but if neard already exited, catch the exception so - # at least this process doesn't die - create_time = 0 - - self.data['neard_process'] = { - 'pid': self.neard.pid, - 'create_time': create_time, - 'path': neard_path, - } - self.save_data() - - def check_stop_neard(self): + def kill_neard(self): if self.neard is not None: logging.info('stopping neard') self.neard.send_signal(signal.SIGINT) self.neard.wait() self.neard = None - self.data['neard_process']['pid'] = None - self.data['neard_process']['create_time'] = 0 + self.data['neard_process'] = None self.save_data() - elif self.data['neard_process']['pid'] is not None: + return + + if self.data['neard_process'] is None: + return + + try: + p = psutil.Process(self.data['neard_process']['pid']) + if int(p.create_time() + ) == self.data['neard_process']['create_time']: + logging.info('stopping neard') + p.send_signal(signal.SIGINT) + p.wait() + except psutil.NoSuchProcess: + pass + self.neard = None + self.data['neard_process'] = None + self.save_data() + + # If this is a regular node, starts neard run. If it's a traffic generator, starts neard mirror run + def start_neard(self): + for i in range(20, -1, -1): + old_log = os.path.join(self.neard_logs_dir, f'log-{i}.txt') + new_log = os.path.join(self.neard_logs_dir, f'log-{i+1}.txt') try: - p = psutil.Process(self.data['neard_process']['pid']) - if int(p.create_time() - ) == self.data['neard_process']['create_time']: - logging.info('stopping neard') - p.send_signal(signal.SIGINT) - p.wait() - except psutil.NoSuchProcess: + os.rename(old_log, new_log) + except FileNotFoundError: pass - self.data['neard_process']['pid'] = None - self.data['neard_process']['create_time'] = 0 + + with open(os.path.join(self.neard_logs_dir, 'log-0.txt'), 'ab') as out: + if self.is_traffic_generator(): + cmd = [ + self.data['current_neard_path'], + 'mirror', + 'run', + '--source-home', + self.neard_home, + '--target-home', + self.target_near_home_path(), + '--no-secret', + ] + else: + cmd = [ + self.data['current_neard_path'], '--log-span-events', + '--home', self.neard_home, '--unsafe-fast-startup', 'run' + ] + self.run_neard( + cmd, + out_file=out, + ) + self.last_start = time.time() + + # returns a bool that tells whether we should attempt a restart + def on_neard_died(self): + if self.is_traffic_generator(): + # TODO: This is just a lazy way to deal with the fact + # that the mirror command is expected to exit after it finishes sending all the traffic. + # For now just don't restart neard on the traffic generator. Here we should be smart + # about restarting only if it makes sense, and we also shouldn't restart over and over. + # Right now we can't just check the exit_code because there's a bug that makes the + # neard mirror run command not exit cleanly when it's finished + return False + + now = time.time() + if now - self.last_start > 600: + self.num_restarts = 1 + return True + if self.num_restarts >= 5: + self.set_state(TestState.STOPPED) self.save_data() + return False + else: + self.num_restarts += 1 + return True + + def check_upgrade_neard(self): + neard_path = self.wanted_neard_path() + + path, running, exit_code = self.poll_neard() + if path is None: + start_neard = self.on_neard_died() + elif not running: + if exit_code is not None: + logging.info(f'neard exited with code {exit_code}.') + start_neard = self.on_neard_died() + else: + if path == neard_path: + start_neard = False + else: + logging.info('upgrading neard upon new epoch') + self.kill_neard() + start_neard = True + + if start_neard: + self.data['current_neard_path'] = neard_path + self.start_neard() + + def get_state(self): + return TestState(self.data['state']) + + def set_state(self, state): + self.data['state'] = state.value + + def network_init(self): + # wait til we get a network_init RPC + if not os.path.exists(self.home_path('validators.json')): + return + + with open(self.home_path('network_init.json'), 'r') as f: + n = json.load(f) + with open(self.target_near_home_path('node_key.json'), 'r') as f: + node_key = json.load(f) + with open(self.target_near_home_path('config.json'), 'r') as f: + config = json.load(f) + boot_nodes = [] + for b in n['boot_nodes']: + if node_key['public_key'] != b.split('@')[0]: + boot_nodes.append(b) + + config['network']['boot_nodes'] = ','.join(boot_nodes) + with open(self.target_near_home_path('config.json'), 'w') as f: + config = json.dump(config, f, indent=2) + + cmd = [ + self.data['binaries'][0]['system_path'], + 'amend-genesis', + '--genesis-file-in', + os.path.join(self.neard_home, 'setup', 'genesis.json'), + '--records-file-in', + os.path.join(self.neard_home, 'setup', 'records.json'), + '--genesis-file-out', + self.target_near_home_path('genesis.json'), + '--records-file-out', + self.target_near_home_path('records.json'), + '--validators', + self.home_path('validators.json'), + '--chain-id', + 'mocknet', + '--transaction-validity-period', + '10000', + '--epoch-length', + str(n['epoch_length']), + '--num-seats', + str(n['num_seats']), + ] + if n['protocol_version'] is not None: + cmd.append('--protocol-version') + cmd.append(str(n['protocol_version'])) + + self.run_neard(cmd) + self.set_state(TestState.AMEND_GENESIS) + self.save_data() + + def check_amend_genesis(self): + path, running, exit_code = self.poll_neard() + if path is None: + logging.error( + 'state is AMEND_GENESIS, but no amend-genesis process is known') + self.set_state(TestState.AWAITING_NETWORK_INIT) + self.save_data() + elif not running: + if exit_code is not None and exit_code != 0: + logging.error( + f'neard amend-genesis exited with code {exit_code}') + # for now just set the state to None, and if this ever happens, the + # test operator will have to intervene manually. Probably shouldn't + # really happen in practice + self.set_state(TestState.NONE) + self.save_data() + else: + # TODO: if exit_code is None then we were interrupted and restarted after starting + # the amend-genesis command. We assume here that the command was successful. Ok for now since + # the command probably won't fail. But should somehow check that it was OK + with open(os.path.join(self.neard_logs_dir, 'initlog.txt'), + 'ab') as out: + cmd = [ + self.data['binaries'][0]['system_path'], '--home', + self.target_near_home_path(), '--unsafe-fast-startup', + 'run' + ] + self.run_neard( + cmd, + out_file=out, + ) + self.set_state(TestState.STATE_ROOTS) + self.save_data() + + def check_genesis_state(self): + path, running, exit_code = self.poll_neard() + if not running: + logging.error( + f'neard exited with code {exit_code} on the first run') + # For now just exit, because if this happens, there is something pretty wrong with + # the setup, so a human needs to investigate and fix the bug + sys.exit(1) + try: + r = requests.get(f'http://{self.data["neard_addr"]}/status', + timeout=5) + if r.status_code == 200: + logging.info('neard finished computing state roots') + self.kill_neard() + + try: + shutil.rmtree(self.home_path('backups')) + except FileNotFoundError: + pass + os.mkdir(self.home_path('backups')) + # Right now we save the backup to backups/start and in the future + # it would be nice to support a feature that lets you stop all the nodes and + # make another backup to restore to + backup_dir = self.home_path('backups', 'start') + logging.info(f'copying data dir to {backup_dir}') + shutil.copytree(self.target_near_home_path('data'), backup_dir) + self.set_state(TestState.STOPPED) + self.save_data() + except requests.exceptions.ConnectionError: + pass + + def reset_near_home(self): + try: + shutil.rmtree(self.target_near_home_path('data')) + except FileNotFoundError: + pass + logging.info('restoring data dir from backup') + shutil.copytree(self.home_path('backups', 'start'), + self.target_near_home_path('data')) + logging.info('data dir restored') + self.set_state(TestState.STOPPED) + self.save_data() # periodically check if we should update neard after a new epoch - # TODO: should we update it at a random time in the middle of the - # epoch instead of the beginning? - def upgrade_neard(self): + def main_loop(self): while True: with self.lock: - if self.data['run']: - self.check_run_neard() - else: - self.check_stop_neard() - + state = self.get_state() + if state == TestState.AWAITING_NETWORK_INIT: + self.network_init() + elif state == TestState.AMEND_GENESIS: + self.check_amend_genesis() + elif state == TestState.STATE_ROOTS: + self.check_genesis_state() + elif state == TestState.RUNNING: + self.check_upgrade_neard() + elif state == TestState.RESETTING: + self.reset_near_home() time.sleep(10) def serve(self, port): # TODO: maybe use asyncio? kind of silly to use multiple threads for # something so lightweight - upgrade_loop = threading.Thread(target=self.upgrade_neard) - upgrade_loop.start() + main_loop = threading.Thread(target=self.main_loop) + main_loop.start() s = RpcServer(('0.0.0.0', port), self) s.serve_forever() diff --git a/pytest/tests/mocknet/helpers/state_contract.rs b/pytest/tests/mocknet/helpers/state_contract.rs index ba9559b5859..d36dc95dc8c 100644 --- a/pytest/tests/mocknet/helpers/state_contract.rs +++ b/pytest/tests/mocknet/helpers/state_contract.rs @@ -42,7 +42,6 @@ impl StatusMessage { // receiver. The callback simply returns `1`. #[near_bindgen] impl FungibleTokenReceiver for StatusMessage { - #[allow(unused_variables)] fn ft_on_transfer( &mut self, sender_id: ValidAccountId, diff --git a/pytest/tests/mocknet/mirror.py b/pytest/tests/mocknet/mirror.py index 3b82b374024..172c403d021 100755 --- a/pytest/tests/mocknet/mirror.py +++ b/pytest/tests/mocknet/mirror.py @@ -41,40 +41,6 @@ def get_nodes(args): return traffic_generator, nodes -def get_node_peer_info(node): - config = mocknet.download_and_read_json(node, - '/home/ubuntu/.near/config.json') - node_key = mocknet.download_and_read_json( - node, '/home/ubuntu/.near/node_key.json') - - key = node_key['public_key'] - port = config['network']['addr'].split(':')[1] - return f'{key}@{node.machine.ip}:{port}' - - -def get_boot_nodes(nodes): - boot_nodes = pmap(get_node_peer_info, nodes[:20]) - return ','.join(boot_nodes) - - -def get_validator_list(nodes): - validators = [] - - validator_keys = pmap( - lambda node: mocknet.download_and_read_json( - node, '/home/ubuntu/.near/validator_key.json'), nodes) - - for i, validator_key in enumerate(validator_keys): - validators.append({ - 'account_id': validator_key['account_id'], - 'public_key': validator_key['public_key'], - # TODO: give a way to specify the stakes - 'amount': str(10**33), - }) - - return validators - - def run_cmd(node, cmd): r = node.machine.run(cmd) if r.exitcode != 0: @@ -84,8 +50,8 @@ def run_cmd(node, cmd): return r -LOG_DIR = '/home/ubuntu/.near/logs' -STATUS_DIR = '/home/ubuntu/.near/logs/status' +LOG_DIR = '/home/ubuntu/logs' +STATUS_DIR = '/home/ubuntu/logs/status' def run_in_background(node, cmd, log_filename, env=''): @@ -97,157 +63,7 @@ def run_in_background(node, cmd, log_filename, env=''): ) -def wait_process(node, log_filename): - r = run_cmd( - node, - f'stat {STATUS_DIR}/{log_filename} >/dev/null && tail --retry -f {STATUS_DIR}/{log_filename} | head -n 1' - ) - if r.stdout.strip() != '0': - sys.exit( - f'bad status in {STATUS_DIR}/{log_filename} on {node.instance_name}: {r.stdout}\ncheck {LOG_DIR}/{log_filename} for details' - ) - - -def check_process(node, log_filename): - r = run_cmd(node, f'head -n 1 {STATUS_DIR}/{log_filename}') - out = r.stdout.strip() - if len(out) > 0 and out != '0': - sys.exit( - f'bad status in {STATUS_DIR}/{log_filename} on {node.instance_name}: {r.stdout}\ncheck {LOG_DIR}/{log_filename} for details' - ) - - -def set_boot_nodes(node, boot_nodes): - if not node.instance_name.endswith('traffic'): - home_dir = '/home/ubuntu/.near' - else: - home_dir = '/home/ubuntu/.near/target' - - run_cmd( - node, - f't=$(mktemp) && jq \'.network.boot_nodes = "{boot_nodes}"\' {home_dir}/config.json > $t && mv $t {home_dir}/config.json' - ) - - -# returns the peer ID of the resulting initialized NEAR dir -def init_home_dir(node, num_validators): - run_cmd(node, f'mkdir -p /home/ubuntu/.near/setup/') - run_cmd(node, f'mkdir -p {LOG_DIR}') - run_cmd(node, f'mkdir -p {STATUS_DIR}') - - if not node.instance_name.endswith('traffic'): - cmd = 'find /home/ubuntu/.near -type f -maxdepth 1 -delete && ' - cmd += 'rm -rf /home/ubuntu/.near/data && ' - cmd += '/home/ubuntu/neard init --account-id "$(hostname).near" && ' - cmd += 'rm -f /home/ubuntu/.near/genesis.json && ' - home_dir = '/home/ubuntu/.near' - else: - cmd = 'rm -rf /home/ubuntu/.near/target/ && ' - cmd += 'mkdir -p /home/ubuntu/.near/target/ && ' - cmd += '/home/ubuntu/neard --home /home/ubuntu/.near/target/ init && ' - cmd += 'rm -f /home/ubuntu/.near/target/validator_key.json && ' - cmd += 'rm -f /home/ubuntu/.near/target/genesis.json && ' - home_dir = '/home/ubuntu/.near/target' - - # TODO: don't hardcode tracked_shards. mirror run only sends txs for the shards in tracked shards. maybe should change that... - config_changes = '.tracked_shards = [0, 1, 2, 3] | .archive = true | .log_summary_style="plain" | .rpc.addr = "0.0.0.0:3030" ' - config_changes += '| .network.skip_sync_wait=false | .genesis_records_file = "records.json" | .rpc.enable_debug_rpc = true ' - if num_validators < 3: - config_changes += f'| .consensus.min_num_peers = {num_validators}' - - cmd += '''t=$(mktemp) && jq '{config_changes}' {home_dir}/config.json > $t && mv $t {home_dir}/config.json'''.format( - config_changes=config_changes, home_dir=home_dir) - run_cmd(node, cmd) - - config = mocknet.download_and_read_json(node, f'{home_dir}/config.json') - node_key = mocknet.download_and_read_json(node, f'{home_dir}/node_key.json') - key = node_key['public_key'] - port = config['network']['addr'].split(':')[1] - return f'{key}@{node.machine.ip}:{port}' - - -BACKUP_DIR = '/home/ubuntu/.near/backup' - - -def make_backup(node, log_filename): - if node.instance_name.endswith('traffic'): - home = '/home/ubuntu/.near/target' - else: - home = '/home/ubuntu/.near' - run_in_background( - node, - f'rm -rf {BACKUP_DIR} && mkdir {BACKUP_DIR} && cp -r {home}/data {BACKUP_DIR}/data && cp {home}/genesis.json {BACKUP_DIR}', - log_filename) - - -def check_backup(node): - if node.instance_name.endswith('traffic'): - genesis = '/home/ubuntu/.near/target/genesis.json' - else: - genesis = '/home/ubuntu/.near/genesis.json' - - cmd = f'current=$(md5sum {genesis} | cut -d " " -f1) ' - cmd += f'&& saved=$(md5sum {BACKUP_DIR}/genesis.json | cut -d " " -f1)' - cmd += f' && if [ $current == $saved ]; then exit 0; else echo "md5sum mismatch between {genesis} and {BACKUP_DIR}/genesis.json"; exit 1; fi' - r = node.machine.run(cmd) - if r.exitcode != 0: - logger.warning( - f'on {node.instance_name} could not check that saved state in {BACKUP_DIR} matches with ~/.near:\n{r.stdout}\n{r.stderr}' - ) - return False - return True - - -def reset_data_dir(node, log_filename): - if node.instance_name.endswith('traffic'): - data = '/home/ubuntu/.near/target/data' - else: - data = '/home/ubuntu/.near/data' - run_in_background(node, f'rm -rf {data} && cp -r {BACKUP_DIR}/data {data}', - log_filename) - - -def amend_genesis_file(node, validators, epoch_length, num_seats, log_filename): - mocknet.upload_json(node, '/home/ubuntu/.near/setup/validators.json', - validators) - - if not node.instance_name.endswith('traffic'): - neard = '/home/ubuntu/neard-setup' - genesis_file_out = '/home/ubuntu/.near/genesis.json' - records_file_out = '/home/ubuntu/.near/records.json' - config_file_path = '/home/ubuntu/.near/config.json' - else: - neard = '/home/ubuntu/neard' - genesis_file_out = '/home/ubuntu/.near/target/genesis.json' - records_file_out = '/home/ubuntu/.near/target/records.json' - amend_genesis_cmd = [ - neard, - 'amend-genesis', - '--genesis-file-in', - '/home/ubuntu/.near/setup/genesis.json', - '--records-file-in', - '/home/ubuntu/.near/setup/records.json', - '--genesis-file-out', - genesis_file_out, - '--records-file-out', - records_file_out, - '--validators', - '/home/ubuntu/.near/setup/validators.json', - '--chain-id', - 'mocknet', - '--transaction-validity-period', - '10000', - '--epoch-length', - str(epoch_length), - '--num-seats', - str(args.num_seats), - ] - amend_genesis_cmd = ' '.join(amend_genesis_cmd) - run_in_background(node, amend_genesis_cmd, log_filename) - - -# like mocknet.wait_node_up() but we also check the status file -def wait_node_up(node, log_filename): +def wait_node_up(node): while True: try: res = node.get_validators() @@ -258,45 +74,9 @@ def wait_node_up(node, log_filename): except (ConnectionRefusedError, requests.exceptions.ConnectionError) as e: pass - check_process(node, log_filename) time.sleep(10) -def neard_running(node): - return len(node.machine.run('ps cax | grep neard').stdout) > 0 - - -def start_neard(node, log_filename): - if node.instance_name.endswith('traffic'): - home = '/home/ubuntu/.near/target' - else: - home = '/home/ubuntu/.near/' - - if not neard_running(node): - run_in_background( - node, - f'/home/ubuntu/neard --unsafe-fast-startup --home {home} run', - log_filename, - env='RUST_LOG=debug') - logger.info(f'started neard on {node.instance_name}') - else: - logger.info(f'neard already running on {node.instance_name}') - - -def start_mirror(node, log_filename): - assert node.instance_name.endswith('traffic') - - if not neard_running(node): - run_in_background( - node, - f'/home/ubuntu/neard mirror run --source-home /home/ubuntu/.near --target-home /home/ubuntu/.near/target --no-secret', - log_filename, - env='RUST_LOG=info,mirror=debug') - logger.info(f'started neard mirror run on {node.instance_name}') - else: - logger.info(f'neard already running on {node.instance_name}') - - def prompt_setup_flags(args): print( 'this will reset all nodes\' home dirs and initialize them with new state. continue? [yes/no]' @@ -316,37 +96,39 @@ def prompt_setup_flags(args): print('number of block producer seats?: ') args.num_seats = int(sys.stdin.readline().strip()) - if args.neard_binary_url is None: - print('neard binary URL?: ') - args.neard_binary_url = sys.stdin.readline().strip() - assert len(args.neard_binary_url) > 0 + if args.genesis_protocol_version is None: + print('genesis protocol version?: ') + args.genesis_protocol_version = int(sys.stdin.readline().strip()) - if args.neard_upgrade_binary_url is None: - print( - 'add a second neard binary URL to upgrade to mid-test? enter nothing here to skip: ' - ) - url = sys.stdin.readline().strip() - if len(url) > 0: - args.neard_upgrade_binary_url = url + +def start_neard_runner(node): + run_in_background(node, f'/home/ubuntu/neard-runner/venv/bin/python /home/ubuntu/neard-runner/neard_runner.py ' \ + '--home /home/ubuntu/neard-runner --neard-home /home/ubuntu/.near ' \ + '--neard-logs /home/ubuntu/neard-logs --port 3000', 'neard-runner.txt') -def upload_neard_runner(node, config): - node.machine.run( - 'rm -rf /home/ubuntu/neard-runner && mkdir /home/ubuntu/neard-runner && mkdir -p /home/ubuntu/neard-logs' - ) +def upload_neard_runner(node): node.machine.upload('tests/mocknet/helpers/neard_runner.py', '/home/ubuntu/neard-runner', switch_user='ubuntu') node.machine.upload('tests/mocknet/helpers/requirements.txt', '/home/ubuntu/neard-runner', switch_user='ubuntu') + + +def init_neard_runner(node, config, remove_home_dir=False): + stop_neard_runner(node) + rm_cmd = 'rm -rf /home/ubuntu/neard-runner && ' if remove_home_dir else '' + run_cmd( + node, + f'{rm_cmd}mkdir -p {LOG_DIR} && mkdir -p {STATUS_DIR} && mkdir -p /home/ubuntu/neard-runner' + ) + upload_neard_runner(node) mocknet.upload_json(node, '/home/ubuntu/neard-runner/config.json', config) cmd = 'cd /home/ubuntu/neard-runner && python3 -m virtualenv venv -p $(which python3)' \ ' && ./venv/bin/pip install -r requirements.txt' run_cmd(node, cmd) - run_in_background(node, f'/home/ubuntu/neard-runner/venv/bin/python /home/ubuntu/neard-runner/neard_runner.py ' \ - '--home /home/ubuntu/neard-runner --neard-home /home/ubuntu/.near ' \ - '--neard-logs /home/ubuntu/neard-logs --port 3000', 'neard-runner.txt') + start_neard_runner(node) def stop_neard_runner(node): @@ -354,14 +136,38 @@ def stop_neard_runner(node): node.machine.run('kill $(ps -C python -o pid=)') -def init_neard_runner(nodes, binary_url, upgrade_binary_url): - if upgrade_binary_url is None: +def prompt_init_flags(args): + if args.neard_binary_url is None: + print('neard binary URL?: ') + args.neard_binary_url = sys.stdin.readline().strip() + assert len(args.neard_binary_url) > 0 + + if args.neard_upgrade_binary_url is None: + print( + 'add a second neard binary URL to upgrade to mid-test? enter nothing here to skip: ' + ) + url = sys.stdin.readline().strip() + if len(url) > 0: + args.neard_upgrade_binary_url = url + + +def init_neard_runners(args, traffic_generator, nodes, remove_home_dir=False): + prompt_init_flags(args) + if args.neard_upgrade_binary_url is None: configs = [{ + "is_traffic_generator": False, "binaries": [{ - "url": binary_url, + "url": args.neard_binary_url, "epoch_height": 0 }] }] * len(nodes) + traffic_generator_config = { + "is_traffic_generator": True, + "binaries": [{ + "url": args.neard_binary_url, + "epoch_height": 0 + }] + } else: # for now this test starts all validators with the same stake, so just make the upgrade # epoch random. If we change the stakes, we should change this to choose how much stake @@ -369,19 +175,90 @@ def init_neard_runner(nodes, binary_url, upgrade_binary_url): configs = [] for i in range(len(nodes)): configs.append({ + "is_traffic_generator": + False, "binaries": [{ - "url": binary_url, + "url": args.neard_binary_url, "epoch_height": 0 }, { - "url": upgrade_binary_url, + "url": args.neard_upgrade_binary_url, "epoch_height": random.randint(1, 4) }] }) + traffic_generator_config = { + "is_traffic_generator": + True, + "binaries": [{ + "url": args.neard_upgrade_binary_url, + "epoch_height": 0 + }] + } + + init_neard_runner(traffic_generator, traffic_generator_config, + remove_home_dir) + pmap(lambda x: init_neard_runner(x[0], x[1], remove_home_dir), + zip(nodes, configs)) + + +def init_cmd(args, traffic_generator, nodes): + init_neard_runners(args, traffic_generator, nodes, remove_home_dir=False) + + +def hard_reset_cmd(args, traffic_generator, nodes): + print(""" + WARNING!!!! + WARNING!!!! + This will undo all chain state, which will force a restart from the beginning, + icluding the genesis state computation which takes several hours. + Continue? [yes/no]""") + if sys.stdin.readline().strip() != 'yes': + return + all_nodes = nodes + [traffic_generator] + pmap(stop_neard_runner, all_nodes) + mocknet.stop_nodes(all_nodes) + init_neard_runners(args, traffic_generator, nodes, remove_home_dir=True) + + +def restart_cmd(args, traffic_generator, nodes): + all_nodes = nodes + [traffic_generator] + pmap(stop_neard_runner, all_nodes) + if args.upload_program: + pmap(upload_neard_runner, all_nodes) + pmap(start_neard_runner, all_nodes) + - pmap(lambda x: upload_neard_runner(x[0], x[1]), zip(nodes, configs)) +# returns boot nodes and validators we want for the new test network +def get_network_nodes(new_test_rpc_responses, num_validators): + validators = [] + boot_nodes = [] + for ip_addr, response in new_test_rpc_responses: + if len(validators) < num_validators: + if response['validator_account_id'] is not None: + # we assume here that validator_account_id is not null, validator_public_key + # better not be null either + validators.append({ + 'account_id': response['validator_account_id'], + 'public_key': response['validator_public_key'], + 'amount': str(10**33), + }) + if len(boot_nodes) < 20: + boot_nodes.append( + f'{response["node_key"]}@{ip_addr}:{response["listen_port"]}') + + if len(validators) >= num_validators and len(boot_nodes) >= 20: + break + # neither of these should happen, since we check the number of available nodes in new_test(), and + # only the traffic generator will respond with null validator_account_id and validator_public_key + if len(validators) == 0: + sys.exit('no validators available after new_test RPCs') + if len(validators) < num_validators: + logger.warning( + f'wanted {num_validators} validators, but only {len(validators)} available' + ) + return validators, boot_nodes -def setup(args, traffic_generator, nodes): +def new_test(args, traffic_generator, nodes): prompt_setup_flags(args) if args.epoch_length <= 0: @@ -394,96 +271,70 @@ def setup(args, traffic_generator, nodes): ) all_nodes = nodes + [traffic_generator] - pmap(stop_neard_runner, nodes) - mocknet.stop_nodes(all_nodes) - # TODO: move all the setup logic to neard_runner.py and just call it here, so - # that each node does its own setup and we don't rely on the computer running this script logger.info(f'resetting/initializing home dirs') - boot_nodes = pmap(lambda node: init_home_dir(node, args.num_validators), - all_nodes) - pmap(lambda node: set_boot_nodes(node, ','.join(boot_nodes[:20])), - all_nodes) - logger.info(f'home dir initialization finished') - - random.shuffle(nodes) - validators = get_validator_list(nodes[:args.num_validators]) + test_keys = pmap(neard_runner_new_test, all_nodes) - init_neard_runner(nodes, args.neard_binary_url, - args.neard_upgrade_binary_url) + validators, boot_nodes = get_network_nodes( + zip([n.machine.ip for n in all_nodes], test_keys), args.num_validators) - logger.info( - f'setting validators and running neard amend-genesis on all nodes. validators: {validators}' - ) - logger.info(f'this step will take a while (> 10 minutes)') + logger.info("""setting validators: {0} +Then running neard amend-genesis on all nodes, and starting neard to compute genesis \ +state roots. This will take a few hours. Run `status` to check if the nodes are \ +ready. After they're ready, you can run `start-traffic`""".format(validators)) pmap( - lambda node: amend_genesis_file(node, validators, args.epoch_length, - args.num_seats, 'amend-genesis.txt'), - all_nodes) - pmap(lambda node: wait_process(node, 'amend-genesis.txt'), all_nodes) - logger.info(f'finished neard amend-genesis step') + lambda node: neard_runner_network_init( + node, validators, boot_nodes, args.epoch_length, args.num_seats, + args.genesis_protocol_version), all_nodes) - logger.info( - f'starting neard nodes then waiting for them to be ready. This may take a long time (a couple hours)' - ) - logger.info( - 'If your connection is broken in the meantime, run "make-backups" to resume' - ) - make_backups(args, traffic_generator, nodes) - - logger.info('test setup complete') - - if args.start_traffic: - start_traffic(args, traffic_generator, nodes) - - -def make_backups(args, traffic_generator, nodes): +def status_cmd(args, traffic_generator, nodes): all_nodes = nodes + [traffic_generator] - pmap(lambda node: start_neard(node, 'neard.txt'), all_nodes) - pmap(lambda node: wait_node_up(node, 'neard.txt'), all_nodes) - mocknet.stop_nodes(all_nodes) - - logger.info(f'copying data dirs to {BACKUP_DIR}') - pmap(lambda node: make_backup(node, 'make-backup.txt'), all_nodes) - pmap(lambda node: wait_process(node, 'make-backup.txt'), all_nodes) + statuses = pmap(neard_runner_ready, all_nodes) + num_ready = 0 + not_ready = [] + for ready, node in zip(statuses, all_nodes): + if not ready: + not_ready.append(node.instance_name) + + if len(not_ready) == 0: + print(f'all {len(all_nodes)} nodes ready') + else: + print( + f'{len(all_nodes)-len(not_ready)}/{len(all_nodes)} ready. Nodes not ready: {not_ready[:3]}' + ) -def reset_data_dirs(args, traffic_generator, nodes): +def reset_cmd(args, traffic_generator, nodes): + print( + 'this will reset all nodes\' home dirs to their initial states right after test initialization finished. continue? [yes/no]' + ) + if sys.stdin.readline().strip() != 'yes': + sys.exit() all_nodes = nodes + [traffic_generator] - stop_nodes(args, traffic_generator, nodes) - # TODO: maybe have the neard runner not return from the JSON rpc until it's stopped? - while True: - if not any(mocknet.is_binary_running_all_nodes( - 'neard', - all_nodes, - )): - break - if not all(pmap(check_backup, all_nodes)): - logger.warning('Not continuing with backup restoration') - return + pmap(neard_runner_reset, all_nodes) + logger.info( + 'Data dir reset in progress. Run the `status` command to see when this is finished. Until it is finished, neard runners may not respond to HTTP requests.' + ) - logger.info('restoring data dirs from /home/ubuntu/.near/data-backup') - pmap(lambda node: reset_data_dir(node, 'reset-data.txt'), all_nodes) - pmap(lambda node: wait_process(node, 'reset-data.txt'), all_nodes) - if args.start_traffic: - start_traffic(args, traffic_generator, nodes) +def stop_nodes_cmd(args, traffic_generator, nodes): + pmap(neard_runner_stop, nodes + [traffic_generator]) -def stop_nodes(args, traffic_generator, nodes): - mocknet.stop_nodes([traffic_generator]) - pmap(neard_runner_stop, nodes) +def stop_traffic_cmd(args, traffic_generator, nodes): + neard_runner_stop(traffic_generator) -def neard_runner_jsonrpc(node, method): - j = {'method': method, 'params': [], 'id': 'dontcare', 'jsonrpc': '2.0'} - r = requests.post(f'http://{node.machine.ip}:3000', json=j, timeout=5) +def neard_runner_jsonrpc(node, method, params=[]): + j = {'method': method, 'params': params, 'id': 'dontcare', 'jsonrpc': '2.0'} + r = requests.post(f'http://{node.machine.ip}:3000', json=j, timeout=30) if r.status_code != 200: logger.warning( f'bad response {r.status_code} trying to send {method} JSON RPC to neard runner on {node.instance_name}:\n{r.content}' ) r.raise_for_status() + return r.json()['result'] def neard_runner_start(node): @@ -494,20 +345,71 @@ def neard_runner_stop(node): neard_runner_jsonrpc(node, 'stop') -def start_traffic(args, traffic_generator, nodes): - if not all(pmap(check_backup, nodes + [traffic_generator])): - logger.warning( - f'Not sending traffic, as the backups in {BACKUP_DIR} dont seem to be up to date' +def neard_runner_new_test(node): + return neard_runner_jsonrpc(node, 'new_test') + + +def neard_runner_network_init(node, validators, boot_nodes, epoch_length, + num_seats, protocol_version): + return neard_runner_jsonrpc(node, + 'network_init', + params={ + 'validators': validators, + 'boot_nodes': boot_nodes, + 'epoch_length': epoch_length, + 'num_seats': num_seats, + 'protocol_version': protocol_version, + }) + + +def neard_update_config(node, state_cache_size_mb): + return neard_runner_jsonrpc(node, + 'update_config', + params={ + 'state_cache_size_mb': state_cache_size_mb, + }) + + +def update_config_cmd(args, traffic_generator, nodes): + nodes = nodes + [traffic_generator] + results = pmap( + lambda node: neard_update_config(node, args.state_cache_size_mb), nodes) + if not all(results): + logger.warn('failed to update configs for some nodes') + return + + +def neard_runner_ready(node): + return neard_runner_jsonrpc(node, 'ready') + + +def neard_runner_reset(node): + return neard_runner_jsonrpc(node, 'reset') + + +def start_nodes_cmd(args, traffic_generator, nodes): + if not all(pmap(neard_runner_ready, nodes)): + logger.warn( + 'not all nodes are ready to start yet. Run the `status` command to check their statuses' ) return + pmap(neard_runner_start, nodes) + pmap(wait_node_up, nodes) + +def start_traffic_cmd(args, traffic_generator, nodes): + if not all(pmap(neard_runner_ready, nodes + [traffic_generator])): + logger.warn( + 'not all nodes are ready to start yet. Run the `status` command to check their statuses' + ) + return pmap(neard_runner_start, nodes) logger.info("waiting for validators to be up") - pmap(lambda node: wait_node_up(node, 'neard.txt'), nodes) + pmap(wait_node_up, nodes) logger.info( "waiting a bit after validators started before starting traffic") time.sleep(10) - start_mirror(traffic_generator, 'mirror.txt') + neard_runner_start(traffic_generator) logger.info( f'test running. to check the traffic sent, try running "curl http://{traffic_generator.machine.ip}:3030/metrics | grep mirror"' ) @@ -519,61 +421,88 @@ def start_traffic(args, traffic_generator, nodes): parser.add_argument('--start-height', type=int, required=True) parser.add_argument('--unique-id', type=str, required=True) - parser.add_argument('--epoch-length', type=int) - parser.add_argument('--num-validators', type=int) - parser.add_argument('--num-seats', type=int) - - parser.add_argument('--neard-binary-url', type=str) - parser.add_argument('--neard-upgrade-binary-url', type=str) - subparsers = parser.add_subparsers(title='subcommands', description='valid subcommands', help='additional help') - setup_parser = subparsers.add_parser('setup', - help=''' + init_parser = subparsers.add_parser('init-neard-runner', + help=''' + Sets up the helper servers on each of the nodes. Doesn't start initializing the test + state, which is done with the `new-test` command. + ''') + init_parser.add_argument('--neard-binary-url', type=str) + init_parser.add_argument('--neard-upgrade-binary-url', type=str) + init_parser.set_defaults(func=init_cmd) + + update_config_parser = subparsers.add_parser('update-config', + help=''' + Update config.json with given flags for all nodes. + ''') + update_config_parser.add_argument('--state-cache-size-mb', type=int) + update_config_parser.set_defaults(func=update_config_cmd) + + restart_parser = subparsers.add_parser('restart-neard-runner', + help=''' + Restarts the neard runner on all nodes. + ''') + restart_parser.add_argument('--upload-program', action='store_true') + restart_parser.set_defaults(func=restart_cmd, upload_program=False) + + hard_reset_parser = subparsers.add_parser('hard-reset', + help=''' + Stops neard and clears all test state on all nodes. + ''') + hard_reset_parser.add_argument('--neard-binary-url', type=str) + hard_reset_parser.add_argument('--neard-upgrade-binary-url', type=str) + hard_reset_parser.set_defaults(func=hard_reset_cmd) + + new_test_parser = subparsers.add_parser('new-test', + help=''' Sets up new state from the prepared records and genesis files with the number of validators specified. This calls neard amend-genesis to create the new genesis and records files, and then starts the neard nodes and waits for them to be online - after computing the genesis state roots. This step takes a very long time (> 12 hours). - Use --start-traffic to start traffic after the setup is complete, which is equivalent to - just running the start-traffic subcommand manually after. + after computing the genesis state roots. This step takes a long time (a few hours). ''') - setup_parser.add_argument('--start-traffic', - default=False, - action='store_true') - setup_parser.set_defaults(func=setup) + new_test_parser.add_argument('--epoch-length', type=int) + new_test_parser.add_argument('--num-validators', type=int) + new_test_parser.add_argument('--num-seats', type=int) + new_test_parser.add_argument('--genesis-protocol-version', type=int) + new_test_parser.set_defaults(func=new_test) - start_parser = subparsers.add_parser( + status_parser = subparsers.add_parser('status', + help=''' + Checks the status of test initialization on each node + ''') + status_parser.set_defaults(func=status_cmd) + + start_traffic_parser = subparsers.add_parser( 'start-traffic', help= - 'starts all nodes and starts neard mirror run on the traffic generator') - start_parser.set_defaults(func=start_traffic) + 'Starts all nodes and starts neard mirror run on the traffic generator.' + ) + start_traffic_parser.set_defaults(func=start_traffic_cmd) + + start_nodes_parser = subparsers.add_parser( + 'start-nodes', + help='Starts all nodes, but does not start the traffic generator.') + start_nodes_parser.set_defaults(func=start_nodes_cmd) stop_parser = subparsers.add_parser('stop-nodes', help='kill all neard processes') - stop_parser.set_defaults(func=stop_nodes) + stop_parser.set_defaults(func=stop_nodes_cmd) - backup_parser = subparsers.add_parser('make-backups', - help=''' - This is run automatically by "setup", but if your connection is interrupted during "setup", this will - resume waiting for the nodes to compute the state roots, and then will make a backup of all data dirs - ''') - backup_parser.add_argument('--start-traffic', - default=False, - action='store_true') - backup_parser.set_defaults(func=make_backups) + stop_parser = subparsers.add_parser( + 'stop-traffic', + help='stop the traffic generator, but leave the other nodes running') + stop_parser.set_defaults(func=stop_traffic_cmd) reset_parser = subparsers.add_parser('reset', help=''' - The setup command saves the data directory after the genesis state roots are computed so that + The new_test command saves the data directory after the genesis state roots are computed so that the test can be reset from the start without having to do that again. This command resets all nodes' data dirs to what was saved then, so that start-traffic will start the test all over again. ''') - reset_parser.add_argument('--start-traffic', - default=False, - action='store_true') - reset_parser.set_defaults(func=reset_data_dirs) + reset_parser.set_defaults(func=reset_cmd) args = parser.parse_args() diff --git a/pytest/tests/sanity/block_sync_flat_storage.py b/pytest/tests/sanity/block_sync_flat_storage.py new file mode 100755 index 00000000000..9c23cd86f7c --- /dev/null +++ b/pytest/tests/sanity/block_sync_flat_storage.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# Spins up one validating node. +# Spins a non-validating node that tracks all shards. +# In the middle of an epoch, the node gets stopped, and the set of tracked shards gets reduced. +# Test that the node correctly handles chunks for the shards that it will care about in the next epoch. +# Spam transactions that require the node to use flat storage to process them correctly. + +import pathlib +import random +import sys + +sys.path.append(str(pathlib.Path(__file__).resolve().parents[2] / 'lib')) + +from cluster import init_cluster, spin_up_node, load_config, apply_config_changes +import account +import transaction +import utils + +EPOCH_LENGTH = 30 + +config0 = { + 'tracked_shards': [0], +} +config1 = { + 'tracked_shards': [0], +} + +config = load_config() +near_root, node_dirs = init_cluster(1, 1, 4, config, + [["epoch_length", EPOCH_LENGTH]], { + 0: config0, + 1: config1 + }) + +boot_node = spin_up_node(config, near_root, node_dirs[0], 0) +node1 = spin_up_node(config, near_root, node_dirs[1], 1, boot_node=boot_node) + +contract_key = boot_node.signer_key +contract = utils.load_test_contract() +latest_block_hash = boot_node.get_latest_block().hash_bytes +deploy_contract_tx = transaction.sign_deploy_contract_tx( + contract_key, contract, 10, latest_block_hash) +result = boot_node.send_tx_and_wait(deploy_contract_tx, 10) +assert 'result' in result and 'error' not in result, ( + 'Expected "result" and no "error" in response, got: {}'.format(result)) + + +def random_workload_until(target, nonce, keys): + while True: + nonce += 1 + height = boot_node.get_latest_block().height + if height > target: + break + if (len(keys) > 100 and random.random() < 0.2) or len(keys) > 1000: + key = keys[random.randint(0, len(keys) - 1)] + call_function(boot_node, 'read', key, nonce) + else: + key = random_u64() + keys.append(key) + call_function(boot_node, 'write', key, nonce) + return (nonce, keys) + + +def random_u64(): + return bytes(random.randint(0, 255) for _ in range(8)) + + +def call_function(node, op, key, nonce): + last_block_hash = node.get_latest_block().hash_bytes + if op == 'read': + args = key + fn = 'read_value' + else: + args = key + random_u64() + fn = 'write_key_value' + + tx = transaction.sign_function_call_tx(node.signer_key, + node.signer_key.account_id, fn, args, + 300 * account.TGAS, 0, nonce, + last_block_hash) + return node.send_tx(tx).get('result') + + +nonce, keys = random_workload_until(EPOCH_LENGTH + 5, 1, []) + +node1.kill() +# Reduce the set of tracked shards and make it variable in time. +# The node is stopped in epoch_height = 1. +# Change the config of tracked shards such that after restart the node cares +# only about shard 0, and in the next epoch it will care about shards [1, 2, 3]. +apply_config_changes(node_dirs[1], { + "tracked_shards": [], + "tracked_shard_schedule": [[0], [0], [1, 2, 3]] +}) + +# Run node0 more to trigger block sync in node1. +nonce, keys = random_workload_until(EPOCH_LENGTH * 2 + 1, nonce, keys) + +# Node1 is now behind and needs to do header sync and block sync. +node1.start(boot_node=boot_node) +utils.wait_for_blocks(node1, target=EPOCH_LENGTH * 2 + 10) diff --git a/pytest/tests/sanity/gc_after_sync1.py b/pytest/tests/sanity/gc_after_sync1.py index 892dd834503..995c5453c12 100755 --- a/pytest/tests/sanity/gc_after_sync1.py +++ b/pytest/tests/sanity/gc_after_sync1.py @@ -25,7 +25,6 @@ "consensus": { "block_fetch_horizon": 10, "block_header_fetch_horizon": 10, - "state_fetch_horizon": 0 }, "tracked_shards": [0], "gc_blocks_limit": 10, diff --git a/pytest/tests/sanity/state_sync_epoch_boundary.py b/pytest/tests/sanity/state_sync_epoch_boundary.py new file mode 100755 index 00000000000..5655f72a269 --- /dev/null +++ b/pytest/tests/sanity/state_sync_epoch_boundary.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +# Spins up one validating node. +# Spins a non-validating node that tracks some shards and the set of tracked +# shards changes regularly. +# The node gets stopped, and gets restarted close to an epoch boundary but in a +# way to trigger state sync. +# +# This test is a regression test to ensure that the node doesn't panic during +# function execution during block sync after a state sync. + +import pathlib +import random +import sys +import tempfile + +sys.path.append(str(pathlib.Path(__file__).resolve().parents[2] / 'lib')) + +from cluster import init_cluster, spin_up_node, load_config, apply_config_changes +import account +import transaction +import utils + +from configured_logger import logger + +EPOCH_LENGTH = 50 + +state_parts_dir = str(pathlib.Path(tempfile.gettempdir()) / 'state_parts') + +config0 = { + 'gc_num_epochs_to_keep': 100, + 'log_summary_period': { + 'secs': 0, + 'nanos': 500000000 + }, + 'log_summary_style': 'plain', + 'state_sync': { + 'dump': { + 'location': { + 'Filesystem': { + 'root_dir': state_parts_dir + } + }, + 'iteration_delay': { + 'secs': 0, + 'nanos': 100000000 + }, + } + }, + 'store.state_snapshot_enabled': True, + 'tracked_shards': [0], +} +config1 = { + 'gc_num_epochs_to_keep': 100, + 'log_summary_period': { + 'secs': 0, + 'nanos': 500000000 + }, + 'log_summary_style': 'plain', + 'state_sync': { + 'sync': { + 'ExternalStorage': { + 'location': { + 'Filesystem': { + 'root_dir': state_parts_dir + } + } + } + } + }, + 'state_sync_enabled': True, + 'consensus.state_sync_timeout': { + 'secs': 0, + 'nanos': 500000000 + }, + 'tracked_shard_schedule': [[0, 2, 3], [0, 2, 3], [0, 1], [0, 1], [0, 1], + [0, 1]], + 'tracked_shards': [], +} + +config = load_config() +near_root, node_dirs = init_cluster(1, 1, 4, config, + [["epoch_length", EPOCH_LENGTH]], { + 0: config0, + 1: config1 + }) + +boot_node = spin_up_node(config, near_root, node_dirs[0], 0) +logger.info('started boot_node') +node1 = spin_up_node(config, near_root, node_dirs[1], 1, boot_node=boot_node) +logger.info('started node1') + +contract = utils.load_test_contract() + +latest_block_hash = boot_node.get_latest_block().hash_bytes +deploy_contract_tx = transaction.sign_deploy_contract_tx( + boot_node.signer_key, contract, 10, latest_block_hash) +result = boot_node.send_tx_and_wait(deploy_contract_tx, 10) +assert 'result' in result and 'error' not in result, ( + 'Expected "result" and no "error" in response, got: {}'.format(result)) + +latest_block_hash = boot_node.get_latest_block().hash_bytes +deploy_contract_tx = transaction.sign_deploy_contract_tx( + node1.signer_key, contract, 10, latest_block_hash) +result = boot_node.send_tx_and_wait(deploy_contract_tx, 10) +assert 'result' in result and 'error' not in result, ( + 'Expected "result" and no "error" in response, got: {}'.format(result)) + + +def epoch_height(block_height): + if block_height == 0: + return 0 + if block_height <= EPOCH_LENGTH: + # According to the protocol specifications, there are two epochs with height 1. + return "1*" + return int((block_height - 1) / EPOCH_LENGTH) + + +# Generates traffic for all possible shards. +# Assumes that `test0`, `test1`, `near` all belong to different shards. +def random_workload_until(target, nonce, keys, target_node): + last_height = -1 + while True: + nonce += 1 + + last_block = target_node.get_latest_block() + height = last_block.height + if height > target: + break + if height != last_height: + logger.info(f'@{height}, epoch_height: {epoch_height(height)}') + last_height = height + + last_block_hash = boot_node.get_latest_block().hash_bytes + if (len(keys) > 100 and random.random() < 0.2) or len(keys) > 1000: + key = keys[random.randint(0, len(keys) - 1)] + call_function('read', key, nonce, boot_node.signer_key, + last_block_hash) + call_function('read', key, nonce, node1.signer_key, last_block_hash) + elif random.random() < 0.5: + if random.random() < 0.3: + key_from, account_to = boot_node.signer_key, node1.signer_key.account_id + elif random.random() < 0.3: + key_from, account_to = boot_node.signer_key, "near" + elif random.random() < 0.5: + key_from, account_to = node1.signer_key, boot_node.signer_key.account_id + else: + key_from, account_to = node1.signer_key, "near" + payment_tx = transaction.sign_payment_tx(key_from, account_to, 1, + nonce, last_block_hash) + boot_node.send_tx(payment_tx).get('result') + else: + key = random_u64() + keys.append(key) + call_function('write', key, nonce, boot_node.signer_key, + last_block_hash) + call_function('write', key, nonce, node1.signer_key, + last_block_hash) + return (nonce, keys) + + +def random_u64(): + return bytes(random.randint(0, 255) for _ in range(8)) + + +def call_function(op, key, nonce, signer_key, last_block_hash): + if op == 'read': + args = key + fn = 'read_value' + else: + args = key + random_u64() + fn = 'write_key_value' + + tx = transaction.sign_function_call_tx(signer_key, signer_key.account_id, + fn, args, 300 * account.TGAS, 0, + nonce, last_block_hash) + return boot_node.send_tx(tx).get('result') + + +nonce, keys = random_workload_until(EPOCH_LENGTH - 5, 1, [], boot_node) + +node1_height = node1.get_latest_block().height +logger.info(f'node1@{node1_height}') +node1.kill() +logger.info(f'killed node1') + +# Run node0 more to trigger block sync in node1. +nonce, keys = random_workload_until(int(EPOCH_LENGTH * 2.7), nonce, keys, + boot_node) + +# Node1 is now behind and needs to do header sync and block sync. +node1.start(boot_node=boot_node) +node1_height = node1.get_latest_block().height +logger.info(f'started node1@{node1_height}') + +nonce, keys = random_workload_until(int(EPOCH_LENGTH * 3.1), nonce, keys, node1) diff --git a/pytest/tests/sanity/state_sync_then_catchup.py b/pytest/tests/sanity/state_sync_then_catchup.py new file mode 100644 index 00000000000..341abeae3ec --- /dev/null +++ b/pytest/tests/sanity/state_sync_then_catchup.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +# Spins up one validating node. +# Spins a non-validating node that tracks some shards and the set of tracked shards changes regularly. +# The node gets stopped, and gets restarted close to an epoch boundary but in a way to trigger state sync. +# +# After the state sync the node has to do a catchup. +# +# Note that the test must generate outgoing receipts for most shards almost +# every block in order to crash if creation of partial encoded chunks becomes +# non-deterministic. + +import pathlib +import random +import sys +import tempfile + +sys.path.append(str(pathlib.Path(__file__).resolve().parents[2] / 'lib')) + +from cluster import init_cluster, spin_up_node, load_config, apply_config_changes +import account +import transaction +import utils + +from configured_logger import logger + +EPOCH_LENGTH = 50 + + +def epoch_height(block_height): + if block_height == 0: + return 0 + if block_height <= EPOCH_LENGTH: + # According to the protocol specifications, there are two epochs with height 1. + return "1*" + return int((block_height - 1) / EPOCH_LENGTH) + + +def random_u64(): + return bytes(random.randint(0, 255) for _ in range(8)) + + +# Generates traffic for all possible shards. +# Assumes that `test0`, `test1`, `near` all belong to different shards. +def random_workload_until(target, nonce, keys, node0, node1, target_node): + last_height = -1 + while True: + nonce += 1 + + last_block = target_node.get_latest_block() + height = last_block.height + if height > target: + break + if height != last_height: + logger.info(f'@{height}, epoch_height: {epoch_height(height)}') + last_height = height + + last_block_hash = node0.get_latest_block().hash_bytes + if random.random() < 0.5: + # Make a transfer between shards. + # The goal is to generate cross-shard receipts. + key_from = random.choice([node0, node1]).signer_key + account_to = random.choice([ + node0.signer_key.account_id, node1.signer_key.account_id, "near" + ]) + payment_tx = transaction.sign_payment_tx(key_from, account_to, 1, + nonce, last_block_hash) + node0.send_tx(payment_tx).get('result') + elif (len(keys) > 100 and random.random() < 0.5) or len(keys) > 1000: + # Do some flat storage reads, but only if we have enough keys populated. + key = keys[random.randint(0, len(keys) - 1)] + for node in [node0, node1]: + call_function('read', key, nonce, node.signer_key, + last_block_hash, node0) + call_function('read', key, nonce, node.signer_key, + last_block_hash, node0) + else: + # Generate some data for flat storage reads + key = random_u64() + keys.append(key) + for node in [node0, node1]: + call_function('write', key, nonce, node.signer_key, + last_block_hash, node0) + return nonce, keys + + +def call_function(op, key, nonce, signer_key, last_block_hash, node): + if op == 'read': + args = key + fn = 'read_value' + else: + args = key + random_u64() + fn = 'write_key_value' + + tx = transaction.sign_function_call_tx(signer_key, signer_key.account_id, + fn, args, 300 * account.TGAS, 0, + nonce, last_block_hash) + return node.send_tx(tx).get('result') + + +def main(): + state_parts_dir = str(pathlib.Path(tempfile.gettempdir()) / 'state_parts') + + config0 = { + 'gc_num_epochs_to_keep': 100, + 'log_summary_period': { + 'secs': 0, + 'nanos': 500000000 + }, + 'log_summary_style': 'plain', + 'state_sync': { + 'dump': { + 'location': { + 'Filesystem': { + 'root_dir': state_parts_dir + } + }, + 'iteration_delay': { + 'secs': 0, + 'nanos': 100000000 + }, + } + }, + 'store.state_snapshot_enabled': True, + 'tracked_shards': [0], + } + config1 = { + 'gc_num_epochs_to_keep': 100, + 'log_summary_period': { + 'secs': 0, + 'nanos': 500000000 + }, + 'log_summary_style': 'plain', + 'state_sync': { + 'sync': { + 'ExternalStorage': { + 'location': { + 'Filesystem': { + 'root_dir': state_parts_dir + } + } + } + } + }, + 'state_sync_enabled': True, + 'consensus.state_sync_timeout': { + 'secs': 0, + 'nanos': 500000000 + }, + 'tracked_shard_schedule': [ + [0], + [0], + [1], + [2], + [3], + [1], + [2], + [3], + ], + 'tracked_shards': [], + } + + config = load_config() + near_root, node_dirs = init_cluster(1, 1, 4, config, + [["epoch_length", EPOCH_LENGTH]], { + 0: config0, + 1: config1 + }) + + boot_node = spin_up_node(config, near_root, node_dirs[0], 0) + logger.info('started boot_node') + node1 = spin_up_node(config, + near_root, + node_dirs[1], + 1, + boot_node=boot_node) + logger.info('started node1') + + contract = utils.load_test_contract() + + latest_block_hash = boot_node.get_latest_block().hash_bytes + deploy_contract_tx = transaction.sign_deploy_contract_tx( + boot_node.signer_key, contract, 10, latest_block_hash) + result = boot_node.send_tx_and_wait(deploy_contract_tx, 10) + assert 'result' in result and 'error' not in result, ( + 'Expected "result" and no "error" in response, got: {}'.format(result)) + + latest_block_hash = boot_node.get_latest_block().hash_bytes + deploy_contract_tx = transaction.sign_deploy_contract_tx( + node1.signer_key, contract, 10, latest_block_hash) + result = boot_node.send_tx_and_wait(deploy_contract_tx, 10) + assert 'result' in result and 'error' not in result, ( + 'Expected "result" and no "error" in response, got: {}'.format(result)) + + nonce, keys = random_workload_until(EPOCH_LENGTH - 5, 1, [], boot_node, + node1, boot_node) + + node1_height = node1.get_latest_block().height + logger.info(f'node1@{node1_height}') + node1.kill() + logger.info(f'killed node1') + + # Run node0 more to trigger block sync in node1. + nonce, keys = random_workload_until(int(EPOCH_LENGTH * 3), nonce, keys, + boot_node, node1, boot_node) + + # Node1 is now behind and needs to do header sync and block sync. + node1.start(boot_node=boot_node) + node1_height = node1.get_latest_block().height + logger.info(f'started node1@{node1_height}') + + nonce, keys = random_workload_until(int(EPOCH_LENGTH * 3.7), nonce, keys, + boot_node, node1, node1) + + +if __name__ == "__main__": + main() diff --git a/pytest/tests/stress/stress.py b/pytest/tests/stress/stress.py index 8c96ed6c2ff..5b3545cd62d 100755 --- a/pytest/tests/stress/stress.py +++ b/pytest/tests/stress/stress.py @@ -47,7 +47,7 @@ # Is only applicable in the scenarios where we expect failures in tx sends. SEND_TX_ATTEMPTS = 10 -# Block_header_fetch_horizon + state_fetch_horizon (which is equalto 5) need to be shorter than the epoch length. +# Block_header_fetch_horizon need to be shorter than the epoch length. # otherwise say epoch boundaries are H and H'. If the current height is H' + eps, and a node finished header sync at # H' + eps - block_header_fetch_horizon, and then rolled state_fetch_horizon back, it will end up before H, and will # try to state sync at the beginning of the epoch *two* epochs ago. No node will respond to such state requests. diff --git a/pytest/tools/mirror/contract/src/lib.rs b/pytest/tools/mirror/contract/src/lib.rs index cc062999f7a..2ecf6fa1eba 100644 --- a/pytest/tools/mirror/contract/src/lib.rs +++ b/pytest/tools/mirror/contract/src/lib.rs @@ -26,12 +26,13 @@ impl KeyAdder { } #[payable] - pub fn create_account(&mut self, account_id: AccountId, public_key: String) -> Promise { - let public_key = PublicKey::from_str(&public_key).unwrap(); - Promise::new(account_id) - .create_account() - .add_full_access_key(public_key) - .transfer(env::attached_deposit()) + pub fn create_account(&mut self, account_id: AccountId, public_key: Option) -> Promise { + let mut p = Promise::new(account_id).create_account(); + if let Some(public_key) = public_key { + let public_key = PublicKey::from_str(&public_key).unwrap(); + p = p.add_full_access_key(public_key); + } + p.transfer(env::attached_deposit()) } pub fn stake(&mut self, amount: Balance, public_key: PublicKey) -> Promise { diff --git a/pytest/tools/mirror/mirror_utils.py b/pytest/tools/mirror/mirror_utils.py index 0416608e479..0c43d427ea1 100644 --- a/pytest/tools/mirror/mirror_utils.py +++ b/pytest/tools/mirror/mirror_utils.py @@ -227,7 +227,7 @@ def __init__(self, near_root, source_home, online_source): def start(self): env = os.environ.copy() - env["RUST_LOG"] = "actix_web=warn,mio=warn,tokio_util=warn,actix_server=warn,actix_http=warn," + env.get( + env["RUST_LOG"] = "actix_web=warn,mio=warn,tokio_util=warn,actix_server=warn,actix_http=warn,indexer=info," + env.get( "RUST_LOG", "debug") with open(dot_near() / f'{MIRROR_DIR}/stdout', 'ab') as stdout, \ open(dot_near() / f'{MIRROR_DIR}/stderr', 'ab') as stderr: @@ -363,9 +363,9 @@ def call_addkey(node, signer_key, new_key, nonce, block_hash, extra_actions=[]): logger.info(f'called add_key for {new_key.account_id} {new_key.pk}: {res}') -def call_create_account(node, signer_key, account_id, nonce, block_hash): - k = key.Key.from_random(account_id) - args = json.dumps({'account_id': account_id, 'public_key': k.pk}) +def call_create_account(node, signer_key, account_id, public_key, nonce, + block_hash): + args = json.dumps({'account_id': account_id, 'public_key': public_key}) args = bytearray(args, encoding='utf-8') actions = [ @@ -380,8 +380,8 @@ def call_create_account(node, signer_key, account_id, nonce, block_hash): signer_key.decoded_sk()) res = node.send_tx(tx) logger.info( - f'called create account contract for {account_id} {k.pk}: {res}') - return k + f'called create account contract for {account_id}, public key: {public_key}: {res}' + ) def call_stake(node, signer_key, amount, public_key, nonce, block_hash): @@ -519,6 +519,8 @@ class TrafficData: def __init__(self, num_accounts): self.nonces = [2] * num_accounts self.implicit_account = None + self.keyless_account0 = 'keyless0.test0' + self.keyless_account1 = 'keyless1.test0' def send_transfers(self, nodes, block_hash, skip_senders=None): for sender in range(len(self.nonces)): @@ -533,6 +535,13 @@ def send_transfers(self, nodes, block_hash, skip_senders=None): nodes[sender].send_tx(tx) self.nonces[sender] += 1 + def check_ok(self, source_node, target_node): + keys = target_node.get_access_key_list(self.keyless_account0) + assert len(keys['result']['keys']) > 0, keys + keys = target_node.get_access_key_list(self.keyless_account1) + assert len(keys['result']['keys']) > 0, keys + check_num_txs(source_node, target_node) + def added_keys_send_transfers(nodes, added_keys, receivers, amount, block_hash): node_idx = 0 @@ -667,15 +676,30 @@ def send_traffic(near_root, source_nodes, traffic_data, callback): traffic_data.nonces[1] += 1 test1_contract_key = AddedKey(test1_contract_key) - test0_subaccount_contract_key = AddedKey( - call_create_account(source_nodes[1], source_nodes[0].signer_key, - 'test0.test0', traffic_data.nonces[0], - block_hash_bytes)) + test0_subaccount_contract_key = AddedKey(key.Key.from_random('test0.test0')) + call_create_account(source_nodes[1], source_nodes[0].signer_key, + test0_subaccount_contract_key.key.account_id, + test0_subaccount_contract_key.key.pk, + traffic_data.nonces[0], block_hash_bytes) + traffic_data.nonces[0] += 1 + test1_subaccount_contract_key = AddedKey(key.Key.from_random('test1.test0')) + call_create_account(source_nodes[1], source_nodes[1].signer_key, + test1_subaccount_contract_key.key.account_id, + test1_subaccount_contract_key.key.pk, + traffic_data.nonces[1], block_hash_bytes) + traffic_data.nonces[1] += 1 + + # here we create an account from a contract without adding an access key, + # and then check that the mirror binary adds a full access key so we can control the account + call_create_account(source_nodes[1], source_nodes[0].signer_key, + traffic_data.keyless_account0, None, + traffic_data.nonces[0], block_hash_bytes) traffic_data.nonces[0] += 1 - test1_subaccount_contract_key = AddedKey( - call_create_account(source_nodes[1], source_nodes[1].signer_key, - 'test1.test0', traffic_data.nonces[1], - block_hash_bytes)) + # now do the same thing but with signer different from the contract account + # to exercise different code paths + call_create_account(source_nodes[1], source_nodes[1].signer_key, + traffic_data.keyless_account1, None, + traffic_data.nonces[1], block_hash_bytes) traffic_data.nonces[1] += 1 test0_deleted_height = None diff --git a/pytest/tools/mirror/offline_test.py b/pytest/tools/mirror/offline_test.py index 41e09f31f5f..59468fff39f 100755 --- a/pytest/tools/mirror/offline_test.py +++ b/pytest/tools/mirror/offline_test.py @@ -64,7 +64,7 @@ def main(): ) break - mirror_utils.check_num_txs(source_nodes[0], target_nodes[0]) + traffic_data.check_ok(source_nodes[0], target_nodes[0]) if __name__ == '__main__': diff --git a/pytest/tools/mirror/online_test.py b/pytest/tools/mirror/online_test.py index 66eff9387cd..d089ce12d27 100755 --- a/pytest/tools/mirror/online_test.py +++ b/pytest/tools/mirror/online_test.py @@ -51,7 +51,7 @@ def main(): f'waiting for {int(time_left)} seconds to allow transactions to make it to the target chain' ) time.sleep(time_left) - mirror_utils.check_num_txs(source_nodes[0], target_nodes[0]) + traffic_data.check_ok(source_nodes[0], target_nodes[0]) if __name__ == '__main__': diff --git a/runtime/near-test-contracts/src/lib.rs b/runtime/near-test-contracts/src/lib.rs index 1d975e24787..5a1314ef42a 100644 --- a/runtime/near-test-contracts/src/lib.rs +++ b/runtime/near-test-contracts/src/lib.rs @@ -220,6 +220,35 @@ impl LargeContract { } } +/// Generate contracts with function bodies of large continuous sequences of `nop` instruction. +/// +/// This is particularly useful for testing how gas instrumentation works and its corner cases. +pub fn function_with_a_lot_of_nop(nops: u64) -> Vec { + use wasm_encoder::{ + CodeSection, ExportKind, ExportSection, Function, FunctionSection, Instruction, Module, + TypeSection, + }; + let mut module = Module::new(); + let mut type_section = TypeSection::new(); + type_section.function([], []); + module.section(&type_section); + let mut functions_section = FunctionSection::new(); + functions_section.function(0); + module.section(&functions_section); + let mut exports_section = ExportSection::new(); + exports_section.export("main", ExportKind::Func, 0); + module.section(&exports_section); + let mut code_section = CodeSection::new(); + let mut f = Function::new([]); + for _ in 0..nops { + f.instruction(&Instruction::Nop); + } + f.instruction(&Instruction::End); + code_section.function(&f); + module.section(&code_section); + module.finish() +} + /// Generate an arbitrary valid contract. pub fn arbitrary_contract(seed: u64) -> Vec { let mut rng = rand::rngs::SmallRng::seed_from_u64(seed); diff --git a/runtime/near-vm-runner/Cargo.toml b/runtime/near-vm-runner/Cargo.toml index 95c3dd4f7d1..f9f0cf6cb88 100644 --- a/runtime/near-vm-runner/Cargo.toml +++ b/runtime/near-vm-runner/Cargo.toml @@ -26,6 +26,7 @@ parity-wasm.workspace = true prefix-sum-vec.workspace = true ripemd.workspace = true serde.workspace = true +serde_repr.workspace = true serde_with.workspace = true sha2.workspace = true sha3.workspace = true @@ -67,6 +68,12 @@ expect-test.workspace = true hex.workspace = true near-primitives.workspace = true near-test-contracts.workspace = true +# Woah. The tests in this crate depend on (the public interface of) this crate itself. This is +# because the `test_builder` loads `RuntimeConfig` which comes from `near-primitives` and that +# `RuntimeConfig` contains types from `[lib]` output of this package. These types are then +# different from `crate::*` stuff that you get in the tests themselves, so in some special +# circumstances it is necessary to refer to the `[lib]` output rather than using `crate::*` stuff. +near-vm-runner.workspace = true rand.workspace = true serde_json = { workspace = true, features = ["preserve_order"] } wasm-smith.workspace = true diff --git a/runtime/near-vm-runner/fuzz/fuzz_targets/diffrunner.rs b/runtime/near-vm-runner/fuzz/fuzz_targets/diffrunner.rs index b9b5310f092..2d782043b52 100644 --- a/runtime/near-vm-runner/fuzz/fuzz_targets/diffrunner.rs +++ b/runtime/near-vm-runner/fuzz/fuzz_targets/diffrunner.rs @@ -1,12 +1,12 @@ #![no_main] -use near_primitives::contract::ContractCode; use near_primitives::runtime::fees::RuntimeFeesConfig; use near_primitives::version::PROTOCOL_VERSION; use near_vm_runner::internal::VMKind; use near_vm_runner::logic::errors::FunctionCallError; use near_vm_runner::logic::mocks::mock_external::MockedExternal; -use near_vm_runner::logic::{VMConfig, VMOutcome}; +use near_vm_runner::logic::{Config, VMOutcome}; +use near_vm_runner::ContractCode; use near_vm_runner_fuzz::{create_context, find_entry_point, ArbitraryModule}; libfuzzer_sys::fuzz_target!(|module: ArbitraryModule| { @@ -20,7 +20,7 @@ fn run_fuzz(code: &ContractCode, vm_kind: VMKind) -> VMOutcome { let mut fake_external = MockedExternal::new(); let mut context = create_context(vec![]); context.prepaid_gas = 10u64.pow(14); - let mut config = VMConfig::test(); + let mut config = Config::test(); config.limit_config.contract_prepare_version = near_vm_runner::logic::ContractPrepareVersion::V2; let fees = RuntimeFeesConfig::test(); diff --git a/runtime/near-vm-runner/fuzz/fuzz_targets/runner.rs b/runtime/near-vm-runner/fuzz/fuzz_targets/runner.rs index 9dd57eb594f..6da19e1d06e 100644 --- a/runtime/near-vm-runner/fuzz/fuzz_targets/runner.rs +++ b/runtime/near-vm-runner/fuzz/fuzz_targets/runner.rs @@ -1,11 +1,11 @@ #![no_main] -use near_primitives::contract::ContractCode; use near_primitives::runtime::fees::RuntimeFeesConfig; use near_primitives::version::PROTOCOL_VERSION; use near_vm_runner::internal::VMKind; use near_vm_runner::logic::mocks::mock_external::MockedExternal; -use near_vm_runner::logic::{VMConfig, VMOutcome}; +use near_vm_runner::logic::{Config, VMOutcome}; +use near_vm_runner::ContractCode; use near_vm_runner_fuzz::{create_context, find_entry_point, ArbitraryModule}; libfuzzer_sys::fuzz_target!(|module: ArbitraryModule| { @@ -17,7 +17,7 @@ fn run_fuzz(code: &ContractCode, vm_kind: VMKind) -> VMOutcome { let mut fake_external = MockedExternal::new(); let mut context = create_context(vec![]); context.prepaid_gas = 10u64.pow(14); - let mut config = VMConfig::test(); + let mut config = Config::test(); config.limit_config.wasmer2_stack_limit = i32::MAX; // If we can crash wasmer2 even without the secondary stack limit it's still good to know let fees = RuntimeFeesConfig::test(); diff --git a/runtime/near-vm-runner/fuzz/src/lib.rs b/runtime/near-vm-runner/fuzz/src/lib.rs index 5d4636d6e0f..a7eb0a8e92f 100644 --- a/runtime/near-vm-runner/fuzz/src/lib.rs +++ b/runtime/near-vm-runner/fuzz/src/lib.rs @@ -1,7 +1,7 @@ use core::fmt; -use near_primitives::contract::ContractCode; use near_vm_runner::internal::wasmparser::{Export, ExternalKind, Parser, Payload, TypeDef}; use near_vm_runner::logic::VMContext; +use near_vm_runner::ContractCode; /// Finds a no-parameter exported function, something like `(func (export "entry-point"))`. pub fn find_entry_point(contract: &ContractCode) -> Option { diff --git a/runtime/near-vm-runner/src/cache.rs b/runtime/near-vm-runner/src/cache.rs index 7ffc9ba2fc3..7dfa3935240 100644 --- a/runtime/near-vm-runner/src/cache.rs +++ b/runtime/near-vm-runner/src/cache.rs @@ -1,9 +1,9 @@ use crate::errors::ContractPrecompilatonResult; use crate::logic::errors::{CacheError, CompilationError}; -use crate::logic::{CompiledContract, CompiledContractCache, ProtocolVersion, VMConfig}; +use crate::logic::{CompiledContract, CompiledContractCache, Config, ProtocolVersion}; use crate::vm_kind::VMKind; +use crate::ContractCode; use borsh::BorshSerialize; -use near_primitives_core::contract::ContractCode; use near_primitives_core::hash::CryptoHash; use std::collections::HashMap; use std::fmt; @@ -43,11 +43,7 @@ fn vm_hash(vm_kind: VMKind) -> u64 { } } -pub fn get_contract_cache_key( - code: &ContractCode, - vm_kind: VMKind, - config: &VMConfig, -) -> CryptoHash { +pub fn get_contract_cache_key(code: &ContractCode, vm_kind: VMKind, config: &Config) -> CryptoHash { let _span = tracing::debug_span!(target: "vm", "get_key").entered(); let key = ContractCacheKey::Version4 { code_hash: *code.hash(), @@ -93,7 +89,7 @@ impl fmt::Debug for MockCompiledContractCache { /// is already in the cache, or if cache is `None`. pub fn precompile_contract( code: &ContractCode, - config: &VMConfig, + config: &Config, current_protocol_version: ProtocolVersion, cache: Option<&dyn CompiledContractCache>, ) -> Result, CacheError> { diff --git a/core/primitives-core/src/contract.rs b/runtime/near-vm-runner/src/code.rs similarity index 89% rename from core/primitives-core/src/contract.rs rename to runtime/near-vm-runner/src/code.rs index e6a54093821..820457894be 100644 --- a/core/primitives-core/src/contract.rs +++ b/runtime/near-vm-runner/src/code.rs @@ -1,4 +1,4 @@ -use crate::hash::{hash as sha256, CryptoHash}; +use near_primitives_core::hash::{hash as sha256, CryptoHash}; pub struct ContractCode { code: Vec, diff --git a/runtime/near-vm-runner/src/config.rs b/runtime/near-vm-runner/src/config.rs new file mode 100644 index 00000000000..e0bdc1e66a6 --- /dev/null +++ b/runtime/near-vm-runner/src/config.rs @@ -0,0 +1,258 @@ +use crate::logic::StorageGetMode; +use near_primitives_core::config::{AccountIdValidityRulesVersion, ExtCostsConfig, ParameterCost}; +use near_primitives_core::types::Gas; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + +/// Dynamic configuration parameters required for the WASM runtime to +/// execute a smart contract. +/// +/// This (`VMConfig`) and `RuntimeFeesConfig` combined are sufficient to define +/// protocol specific behavior of the contract runtime. The former contains +/// configuration for the WASM runtime specifically, while the latter contains +/// configuration for the transaction runtime and WASM runtime. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct Config { + /// Costs for runtime externals + pub ext_costs: ExtCostsConfig, + + /// Gas cost of a growing memory by single page. + pub grow_mem_cost: u32, + /// Gas cost of a regular operation. + pub regular_op_cost: u32, + + /// Disable the fix for the #9393 issue in near-vm-runner. + pub disable_9393_fix: bool, + + /// Set to `StorageGetMode::FlatStorage` in order to enable the `FlatStorageReads` protocol + /// feature. + pub storage_get_mode: StorageGetMode, + + /// Enable the `FixContractLoadingCost` protocol feature. + pub fix_contract_loading_cost: bool, + + /// Enable the `ImplicitAccountCreation` protocol feature. + pub implicit_account_creation: bool, + + /// Describes limits for VM and Runtime. + pub limit_config: LimitConfig, +} + +/// Our original code for limiting WASM stack was buggy. We fixed that, but we +/// still have to use old (`V0`) limiter for old protocol versions. +/// +/// This struct here exists to enforce that the value in the config is either +/// `0` or `1`. We could have used a `bool` instead, but there's a chance that +/// our current impl isn't perfect either and would need further tweaks in the +/// future. +#[derive( + Debug, + Clone, + Copy, + Hash, + PartialEq, + Eq, + serde_repr::Serialize_repr, + serde_repr::Deserialize_repr, +)] +#[repr(u8)] +pub enum ContractPrepareVersion { + /// Oldest, buggiest version. + /// + /// Don't use it unless specifically to support old protocol version. + V0, + /// Old, slow and buggy version. + /// + /// Better than V0, but don’t use this nevertheless. + V1, + /// finite-wasm 0.3.0 based contract preparation code. + V2, +} + +impl ContractPrepareVersion { + pub fn v0() -> ContractPrepareVersion { + ContractPrepareVersion::V0 + } +} + +/// Describes limits for VM and Runtime. +/// TODO #4139: consider switching to strongly-typed wrappers instead of raw quantities +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Hash, PartialEq, Eq)] +pub struct LimitConfig { + /// Max amount of gas that can be used, excluding gas attached to promises. + pub max_gas_burnt: Gas, + + /// How tall the stack is allowed to grow? + /// + /// See to find out how the stack frame cost + /// is calculated. + pub max_stack_height: u32, + /// Whether a legacy version of stack limiting should be used, see + /// [`ContractPrepareVersion`]. + #[serde(default = "ContractPrepareVersion::v0")] + pub contract_prepare_version: ContractPrepareVersion, + + /// The initial number of memory pages. + /// NOTE: It's not a limiter itself, but it's a value we use for initial_memory_pages. + pub initial_memory_pages: u32, + /// What is the maximal memory pages amount is allowed to have for a contract. + pub max_memory_pages: u32, + + /// Limit of memory used by registers. + pub registers_memory_limit: u64, + /// Maximum number of bytes that can be stored in a single register. + pub max_register_size: u64, + /// Maximum number of registers that can be used simultaneously. + /// + /// Note that due to an implementation quirk [read: a bug] in VMLogic, if we + /// have this number of registers, no subsequent writes to the registers + /// will succeed even if they replace an existing register. + pub max_number_registers: u64, + + /// Maximum number of log entries. + pub max_number_logs: u64, + /// Maximum total length in bytes of all log messages. + pub max_total_log_length: u64, + + /// Max total prepaid gas for all function call actions per receipt. + pub max_total_prepaid_gas: Gas, + + /// Max number of actions per receipt. + pub max_actions_per_receipt: u64, + /// Max total length of all method names (including terminating character) for a function call + /// permission access key. + pub max_number_bytes_method_names: u64, + /// Max length of any method name (without terminating character). + pub max_length_method_name: u64, + /// Max length of arguments in a function call action. + pub max_arguments_length: u64, + /// Max length of returned data + pub max_length_returned_data: u64, + /// Max contract size + pub max_contract_size: u64, + /// Max transaction size + pub max_transaction_size: u64, + /// Max storage key size + pub max_length_storage_key: u64, + /// Max storage value size + pub max_length_storage_value: u64, + /// Max number of promises that a function call can create + pub max_promises_per_function_call_action: u64, + /// Max number of input data dependencies + pub max_number_input_data_dependencies: u64, + /// If present, stores max number of functions in one contract + #[serde(skip_serializing_if = "Option::is_none")] + pub max_functions_number_per_contract: Option, + /// If present, stores the secondary stack limit as implemented by wasmer2. + /// + /// This limit should never be hit normally. + #[serde(default = "wasmer2_stack_limit_default")] + pub wasmer2_stack_limit: i32, + /// If present, stores max number of locals declared globally in one contract + #[serde(skip_serializing_if = "Option::is_none")] + pub max_locals_per_contract: Option, + /// Whether to enforce account_id well-formedness where it wasn't enforced + /// historically. + #[serde(default = "AccountIdValidityRulesVersion::v0")] + pub account_id_validity_rules_version: AccountIdValidityRulesVersion, +} + +fn wasmer2_stack_limit_default() -> i32 { + 100 * 1024 +} + +impl Config { + pub fn test() -> Self { + Self { + ext_costs: ExtCostsConfig::test(), + grow_mem_cost: 1, + // Refer to `near_primitives_core::config::SAFETY_MULTIPLIER`. + regular_op_cost: 3 * 1285457, + disable_9393_fix: false, + limit_config: LimitConfig::test(), + fix_contract_loading_cost: cfg!(feature = "protocol_feature_fix_contract_loading_cost"), + storage_get_mode: StorageGetMode::FlatStorage, + implicit_account_creation: true, + } + } + + /// Computes non-cryptographically-proof hash. The computation is fast but not cryptographically + /// secure. + pub fn non_crypto_hash(&self) -> u64 { + let mut s = DefaultHasher::new(); + self.hash(&mut s); + s.finish() + } + + pub fn free() -> Self { + Self { + ext_costs: ExtCostsConfig { + costs: near_primitives_core::enum_map::enum_map! { + _ => ParameterCost { gas: 0, compute: 0 } + }, + }, + grow_mem_cost: 0, + regular_op_cost: 0, + disable_9393_fix: false, + // We shouldn't have any costs in the limit config. + limit_config: LimitConfig { max_gas_burnt: u64::MAX, ..LimitConfig::test() }, + fix_contract_loading_cost: cfg!(feature = "protocol_feature_fix_contract_loading_cost"), + storage_get_mode: StorageGetMode::FlatStorage, + implicit_account_creation: true, + } + } +} + +impl LimitConfig { + pub fn test() -> Self { + const KB: u32 = 1024; + let max_contract_size = 4 * 2u64.pow(20); + Self { + max_gas_burnt: 2 * 10u64.pow(14), // with 10**15 block gas limit this will allow 5 calls. + max_stack_height: 256 * KB, + contract_prepare_version: ContractPrepareVersion::V2, + initial_memory_pages: 2u32.pow(10), // 64Mib of memory. + max_memory_pages: 2u32.pow(11), // 128Mib of memory. + + // By default registers are limited by 1GiB of memory. + registers_memory_limit: 2u64.pow(30), + // By default each register is limited by 100MiB of memory. + max_register_size: 2u64.pow(20) * 100, + // By default there is at most 100 registers. + max_number_registers: 100, + + max_number_logs: 100, + // Total logs size is 16Kib + max_total_log_length: 16 * 1024, + + // Updating the maximum prepaid gas to limit the maximum depth of a transaction to 64 + // blocks. + // This based on `63 * min_receipt_with_function_call_gas()`. Where 63 is max depth - 1. + max_total_prepaid_gas: 300 * 10u64.pow(12), + + // Safety limit. Unlikely to hit it for most common transactions and receipts. + max_actions_per_receipt: 100, + // Should be low enough to deserialize an access key without paying. + max_number_bytes_method_names: 2000, + max_length_method_name: 256, // basic safety limit + max_arguments_length: 4 * 2u64.pow(20), // 4 Mib + max_length_returned_data: 4 * 2u64.pow(20), // 4 Mib + max_contract_size, // 4 Mib, + max_transaction_size: 4 * 2u64.pow(20), // 4 Mib + + max_length_storage_key: 4 * 2u64.pow(20), // 4 Mib + max_length_storage_value: 4 * 2u64.pow(20), // 4 Mib + // Safety limit and unlikely abusable. + max_promises_per_function_call_action: 1024, + // Unlikely to hit it for normal development. + max_number_input_data_dependencies: 128, + max_functions_number_per_contract: Some(10000), + wasmer2_stack_limit: 200 * 1024, + // To utilize a local in an useful way, at least two `local.*` instructions are + // necessary (they only take constant operands indicating the local to access), which + // is 4 bytes worth of code for each local. + max_locals_per_contract: Some(max_contract_size / 4), + account_id_validity_rules_version: AccountIdValidityRulesVersion::V1, + } + } +} diff --git a/runtime/near-vm-runner/src/lib.rs b/runtime/near-vm-runner/src/lib.rs index d656008a3c4..1645a9459f0 100644 --- a/runtime/near-vm-runner/src/lib.rs +++ b/runtime/near-vm-runner/src/lib.rs @@ -1,6 +1,8 @@ #![doc = include_str!("../README.md")] mod cache; +mod code; +mod config; mod errors; mod features; mod imports; @@ -24,8 +26,9 @@ mod wasmer_runner; mod wasmtime_runner; pub use crate::logic::with_ext_cost_counter; - pub use cache::{get_contract_cache_key, precompile_contract, MockCompiledContractCache}; +pub use code::ContractCode; +pub use config::ContractPrepareVersion; pub use runner::{run, VM}; /// This is public for internal experimentation use only, and should otherwise be considered an diff --git a/runtime/near-vm-runner/src/logic/dependencies.rs b/runtime/near-vm-runner/src/logic/dependencies.rs index 4567fbaeb7a..e75c29eb6e6 100644 --- a/runtime/near-vm-runner/src/logic/dependencies.rs +++ b/runtime/near-vm-runner/src/logic/dependencies.rs @@ -1,8 +1,13 @@ //! External dependencies of the near-vm-logic. +use super::types::ReceiptIndex; use super::TrieNodesCount; use super::VMLogicError; +use near_crypto::PublicKey; use near_primitives_core::hash::CryptoHash; +use near_primitives_core::types::Gas; +use near_primitives_core::types::GasWeight; +use near_primitives_core::types::Nonce; use near_primitives_core::types::{AccountId, Balance}; use std::borrow::Cow; @@ -96,6 +101,7 @@ pub trait MemoryLike { } /// This enum represents if a storage_get call will be performed through flat storage or trie +#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] pub enum StorageGetMode { FlatStorage, Trie, @@ -249,4 +255,193 @@ pub trait External { /// Returns total stake of validators in the current epoch. fn validator_total_stake(&self) -> Result; + + /// Create a receipt which will be executed after all the receipts identified by + /// `receipt_indices` are complete. + /// + /// If any of the [`ReceiptIndex`]es do not refer to a known receipt, this function will fail + /// with an error. + /// + /// # Arguments + /// + /// * `generate_data_id` - function to generate a data id to connect receipt output to + /// * `receipt_indices` - a list of receipt indices the new receipt is depend on + /// * `receiver_id` - account id of the receiver of the receipt created + fn create_receipt( + &mut self, + receipt_indices: Vec, + receiver_id: AccountId, + ) -> Result; + + /// Attach the [`CreateAccountAction`] action to an existing receipt. + /// + /// # Arguments + /// + /// * `receipt_index` - an index of Receipt to append an action + /// + /// # Panics + /// + /// Panics if the `receipt_index` does not refer to a known receipt. + fn append_action_create_account( + &mut self, + receipt_index: ReceiptIndex, + ) -> Result<(), VMLogicError>; + + /// Attach the [`DeployContractAction`] action to an existing receipt. + /// + /// # Arguments + /// + /// * `receipt_index` - an index of Receipt to append an action + /// * `code` - a Wasm code to attach + /// + /// # Panics + /// + /// Panics if the `receipt_index` does not refer to a known receipt. + fn append_action_deploy_contract( + &mut self, + receipt_index: ReceiptIndex, + code: Vec, + ) -> Result<(), VMLogicError>; + + /// Attach the [`FunctionCallAction`] action to an existing receipt. + /// + /// `prepaid_gas` and `gas_weight` can either be specified or both. If a `gas_weight` is + /// specified, the action should be allocated gas in + /// [`distribute_unused_gas`](Self::distribute_unused_gas). + /// + /// For more information, see [super::VMLogic::promise_batch_action_function_call_weight]. + /// + /// # Arguments + /// + /// * `receipt_index` - an index of Receipt to append an action + /// * `method_name` - a name of the contract method to call + /// * `arguments` - a Wasm code to attach + /// * `attached_deposit` - amount of tokens to transfer with the call + /// * `prepaid_gas` - amount of prepaid gas to attach to the call + /// * `gas_weight` - relative weight of unused gas to distribute to the function call action + /// + /// # Panics + /// + /// Panics if the `receipt_index` does not refer to a known receipt. + fn append_action_function_call_weight( + &mut self, + receipt_index: ReceiptIndex, + method_name: Vec, + args: Vec, + attached_deposit: Balance, + prepaid_gas: Gas, + gas_weight: GasWeight, + ) -> Result<(), VMLogicError>; + + /// Attach the [`TransferAction`] action to an existing receipt. + /// + /// # Arguments + /// + /// * `receipt_index` - an index of Receipt to append an action + /// * `amount` - amount of tokens to transfer + /// + /// # Panics + /// + /// Panics if the `receipt_index` does not refer to a known receipt. + fn append_action_transfer( + &mut self, + receipt_index: ReceiptIndex, + deposit: Balance, + ) -> Result<(), VMLogicError>; + + /// Attach the [`StakeAction`] action to an existing receipt. + /// + /// # Arguments + /// + /// * `receipt_index` - an index of Receipt to append an action + /// * `stake` - amount of tokens to stake + /// * `public_key` - a validator public key + /// + /// # Panics + /// + /// Panics if the `receipt_index` does not refer to a known receipt. + fn append_action_stake( + &mut self, + receipt_index: ReceiptIndex, + stake: Balance, + public_key: PublicKey, + ); + + /// Attach the [`AddKeyAction`] action to an existing receipt. + /// + /// # Arguments + /// + /// * `receipt_index` - an index of Receipt to append an action + /// * `public_key` - a public key for an access key + /// * `nonce` - a nonce + /// + /// # Panics + /// + /// Panics if the `receipt_index` does not refer to a known receipt. + fn append_action_add_key_with_full_access( + &mut self, + receipt_index: ReceiptIndex, + public_key: PublicKey, + nonce: Nonce, + ); + + /// Attach the [`AddKeyAction`] action an existing receipt. + /// + /// The access key associated with the action will have the + /// [`AccessKeyPermission::FunctionCall`] permission scope. + /// + /// # Arguments + /// + /// * `receipt_index` - an index of Receipt to append an action + /// * `public_key` - a public key for an access key + /// * `nonce` - a nonce + /// * `allowance` - amount of tokens allowed to spend by this access key + /// * `receiver_id` - a contract witch will be allowed to call with this access key + /// * `method_names` - a list of method names is allowed to call with this access key (empty = any method) + /// + /// # Panics + /// + /// Panics if the `receipt_index` does not refer to a known receipt. + fn append_action_add_key_with_function_call( + &mut self, + receipt_index: ReceiptIndex, + public_key: PublicKey, + nonce: Nonce, + allowance: Option, + receiver_id: AccountId, + method_names: Vec>, + ) -> Result<(), VMLogicError>; + + /// Attach the [`DeleteKeyAction`] action to an existing receipt. + /// + /// # Arguments + /// + /// * `receipt_index` - an index of Receipt to append an action + /// * `public_key` - a public key for an access key to delete + /// + /// # Panics + /// + /// Panics if the `receipt_index` does not refer to a known receipt. + fn append_action_delete_key(&mut self, receipt_index: ReceiptIndex, public_key: PublicKey); + + /// Attach the [`DeleteAccountAction`] action to an existing receipt + /// + /// # Arguments + /// + /// * `receipt_index` - an index of Receipt to append an action + /// * `beneficiary_id` - an account id to which the rest of the funds of the removed account will be transferred + /// + /// # Panics + /// + /// Panics if the `receipt_index` does not refer to a known receipt. + fn append_action_delete_account( + &mut self, + receipt_index: ReceiptIndex, + beneficiary_id: AccountId, + ) -> Result<(), VMLogicError>; + + /// # Panic + /// + /// Panics if `ReceiptIndex` is invalid. + fn get_receipt_receiver(&self, receipt_index: ReceiptIndex) -> &AccountId; } diff --git a/runtime/near-vm-runner/src/logic/logic.rs b/runtime/near-vm-runner/src/logic/logic.rs index a6ad99bb3b4..528945108bd 100644 --- a/runtime/near-vm-runner/src/logic/logic.rs +++ b/runtime/near-vm-runner/src/logic/logic.rs @@ -2,22 +2,20 @@ use super::context::VMContext; use super::dependencies::{External, MemSlice, MemoryLike}; use super::errors::{FunctionCallError, InconsistentStateError}; use super::gas_counter::{FastGasCounter, GasCounter}; -use super::receipt_manager::ReceiptManager; use super::types::{PromiseIndex, PromiseResult, ReceiptIndex, ReturnData}; use super::utils::split_method_names; use super::{HostError, VMLogicError}; -use super::{ReceiptMetadata, StorageGetMode, ValuePtr}; +use super::{StorageGetMode, ValuePtr}; +use crate::config::Config; use near_crypto::Secp256K1Signature; -use near_primitives_core::checked_feature; use near_primitives_core::config::ExtCosts::*; use near_primitives_core::config::ViewConfig; -use near_primitives_core::config::{ActionCosts, ExtCosts, VMConfig}; +use near_primitives_core::config::{ActionCosts, ExtCosts}; use near_primitives_core::profile::ProfileDataV3; use near_primitives_core::runtime::fees::RuntimeFeesConfig; use near_primitives_core::runtime::fees::{transfer_exec_fee, transfer_send_fee}; use near_primitives_core::types::{ - AccountId, Balance, Compute, EpochHeight, Gas, GasDistribution, GasWeight, ProtocolVersion, - StorageUsage, + AccountId, Balance, Compute, EpochHeight, Gas, GasWeight, StorageUsage, }; use std::mem::size_of; @@ -36,7 +34,7 @@ pub struct VMLogic<'a> { /// Part of Context API and Economics API that was extracted from the receipt. context: VMContext, /// All gas and economic parameters required during contract execution. - config: &'a VMConfig, + config: &'a Config, /// Fees for creating (async) actions on runtime. fees_config: &'a RuntimeFeesConfig, /// If this method execution is invoked directly as a callback by one or more contract calls the @@ -67,12 +65,6 @@ pub struct VMLogic<'a> { /// Tracks the total log length. The sum of length of all logs. total_log_length: u64, - /// Current protocol version that is used for the function call. - current_protocol_version: ProtocolVersion, - - /// Handles the receipts generated through execution. - receipt_manager: ReceiptManager, - /// Stores the amount of stack space remaining remaining_stack: u64, } @@ -133,14 +125,13 @@ impl PublicKeyBuffer { } impl<'a> VMLogic<'a> { - pub fn new_with_protocol_version( + pub fn new( ext: &'a mut dyn External, context: VMContext, - config: &'a VMConfig, + config: &'a Config, fees_config: &'a RuntimeFeesConfig, promise_results: &'a [PromiseResult], memory: &'a mut dyn MemoryLike, - current_protocol_version: ProtocolVersion, ) -> Self { // Overflow should be checked before calling VMLogic. let current_account_balance = context.account_balance + context.attached_deposit; @@ -174,8 +165,6 @@ impl<'a> VMLogic<'a> { registers: Default::default(), promises: vec![], total_log_length: 0, - current_protocol_version, - receipt_manager: ReceiptManager::default(), remaining_stack: u64::from(config.limit_config.max_stack_height), } } @@ -185,23 +174,13 @@ impl<'a> VMLogic<'a> { &self.logs } - /// Returns receipt metadata for created receipts - pub fn action_receipts(&self) -> &[(AccountId, ReceiptMetadata)] { - &self.receipt_manager.action_receipts - } - - #[cfg(test)] - pub(super) fn receipt_manager(&self) -> &ReceiptManager { - &self.receipt_manager - } - #[cfg(test)] pub(super) fn gas_counter(&self) -> &GasCounter { &self.gas_counter } #[cfg(test)] - pub(super) fn config(&self) -> &VMConfig { + pub(super) fn config(&self) -> &Config { &self.config } @@ -1478,7 +1457,7 @@ impl<'a> VMLogic<'a> { let account_id = self.read_and_parse_account_id(account_id_ptr, account_id_len)?; let sir = account_id == self.context.current_account_id; self.pay_gas_for_new_receipt(sir, &[])?; - let new_receipt_idx = self.receipt_manager.create_receipt(self.ext, vec![], account_id)?; + let new_receipt_idx = self.ext.create_receipt(vec![], account_id)?; self.checked_push_promise(Promise::Receipt(new_receipt_idx)) } @@ -1531,21 +1510,15 @@ impl<'a> VMLogic<'a> { let sir = account_id == self.context.current_account_id; let deps: Vec<_> = receipt_dependencies .iter() - .map(|&receipt_idx| self.get_account_by_receipt(receipt_idx) == &account_id) + .map(|&receipt_idx| self.ext.get_receipt_receiver(receipt_idx) == &account_id) .collect(); self.pay_gas_for_new_receipt(sir, &deps)?; - let new_receipt_idx = - self.receipt_manager.create_receipt(self.ext, receipt_dependencies, account_id)?; + let new_receipt_idx = self.ext.create_receipt(receipt_dependencies, account_id)?; self.checked_push_promise(Promise::Receipt(new_receipt_idx)) } - /// Helper function to return the account id towards which the receipt is directed. - fn get_account_by_receipt(&self, receipt_idx: ReceiptIndex) -> &AccountId { - self.receipt_manager.get_receipt_receiver(receipt_idx) - } - /// Helper function to return the receipt index corresponding to the given promise index. /// It also pulls account ID for the given receipt and compares it with the current account ID /// to return whether the receipt's account ID is the same. @@ -1562,7 +1535,7 @@ impl<'a> VMLogic<'a> { Promise::NotReceipt(_) => Err(HostError::CannotAppendActionToJointPromise), }?; - let account_id = self.get_account_by_receipt(receipt_idx); + let account_id = self.ext.get_receipt_receiver(receipt_idx); let sir = account_id == &self.context.current_account_id; Ok((receipt_idx, sir)) } @@ -1593,7 +1566,7 @@ impl<'a> VMLogic<'a> { self.pay_action_base(ActionCosts::create_account, sir)?; - self.receipt_manager.append_action_create_account(receipt_idx)?; + self.ext.append_action_create_account(receipt_idx)?; Ok(()) } @@ -1640,7 +1613,7 @@ impl<'a> VMLogic<'a> { self.pay_action_base(ActionCosts::deploy_contract_base, sir)?; self.pay_action_per_byte(ActionCosts::deploy_contract_byte, code_len, sir)?; - self.receipt_manager.append_action_deploy_contract(receipt_idx, code)?; + self.ext.append_action_deploy_contract(receipt_idx, code)?; Ok(()) } @@ -1758,7 +1731,7 @@ impl<'a> VMLogic<'a> { self.deduct_balance(amount)?; - self.receipt_manager.append_action_function_call_weight( + self.ext.append_action_function_call_weight( receipt_idx, method_name, arguments, @@ -1799,11 +1772,9 @@ impl<'a> VMLogic<'a> { let amount = self.memory.get_u128(&mut self.gas_counter, amount_ptr)?; let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; - let receiver_id = self.get_account_by_receipt(receipt_idx); + let receiver_id = self.ext.get_receipt_receiver(receipt_idx); let is_receiver_implicit = - checked_feature!("stable", ImplicitAccountCreation, self.current_protocol_version) - && receiver_id.is_implicit(); - + self.config.implicit_account_creation && receiver_id.is_implicit(); let send_fee = transfer_send_fee(self.fees_config, sir, is_receiver_implicit); let exec_fee = transfer_exec_fee(self.fees_config, is_receiver_implicit); let burn_gas = send_fee; @@ -1812,7 +1783,7 @@ impl<'a> VMLogic<'a> { self.deduct_balance(amount)?; - self.receipt_manager.append_action_transfer(receipt_idx, amount)?; + self.ext.append_action_transfer(receipt_idx, amount)?; Ok(()) } @@ -1851,7 +1822,7 @@ impl<'a> VMLogic<'a> { let public_key = self.get_public_key(public_key_ptr, public_key_len)?; let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; self.pay_action_base(ActionCosts::stake, sir)?; - self.receipt_manager.append_action_stake(receipt_idx, amount, public_key.decode()?); + self.ext.append_action_stake(receipt_idx, amount, public_key.decode()?); Ok(()) } @@ -1889,11 +1860,7 @@ impl<'a> VMLogic<'a> { let public_key = self.get_public_key(public_key_ptr, public_key_len)?; let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; self.pay_action_base(ActionCosts::add_full_access_key, sir)?; - self.receipt_manager.append_action_add_key_with_full_access( - receipt_idx, - public_key.decode()?, - nonce, - ); + self.ext.append_action_add_key_with_full_access(receipt_idx, public_key.decode()?, nonce); Ok(()) } @@ -1949,7 +1916,7 @@ impl<'a> VMLogic<'a> { self.pay_action_base(ActionCosts::add_function_call_key_base, sir)?; self.pay_action_per_byte(ActionCosts::add_function_call_key_byte, num_bytes, sir)?; - self.receipt_manager.append_action_add_key_with_function_call( + self.ext.append_action_add_key_with_function_call( receipt_idx, public_key.decode()?, nonce, @@ -1993,7 +1960,7 @@ impl<'a> VMLogic<'a> { let public_key = self.get_public_key(public_key_ptr, public_key_len)?; let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; self.pay_action_base(ActionCosts::delete_key, sir)?; - self.receipt_manager.append_action_delete_key(receipt_idx, public_key.decode()?); + self.ext.append_action_delete_key(receipt_idx, public_key.decode()?); Ok(()) } @@ -2032,7 +1999,7 @@ impl<'a> VMLogic<'a> { let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; self.pay_action_base(ActionCosts::delete_account, sir)?; - self.receipt_manager.append_action_delete_account(receipt_idx, beneficiary_id)?; + self.ext.append_action_delete_account(receipt_idx, beneficiary_id)?; Ok(()) } @@ -2505,13 +2472,7 @@ impl<'a> VMLogic<'a> { } self.gas_counter.pay_per(storage_read_key_byte, key.len() as u64)?; let nodes_before = self.ext.get_trie_nodes_count(); - let read_mode = - if checked_feature!("stable", FlatStorageReads, self.current_protocol_version) { - StorageGetMode::FlatStorage - } else { - StorageGetMode::Trie - }; - let read = self.ext.storage_get(&key, read_mode); + let read = self.ext.storage_get(&key, self.config.storage_get_mode); let nodes_delta = self .ext .get_trie_nodes_count() @@ -2654,13 +2615,7 @@ impl<'a> VMLogic<'a> { } self.gas_counter.pay_per(storage_has_key_byte, key.len() as u64)?; let nodes_before = self.ext.get_trie_nodes_count(); - let read_mode = - if checked_feature!("stable", FlatStorageReads, self.current_protocol_version) { - StorageGetMode::FlatStorage - } else { - StorageGetMode::Trie - }; - let res = self.ext.storage_has_key(&key, read_mode); + let res = self.ext.storage_has_key(&key, self.config.storage_get_mode); let nodes_delta = self .ext .get_trie_nodes_count() @@ -2793,18 +2748,7 @@ impl<'a> VMLogic<'a> { /// If `FunctionCallWeight` protocol feature (127) is enabled, unused gas will be /// distributed to functions that specify a gas weight. If there are no functions with /// a gas weight, the outcome will contain unused gas as usual. - pub fn compute_outcome_and_distribute_gas(mut self) -> VMOutcome { - if !self.context.is_view() { - // Distribute unused gas to scheduled function calls - let unused_gas = self.gas_counter.unused_gas(); - - // Spend all remaining gas by distributing it among function calls that specify - // a gas weight - if let GasDistribution::All = self.receipt_manager.distribute_unused_gas(unused_gas) { - self.gas_counter.prepay_gas(unused_gas).unwrap(); - } - } - + pub fn compute_outcome(self) -> VMOutcome { let burnt_gas = self.gas_counter.burnt_gas(); let used_gas = self.gas_counter.used_gas(); @@ -2821,7 +2765,6 @@ impl<'a> VMLogic<'a> { compute_usage, logs: self.logs, profile, - action_receipts: self.receipt_manager.action_receipts, aborted: None, } } @@ -2885,7 +2828,6 @@ impl<'a> VMLogic<'a> { pub fn before_loading_executable( &mut self, method_name: &str, - current_protocol_version: u32, wasm_code_bytes: usize, ) -> std::result::Result<(), super::errors::FunctionCallError> { if method_name.is_empty() { @@ -2894,11 +2836,7 @@ impl<'a> VMLogic<'a> { ); return Err(error); } - if checked_feature!( - "protocol_feature_fix_contract_loading_cost", - FixContractLoadingCost, - current_protocol_version - ) { + if self.config.fix_contract_loading_cost { if self.add_contract_loading_fee(wasm_code_bytes as u64).is_err() { let error = super::errors::FunctionCallError::HostError(super::HostError::GasExceeded); @@ -2911,14 +2849,9 @@ impl<'a> VMLogic<'a> { /// Legacy code to preserve old gas charging behaviour in old protocol versions. pub fn after_loading_executable( &mut self, - current_protocol_version: u32, wasm_code_bytes: usize, ) -> std::result::Result<(), super::errors::FunctionCallError> { - if !checked_feature!( - "protocol_feature_fix_contract_loading_cost", - FixContractLoadingCost, - current_protocol_version - ) { + if !self.config.fix_contract_loading_cost { if self.add_contract_loading_fee(wasm_code_bytes as u64).is_err() { return Err(super::errors::FunctionCallError::HostError( super::HostError::GasExceeded, @@ -2940,7 +2873,6 @@ pub struct VMOutcome { pub logs: Vec, /// Data collected from making a contract call pub profile: ProfileDataV3, - pub action_receipts: Vec<(AccountId, ReceiptMetadata)>, pub aborted: Option, } @@ -2948,7 +2880,7 @@ impl VMOutcome { /// Consumes the `VMLogic` object and computes the final outcome with the /// given error that stopped execution from finishing successfully. pub fn abort(logic: VMLogic, error: FunctionCallError) -> VMOutcome { - let mut outcome = logic.compute_outcome_and_distribute_gas(); + let mut outcome = logic.compute_outcome(); outcome.aborted = Some(error); outcome } @@ -2956,7 +2888,7 @@ impl VMOutcome { /// Consumes the `VMLogic` object and computes the final outcome for a /// successful execution. pub fn ok(logic: VMLogic) -> VMOutcome { - logic.compute_outcome_and_distribute_gas() + logic.compute_outcome() } /// Creates an outcome with a no-op outcome. @@ -2973,7 +2905,6 @@ impl VMOutcome { compute_usage: 0, logs: Vec::new(), profile: ProfileDataV3::default(), - action_receipts: Vec::new(), aborted: Some(error), } } @@ -2983,13 +2914,8 @@ impl VMOutcome { pub fn abort_but_nop_outcome_in_old_protocol( logic: VMLogic, error: FunctionCallError, - current_protocol_version: u32, ) -> VMOutcome { - if checked_feature!( - "protocol_feature_fix_contract_loading_cost", - FixContractLoadingCost, - current_protocol_version - ) { + if logic.config.fix_contract_loading_cost { Self::abort(logic, error) } else { Self::nop_outcome(error) diff --git a/runtime/near-vm-runner/src/logic/mocks/mock_external.rs b/runtime/near-vm-runner/src/logic/mocks/mock_external.rs index ab317bf02f5..9999ae1d7bc 100644 --- a/runtime/near-vm-runner/src/logic/mocks/mock_external.rs +++ b/runtime/near-vm-runner/src/logic/mocks/mock_external.rs @@ -1,14 +1,74 @@ +use crate::logic::types::ReceiptIndex; use crate::logic::TrieNodesCount; use crate::logic::{External, StorageGetMode, ValuePtr}; use near_primitives_core::hash::{hash, CryptoHash}; -use near_primitives_core::types::{AccountId, Balance}; +use near_primitives_core::types::{AccountId, Balance, Gas, GasWeight}; use std::collections::HashMap; +#[derive(serde::Serialize)] +#[serde(remote = "GasWeight")] +struct GasWeightSer(u64); + +#[derive(Debug, Clone, serde::Serialize)] +pub enum MockAction { + CreateReceipt { + receipt_indices: Vec, + receiver_id: AccountId, + }, + CreateAccount { + receipt_index: ReceiptIndex, + }, + DeployContract { + receipt_index: ReceiptIndex, + code: Vec, + }, + FunctionCallWeight { + receipt_index: ReceiptIndex, + method_name: Vec, + args: Vec, + attached_deposit: u128, + prepaid_gas: Gas, + #[serde(with = "GasWeightSer")] + gas_weight: GasWeight, + }, + Transfer { + receipt_index: ReceiptIndex, + deposit: u128, + }, + Stake { + receipt_index: ReceiptIndex, + stake: u128, + public_key: near_crypto::PublicKey, + }, + DeleteAccount { + receipt_index: ReceiptIndex, + beneficiary_id: AccountId, + }, + DeleteKey { + receipt_index: ReceiptIndex, + public_key: near_crypto::PublicKey, + }, + AddKeyWithFunctionCall { + receipt_index: ReceiptIndex, + public_key: near_crypto::PublicKey, + nonce: u64, + allowance: Option, + receiver_id: AccountId, + method_names: Vec>, + }, + AddKeyWithFullAccess { + receipt_index: ReceiptIndex, + public_key: near_crypto::PublicKey, + nonce: u64, + }, +} + #[derive(Default, Clone)] /// Emulates the trie and the mock handling code. pub struct MockedExternal { pub fake_trie: HashMap, Vec>, pub validators: HashMap, + pub action_log: Vec, data_count: u64, } @@ -89,4 +149,122 @@ impl External for MockedExternal { fn validator_total_stake(&self) -> Result { Ok(self.validators.values().sum()) } + + fn create_receipt( + &mut self, + receipt_indices: Vec, + receiver_id: AccountId, + ) -> Result { + let index = self.action_log.len(); + self.action_log.push(MockAction::CreateReceipt { receipt_indices, receiver_id }); + Ok(index as u64) + } + + fn append_action_create_account( + &mut self, + receipt_index: ReceiptIndex, + ) -> Result<(), crate::logic::VMLogicError> { + self.action_log.push(MockAction::CreateAccount { receipt_index }); + Ok(()) + } + + fn append_action_deploy_contract( + &mut self, + receipt_index: ReceiptIndex, + code: Vec, + ) -> Result<(), crate::logic::VMLogicError> { + self.action_log.push(MockAction::DeployContract { receipt_index, code }); + Ok(()) + } + + fn append_action_function_call_weight( + &mut self, + receipt_index: ReceiptIndex, + method_name: Vec, + args: Vec, + attached_deposit: Balance, + prepaid_gas: Gas, + gas_weight: GasWeight, + ) -> Result<(), crate::logic::VMLogicError> { + self.action_log.push(MockAction::FunctionCallWeight { + receipt_index, + method_name, + args, + attached_deposit, + prepaid_gas, + gas_weight, + }); + Ok(()) + } + + fn append_action_transfer( + &mut self, + receipt_index: ReceiptIndex, + deposit: Balance, + ) -> Result<(), crate::logic::VMLogicError> { + self.action_log.push(MockAction::Transfer { receipt_index, deposit }); + Ok(()) + } + + fn append_action_stake( + &mut self, + receipt_index: ReceiptIndex, + stake: Balance, + public_key: near_crypto::PublicKey, + ) { + self.action_log.push(MockAction::Stake { receipt_index, stake, public_key }); + } + + fn append_action_add_key_with_full_access( + &mut self, + receipt_index: ReceiptIndex, + public_key: near_crypto::PublicKey, + nonce: near_primitives_core::types::Nonce, + ) { + self.action_log.push(MockAction::AddKeyWithFullAccess { receipt_index, public_key, nonce }); + } + + fn append_action_add_key_with_function_call( + &mut self, + receipt_index: ReceiptIndex, + public_key: near_crypto::PublicKey, + nonce: near_primitives_core::types::Nonce, + allowance: Option, + receiver_id: AccountId, + method_names: Vec>, + ) -> Result<(), crate::logic::VMLogicError> { + self.action_log.push(MockAction::AddKeyWithFunctionCall { + receipt_index, + public_key, + nonce, + allowance, + receiver_id, + method_names, + }); + Ok(()) + } + + fn append_action_delete_key( + &mut self, + receipt_index: ReceiptIndex, + public_key: near_crypto::PublicKey, + ) { + self.action_log.push(MockAction::DeleteKey { receipt_index, public_key }); + } + + fn append_action_delete_account( + &mut self, + receipt_index: ReceiptIndex, + beneficiary_id: AccountId, + ) -> Result<(), crate::logic::VMLogicError> { + self.action_log.push(MockAction::DeleteAccount { receipt_index, beneficiary_id }); + Ok(()) + } + + fn get_receipt_receiver(&self, receipt_index: ReceiptIndex) -> &AccountId { + match &self.action_log[receipt_index as usize] { + MockAction::CreateReceipt { receiver_id, .. } => receiver_id, + _ => panic!("not a valid receipt index!"), + } + } } diff --git a/runtime/near-vm-runner/src/logic/mod.rs b/runtime/near-vm-runner/src/logic/mod.rs index b99dc28705e..36c8ad2ae19 100644 --- a/runtime/near-vm-runner/src/logic/mod.rs +++ b/runtime/near-vm-runner/src/logic/mod.rs @@ -3,17 +3,13 @@ use near_primitives_core::hash::CryptoHash; use std::fmt; use types::AccountId; -pub mod action; mod alt_bn128; mod context; -pub mod delegate_action; mod dependencies; pub mod errors; pub mod gas_counter; mod logic; pub mod mocks; -mod receipt_manager; -pub mod signable_message; pub mod test_utils; #[cfg(test)] mod tests; @@ -21,14 +17,13 @@ pub mod types; mod utils; mod vmstate; +pub use crate::config::{Config, ContractPrepareVersion, LimitConfig}; pub use context::VMContext; pub use dependencies::{External, MemSlice, MemoryLike, StorageGetMode, ValuePtr}; pub use errors::{HostError, VMLogicError}; pub use logic::{VMLogic, VMOutcome}; -pub use near_primitives_core::config::*; pub use near_primitives_core::profile; pub use near_primitives_core::types::ProtocolVersion; -pub use receipt_manager::ReceiptMetadata; pub use types::ReturnData; pub use gas_counter::with_ext_cost_counter; diff --git a/runtime/near-vm-runner/src/logic/tests/context.rs b/runtime/near-vm-runner/src/logic/tests/context.rs index fe6ec44ee51..7a839baa587 100644 --- a/runtime/near-vm-runner/src/logic/tests/context.rs +++ b/runtime/near-vm-runner/src/logic/tests/context.rs @@ -1,5 +1,6 @@ use crate::logic::tests::vm_logic_builder::VMLogicBuilder; -use near_primitives_core::config::{VMLimitConfig, ViewConfig}; +use crate::logic::LimitConfig; +use near_primitives_core::config::ViewConfig; macro_rules! decl_test_bytes { ($testname:ident, $method:ident, $ctx:ident, $want:expr) => { @@ -96,8 +97,7 @@ fn test_attached_deposit_view() { fn test_view(amount: u128) { let mut logic_builder = VMLogicBuilder::default(); let context = &mut logic_builder.context; - context.view_config = - Some(ViewConfig { max_gas_burnt: VMLimitConfig::test().max_gas_burnt }); + context.view_config = Some(ViewConfig { max_gas_burnt: LimitConfig::test().max_gas_burnt }); context.account_balance = 0; context.attached_deposit = amount; let mut logic = logic_builder.build(); diff --git a/runtime/near-vm-runner/src/logic/tests/ed25519_verify.rs b/runtime/near-vm-runner/src/logic/tests/ed25519_verify.rs index 0a806317a6f..23f70146c37 100644 --- a/runtime/near-vm-runner/src/logic/tests/ed25519_verify.rs +++ b/runtime/near-vm-runner/src/logic/tests/ed25519_verify.rs @@ -1,8 +1,9 @@ use crate::logic::tests::helpers::*; use crate::logic::tests::vm_logic_builder::VMLogicBuilder; use crate::logic::HostError; -use crate::logic::{ExtCosts, VMLogicError}; +use crate::logic::VMLogicError; use crate::map; +use near_primitives_core::config::ExtCosts; use std::collections::HashMap; const SIGNATURE: [u8; 64] = [ diff --git a/runtime/near-vm-runner/src/logic/tests/gas_counter.rs b/runtime/near-vm-runner/src/logic/tests/gas_counter.rs index 9f3b2de7c2c..22838924b5c 100644 --- a/runtime/near-vm-runner/src/logic/tests/gas_counter.rs +++ b/runtime/near-vm-runner/src/logic/tests/gas_counter.rs @@ -1,10 +1,8 @@ -use crate::logic::action::{Action, FunctionCallAction}; -use crate::logic::receipt_manager::ReceiptMetadata; use crate::logic::tests::helpers::*; use crate::logic::tests::vm_logic_builder::{TestVMLogic, VMLogicBuilder}; use crate::logic::types::Gas; +use crate::logic::{Config, MemSlice}; use crate::logic::{HostError, VMLogicError}; -use crate::logic::{MemSlice, VMConfig}; use borsh::BorshSerialize; use expect_test::expect; use near_primitives_core::config::{ActionCosts, ExtCosts}; @@ -22,7 +20,7 @@ fn test_dont_burn_gas_when_exceeding_attached_gas_limit() { let index = promise_create(&mut logic, b"rick.test", 0, 0).expect("should create a promise"); promise_batch_action_function_call(&mut logic, index, 0, gas_limit * 2) .expect_err("should fail with gas limit"); - let outcome = logic.compute_outcome_and_distribute_gas(); + let outcome = logic.compute_outcome(); // Just avoid hard-coding super-precise amount of gas burnt. assert!(outcome.burnt_gas < gas_limit / 2); @@ -43,7 +41,7 @@ fn test_limit_wasm_gas_after_attaching_gas() { promise_batch_action_function_call(&mut logic, index, 0, gas_limit / 2) .expect("should add action to receipt"); logic.gas_opcodes((op_limit / 2) as u32).expect_err("should fail with gas limit"); - let outcome = logic.compute_outcome_and_distribute_gas(); + let outcome = logic.compute_outcome(); assert_eq!(outcome.used_gas, gas_limit); assert!(gas_limit / 2 < outcome.burnt_gas); @@ -61,7 +59,7 @@ fn test_cant_burn_more_than_max_gas_burnt_gas() { let mut logic = logic_builder.build(); logic.gas_opcodes(op_limit * 3).expect_err("should fail with gas limit"); - let outcome = logic.compute_outcome_and_distribute_gas(); + let outcome = logic.compute_outcome(); assert_eq!(outcome.burnt_gas, gas_limit); assert_eq!(outcome.used_gas, gas_limit * 2); @@ -78,7 +76,7 @@ fn test_cant_burn_more_than_prepaid_gas() { let mut logic = logic_builder.build(); logic.gas_opcodes(op_limit * 3).expect_err("should fail with gas limit"); - let outcome = logic.compute_outcome_and_distribute_gas(); + let outcome = logic.compute_outcome(); assert_eq!(outcome.burnt_gas, gas_limit); assert_eq!(outcome.used_gas, gas_limit); @@ -96,7 +94,7 @@ fn test_hit_max_gas_burnt_limit() { promise_create(&mut logic, b"rick.test", 0, gas_limit / 2).expect("should create a promise"); logic.gas_opcodes(op_limit * 2).expect_err("should fail with gas limit"); - let outcome = logic.compute_outcome_and_distribute_gas(); + let outcome = logic.compute_outcome(); assert_eq!(outcome.burnt_gas, gas_limit); assert!(outcome.used_gas > gas_limit * 2); @@ -114,112 +112,12 @@ fn test_hit_prepaid_gas_limit() { promise_create(&mut logic, b"rick.test", 0, gas_limit / 2).expect("should create a promise"); logic.gas_opcodes(op_limit * 2).expect_err("should fail with gas limit"); - let outcome = logic.compute_outcome_and_distribute_gas(); + let outcome = logic.compute_outcome(); assert_eq!(outcome.burnt_gas, gas_limit); assert_eq!(outcome.used_gas, gas_limit); } -#[track_caller] -fn assert_with_gas(receipt: &ReceiptMetadata, expcted_gas: Gas) { - match receipt.actions[0] { - Action::FunctionCall(FunctionCallAction { gas, .. }) => { - assert_eq!(expcted_gas, gas); - } - _ => { - panic!("expected function call action"); - } - } -} - -#[track_caller] -fn function_call_weight_check(function_calls: &[(Gas, u64, Gas)]) { - let gas_limit = 10_000_000_000; - - let mut logic_builder = VMLogicBuilder::free(); - logic_builder.config.limit_config.max_gas_burnt = gas_limit; - logic_builder.context.prepaid_gas = gas_limit; - let mut logic = logic_builder.build(); - - let mut ratios = vec![]; - - // Schedule all function calls - for &(static_gas, gas_weight, _) in function_calls { - let index = promise_batch_create(&mut logic, "rick.test").expect("should create a promise"); - promise_batch_action_function_call_weight(&mut logic, index, 0, static_gas, gas_weight) - .expect("batch action function call should succeed"); - ratios.push((index, gas_weight)); - } - - // Test static gas assigned before - let receipts = logic.receipt_manager().action_receipts.iter().map(|(_, rec)| rec); - for (receipt, &(static_gas, _, _)) in receipts.zip(function_calls) { - assert_with_gas(receipt, static_gas); - } - - let outcome = logic.compute_outcome_and_distribute_gas(); - - // Test gas is distributed after outcome calculated. - let receipts = outcome.action_receipts.iter().map(|(_, rec)| rec); - - // Assert lengths are equal for zip - assert_eq!(receipts.len(), function_calls.len()); - - // Assert sufficient amount was given to - for (receipt, &(_, _, expected)) in receipts.zip(function_calls) { - assert_with_gas(receipt, expected); - } - - // Verify that all gas was consumed (assumes at least one ratio is provided) - assert_eq!(outcome.used_gas, gas_limit); -} - -#[test] -fn function_call_weight_basic_cases_test() { - // Following tests input are in the format (static gas, gas weight, expected gas) - // and the gas limit is `10_000_000_000` - - // Single function call - function_call_weight_check(&[(0, 1, 10_000_000_000)]); - - // Single function with static gas - function_call_weight_check(&[(888, 1, 10_000_000_000)]); - - // Large weight - function_call_weight_check(&[(0, 88888, 10_000_000_000)]); - - // Weight larger than gas limit - function_call_weight_check(&[(0, 11u64.pow(14), 10_000_000_000)]); - - // Split two - function_call_weight_check(&[(0, 3, 6_000_000_000), (0, 2, 4_000_000_000)]); - - // Split two with static gas - function_call_weight_check(&[(1_000_000, 3, 5_998_600_000), (3_000_000, 2, 4_001_400_000)]); - - // Many different gas weights - function_call_weight_check(&[ - (1_000_000, 3, 2_699_800_000), - (3_000_000, 2, 1_802_200_000), - (0, 1, 899_600_000), - (1_000_000_000, 0, 1_000_000_000), - (0, 4, 3_598_400_000), - ]); - - // Weight over u64 bounds - function_call_weight_check(&[(0, u64::MAX, 9_999_999_999), (0, 1000, 1)]); - - // Weight over gas limit with three function calls - function_call_weight_check(&[ - (0, 10_000_000_000, 4_999_999_999), - (0, 1, 0), - (0, 10_000_000_000, 5_000_000_001), - ]); - - // Weights with one zero and one non-zero - function_call_weight_check(&[(0, 0, 0), (0, 1, 10_000_000_000)]) -} - #[test] fn function_call_no_weight_refund() { let gas_limit = 10u64.pow(14); @@ -233,7 +131,7 @@ fn function_call_no_weight_refund() { promise_batch_action_function_call_weight(&mut logic, index, 0, 1000, 0) .expect("batch action function call should succeed"); - let outcome = logic.compute_outcome_and_distribute_gas(); + let outcome = logic.compute_outcome(); // Verify that unused gas was not allocated to function call assert!(outcome.used_gas < gas_limit); @@ -820,7 +718,7 @@ fn create_promise_dependency(logic: &mut TestVMLogic) -> Result<(), VMLogicError /// Given the limit in gas, compute the corresponding limit in wasm ops for use /// with [`VMLogic::gas`] function. fn op_limit(gas_limit: Gas) -> u32 { - (gas_limit / (VMConfig::test().regular_op_cost as u64)) as u32 + (gas_limit / (Config::test().regular_op_cost as u64)) as u32 } fn test_pk() -> Vec { diff --git a/runtime/near-vm-runner/src/logic/tests/helpers.rs b/runtime/near-vm-runner/src/logic/tests/helpers.rs index 7e46ab37e63..e73f726e7dc 100644 --- a/runtime/near-vm-runner/src/logic/tests/helpers.rs +++ b/runtime/near-vm-runner/src/logic/tests/helpers.rs @@ -29,13 +29,11 @@ pub(super) fn promise_create( ) } -#[allow(dead_code)] pub(super) fn promise_batch_create(logic: &mut TestVMLogic, account_id: &str) -> Result { let account_id = logic.internal_mem_write(account_id.as_bytes()); logic.promise_batch_create(account_id.len, account_id.ptr) } -#[allow(dead_code)] pub(super) fn promise_batch_action_function_call( logic: &mut TestVMLogic<'_>, promise_index: u64, @@ -52,7 +50,6 @@ pub(super) fn promise_batch_action_function_call( ) } -#[allow(dead_code)] pub(super) fn promise_batch_action_function_call_ext( logic: &mut TestVMLogic<'_>, promise_index: u64, @@ -76,7 +73,6 @@ pub(super) fn promise_batch_action_function_call_ext( ) } -#[allow(dead_code)] pub(super) fn promise_batch_action_function_call_weight( logic: &mut TestVMLogic<'_>, promise_index: u64, @@ -95,7 +91,6 @@ pub(super) fn promise_batch_action_function_call_weight( ) } -#[allow(dead_code)] pub(super) fn promise_batch_action_function_call_weight_ext( logic: &mut TestVMLogic<'_>, promise_index: u64, @@ -121,7 +116,6 @@ pub(super) fn promise_batch_action_function_call_weight_ext( ) } -#[allow(dead_code)] pub(super) fn promise_batch_action_add_key_with_function_call( logic: &mut TestVMLogic<'_>, promise_index: u64, @@ -149,7 +143,6 @@ pub(super) fn promise_batch_action_add_key_with_function_call( ) } -#[allow(dead_code)] pub(super) fn promise_batch_action_add_key_with_full_access( logic: &mut TestVMLogic<'_>, promise_index: u64, diff --git a/runtime/near-vm-runner/src/logic/tests/logs.rs b/runtime/near-vm-runner/src/logic/tests/logs.rs index c6aa654b98b..ec939027ab6 100644 --- a/runtime/near-vm-runner/src/logic/tests/logs.rs +++ b/runtime/near-vm-runner/src/logic/tests/logs.rs @@ -1,8 +1,9 @@ use crate::logic::tests::helpers::*; use crate::logic::tests::vm_logic_builder::VMLogicBuilder; use crate::logic::HostError; -use crate::logic::{ExtCosts, MemSlice, VMLogic, VMLogicError}; +use crate::logic::{MemSlice, VMLogic, VMLogicError}; use crate::map; +use near_primitives_core::config::ExtCosts; #[test] fn test_valid_utf8() { @@ -11,7 +12,7 @@ fn test_valid_utf8() { let string = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%"; let bytes = logic.internal_mem_write(string.as_bytes()); logic.log_utf8(bytes.len, bytes.ptr).expect("Valid UTF-8 in bytes"); - let outcome = logic.compute_outcome_and_distribute_gas(); + let outcome = logic.compute_outcome(); assert_eq!(outcome.logs[0], string); assert_costs(map! { ExtCosts::base: 1, @@ -30,7 +31,7 @@ fn test_invalid_utf8() { let mut logic = logic_builder.build(); let bytes = logic.internal_mem_write(b"\x80"); assert_eq!(logic.log_utf8(bytes.len, bytes.ptr), Err(HostError::BadUTF8.into())); - let outcome = logic.compute_outcome_and_distribute_gas(); + let outcome = logic.compute_outcome(); assert_eq!(outcome.logs.len(), 0); assert_costs(map! { ExtCosts::base: 1, @@ -51,7 +52,7 @@ fn test_valid_null_terminated_utf8() { let mut logic = logic_builder.build(); let bytes = logic.internal_mem_write(cstring.as_bytes()); logic.log_utf8(u64::MAX, bytes.ptr).expect("Valid null-terminated utf-8 string_bytes"); - let outcome = logic.compute_outcome_and_distribute_gas(); + let outcome = logic.compute_outcome(); assert_costs(map! { ExtCosts::base: 1, ExtCosts::log_base: 1, @@ -83,7 +84,7 @@ fn test_log_max_limit() { ExtCosts::utf8_decoding_base: 1, }); - let outcome = logic.compute_outcome_and_distribute_gas(); + let outcome = logic.compute_outcome(); assert_eq!(outcome.logs.len(), 0); } @@ -106,7 +107,7 @@ fn test_log_total_length_limit() { Err(HostError::TotalLogLengthExceeded { length: limit + 1, limit }.into()) ); - let outcome = logic.compute_outcome_and_distribute_gas(); + let outcome = logic.compute_outcome(); assert_eq!(outcome.logs.len() as u64, num_logs - 1); } @@ -140,7 +141,7 @@ fn test_log_number_limit() { ExtCosts::utf8_decoding_byte: bytes.len * max_number_logs, }); - let outcome = logic.compute_outcome_and_distribute_gas(); + let outcome = logic.compute_outcome(); assert_eq!(outcome.logs.len() as u64, max_number_logs); } @@ -184,7 +185,7 @@ fn test_log_utf16_number_limit() { ExtCosts::utf16_decoding_byte: bytes.len * max_number_logs, }); - let outcome = logic.compute_outcome_and_distribute_gas(); + let outcome = logic.compute_outcome(); assert_eq!(outcome.logs.len() as u64, max_number_logs); } @@ -215,7 +216,7 @@ fn test_log_total_length_limit_mixed() { Err(HostError::TotalLogLengthExceeded { length: limit + 1, limit }.into()) ); - let outcome = logic.compute_outcome_and_distribute_gas(); + let outcome = logic.compute_outcome(); assert_eq!(outcome.logs.len() as u64, num_logs_each * 2); } @@ -240,7 +241,7 @@ fn test_log_utf8_max_limit_null_terminated() { ExtCosts::utf8_decoding_base: 1, }); - let outcome = logic.compute_outcome_and_distribute_gas(); + let outcome = logic.compute_outcome(); assert_eq!(outcome.logs.len(), 0); } @@ -265,7 +266,7 @@ fn test_valid_log_utf16() { ExtCosts::log_base: 1, ExtCosts::log_byte: string.len() as u64, }); - let outcome = logic.compute_outcome_and_distribute_gas(); + let outcome = logic.compute_outcome(); assert_eq!(outcome.logs[0], string); } @@ -339,7 +340,7 @@ fn test_valid_log_utf16_null_terminated() { logic.log_utf16(u64::MAX, bytes.ptr).expect("Valid utf-16 string_bytes"); - let outcome = logic.compute_outcome_and_distribute_gas(); + let outcome = logic.compute_outcome(); assert_eq!(outcome.logs[0], string); assert_costs(map! { ExtCosts::base: 1, diff --git a/runtime/near-vm-runner/src/logic/tests/miscs.rs b/runtime/near-vm-runner/src/logic/tests/miscs.rs index a6f9c172b0d..6deafa4bbef 100644 --- a/runtime/near-vm-runner/src/logic/tests/miscs.rs +++ b/runtime/near-vm-runner/src/logic/tests/miscs.rs @@ -1,9 +1,9 @@ use crate::logic::tests::helpers::*; use crate::logic::tests::vm_logic_builder::VMLogicBuilder; -use crate::logic::ExtCosts; use crate::logic::HostError; use crate::map; use hex::FromHex; +use near_primitives_core::config::ExtCosts; use serde::de::Error; use serde_json::from_slice; use std::{fmt::Display, fs}; diff --git a/runtime/near-vm-runner/src/logic/tests/promises.rs b/runtime/near-vm-runner/src/logic/tests/promises.rs index d85b7c5e762..9c429fb1dc6 100644 --- a/runtime/near-vm-runner/src/logic/tests/promises.rs +++ b/runtime/near-vm-runner/src/logic/tests/promises.rs @@ -1,25 +1,13 @@ -use crate::logic::action::Action; +use crate::logic::mocks::mock_external::MockedExternal; use crate::logic::tests::helpers::*; use crate::logic::tests::vm_logic_builder::VMLogicBuilder; use crate::logic::types::PromiseResult; -use crate::logic::VMLogic; use borsh::BorshSerialize; use near_crypto::PublicKey; use serde_json; -fn vm_receipts<'a>(logic: &'a VMLogic) -> Vec { - #[derive(serde::Serialize)] - struct ReceiptView<'a, T> { - receiver_id: T, - actions: &'a [Action], - } - - logic - .receipt_manager() - .action_receipts - .iter() - .map(|(receiver_id, metadata)| ReceiptView { receiver_id, actions: &metadata.actions }) - .collect() +fn vm_receipts<'a>(ext: &'a MockedExternal) -> Vec { + ext.action_log.clone() } #[test] @@ -58,20 +46,103 @@ fn test_promise_batch_action_function_call() { promise_batch_action_function_call(&mut logic, index, 0, 0) .expect("should add an action to receipt"); - let expected = serde_json::json!([ - { - "receiver_id":"rick.test", - "actions":[ - { - "FunctionCall":{ - "method_name":"promise_create","args":"YXJncw==","gas":0,"deposit":"0"}}, - { - "FunctionCall":{ - "method_name":"promise_batch_action","args":"cHJvbWlzZV9iYXRjaF9hY3Rpb25fYXJncw==","gas":0,"deposit":"0"} + expect_test::expect![[r#" + [ + { + "CreateReceipt": { + "receipt_indices": [], + "receiver_id": "rick.test" + } + }, + { + "FunctionCallWeight": { + "receipt_index": 0, + "method_name": [ + 112, + 114, + 111, + 109, + 105, + 115, + 101, + 95, + 99, + 114, + 101, + 97, + 116, + 101 + ], + "args": [ + 97, + 114, + 103, + 115 + ], + "attached_deposit": 0, + "prepaid_gas": 0, + "gas_weight": 0 + } + }, + { + "FunctionCallWeight": { + "receipt_index": 0, + "method_name": [ + 112, + 114, + 111, + 109, + 105, + 115, + 101, + 95, + 98, + 97, + 116, + 99, + 104, + 95, + 97, + 99, + 116, + 105, + 111, + 110 + ], + "args": [ + 112, + 114, + 111, + 109, + 105, + 115, + 101, + 95, + 98, + 97, + 116, + 99, + 104, + 95, + 97, + 99, + 116, + 105, + 111, + 110, + 95, + 97, + 114, + 103, + 115 + ], + "attached_deposit": 0, + "prepaid_gas": 0, + "gas_weight": 0 } - ] - }]); - assert_eq!(&serde_json::to_string(&vm_receipts(&logic)).unwrap(), &expected.to_string()); + } + ]"#]] + .assert_eq(&serde_json::to_string_pretty(&vm_receipts(&logic_builder.ext)).unwrap()); } #[test] @@ -93,23 +164,51 @@ fn test_promise_batch_action_create_account() { .promise_batch_action_create_account(index) .expect("should add an action to create account"); assert_eq!(logic.used_gas().unwrap(), 12578263688564); - let expected = serde_json::json!([ - { - "receiver_id": "rick.test", - "actions": [ - { - "FunctionCall": { - "method_name": "promise_create", - "args": "YXJncw==", - "gas": 0, - "deposit": "0" - } - }, - {"CreateAccount": {}} - ] - } - ]); - assert_eq!(&serde_json::to_string(&vm_receipts(&logic)).unwrap(), &expected.to_string()); + expect_test::expect![[r#" + [ + { + "CreateReceipt": { + "receipt_indices": [], + "receiver_id": "rick.test" + } + }, + { + "FunctionCallWeight": { + "receipt_index": 0, + "method_name": [ + 112, + 114, + 111, + 109, + 105, + 115, + 101, + 95, + 99, + 114, + 101, + 97, + 116, + 101 + ], + "args": [ + 97, + 114, + 103, + 115 + ], + "attached_deposit": 0, + "prepaid_gas": 0, + "gas_weight": 0 + } + }, + { + "CreateAccount": { + "receipt_index": 0 + } + } + ]"#]] + .assert_eq(&serde_json::to_string_pretty(&vm_receipts(&logic_builder.ext)).unwrap()); } #[test] @@ -134,29 +233,59 @@ fn test_promise_batch_action_deploy_contract() { .promise_batch_action_deploy_contract(index, code.len, code.ptr) .expect("should add an action to deploy contract"); assert_eq!(logic.used_gas().unwrap(), 5255774958146); - let expected = serde_json::json!( - [ - { - - "receiver_id": "rick.test", - "actions": [ + expect_test::expect![[r#" + [ { - "FunctionCall": { - "method_name": "promise_create", - "args": "YXJncw==", - "gas": 0, - "deposit": "0" + "CreateReceipt": { + "receipt_indices": [], + "receiver_id": "rick.test" + } + }, + { + "FunctionCallWeight": { + "receipt_index": 0, + "method_name": [ + 112, + 114, + 111, + 109, + 105, + 115, + 101, + 95, + 99, + 114, + 101, + 97, + 116, + 101 + ], + "args": [ + 97, + 114, + 103, + 115 + ], + "attached_deposit": 0, + "prepaid_gas": 0, + "gas_weight": 0 } }, { "DeployContract": { - "code": "c2FtcGxl" + "receipt_index": 0, + "code": [ + 115, + 97, + 109, + 112, + 108, + 101 + ] } } - ] - } - ]); - assert_eq!(&serde_json::to_string(&vm_receipts(&logic)).unwrap(), &expected.to_string()); + ]"#]] + .assert_eq(&serde_json::to_string_pretty(&vm_receipts(&logic_builder.ext)).unwrap()); } #[test] @@ -183,29 +312,52 @@ fn test_promise_batch_action_transfer() { .expect("should add an action to transfer money"); logic.promise_batch_action_transfer(index, num_1u128.ptr).expect_err("not enough money"); assert_eq!(logic.used_gas().unwrap(), 5349703444787); - let expected = serde_json::json!( - [ - { - - "receiver_id": "rick.test", - "actions": [ - { - "FunctionCall": { - "method_name": "promise_create", - "args": "YXJncw==", - "gas": 0, - "deposit": "0" - } - }, - { - "Transfer": { - "deposit": "110" - } + expect_test::expect![[r#" + [ + { + "CreateReceipt": { + "receipt_indices": [], + "receiver_id": "rick.test" + } + }, + { + "FunctionCallWeight": { + "receipt_index": 0, + "method_name": [ + 112, + 114, + 111, + 109, + 105, + 115, + 101, + 95, + 99, + 114, + 101, + 97, + 116, + 101 + ], + "args": [ + 97, + 114, + 103, + 115 + ], + "attached_deposit": 0, + "prepaid_gas": 0, + "gas_weight": 0 } - ] - } - ]); - assert_eq!(&serde_json::to_string(&vm_receipts(&logic)).unwrap(), &expected.to_string()); + }, + { + "Transfer": { + "receipt_index": 0, + "deposit": 110 + } + } + ]"#]] + .assert_eq(&serde_json::to_string_pretty(&vm_receipts(&logic_builder.ext)).unwrap()); } #[test] @@ -236,29 +388,53 @@ fn test_promise_batch_action_stake() { .promise_batch_action_stake(index, num_110u128.ptr, key.len, key.ptr) .expect("should add an action to stake"); assert_eq!(logic.used_gas().unwrap(), 5138414976215); - let expected = serde_json::json!([ - { - - "receiver_id": "rick.test", - "actions": [ - { - "FunctionCall": { - "method_name": "promise_create", - "args": "YXJncw==", - "gas": 0, - "deposit": "0" - } - }, - { - "Stake": { - "stake": "110", - "public_key": "ed25519:5do5nkAEVhL8iteDvXNgxi4pWK78Y7DDadX11ArFNyrf" - } - } - ] - } - ]); - assert_eq!(&serde_json::to_string(&vm_receipts(&logic)).unwrap(), &expected.to_string()); + expect_test::expect![[r#" + [ + { + "CreateReceipt": { + "receipt_indices": [], + "receiver_id": "rick.test" + } + }, + { + "FunctionCallWeight": { + "receipt_index": 0, + "method_name": [ + 112, + 114, + 111, + 109, + 105, + 115, + 101, + 95, + 99, + 114, + 101, + 97, + 116, + 101 + ], + "args": [ + 97, + 114, + 103, + 115 + ], + "attached_deposit": 0, + "prepaid_gas": 0, + "gas_weight": 0 + } + }, + { + "Stake": { + "receipt_index": 0, + "stake": 110, + "public_key": "ed25519:5do5nkAEVhL8iteDvXNgxi4pWK78Y7DDadX11ArFNyrf" + } + } + ]"#]] + .assert_eq(&serde_json::to_string_pretty(&vm_receipts(&logic_builder.ext)).unwrap()); } #[test] @@ -311,41 +487,67 @@ fn test_promise_batch_action_add_key_with_function_call() { ) .expect("should add allowance"); assert_eq!(logic.used_gas().unwrap(), 5126680499695); - let expected = serde_json::json!( - [ - { - "receiver_id": "rick.test", - "actions": [ - { - "FunctionCall": { - "method_name": "promise_create", - "args": "YXJncw==", - "gas": 0, - "deposit": "0" - } - }, - { - "AddKey": { - "public_key": "ed25519:5do5nkAEVhL8iteDvXNgxi4pWK78Y7DDadX11ArFNyrf", - "access_key": { - "nonce": 1, - "permission": { - "FunctionCall": { - "allowance": "999", - "receiver_id": "sam", - "method_names": [ - "foo", - "bar" - ] - } - } - } - } - } - ] - } - ]); - assert_eq!(&serde_json::to_string(&vm_receipts(&logic)).unwrap(), &expected.to_string()); + expect_test::expect![[r#" + [ + { + "CreateReceipt": { + "receipt_indices": [], + "receiver_id": "rick.test" + } + }, + { + "FunctionCallWeight": { + "receipt_index": 0, + "method_name": [ + 112, + 114, + 111, + 109, + 105, + 115, + 101, + 95, + 99, + 114, + 101, + 97, + 116, + 101 + ], + "args": [ + 97, + 114, + 103, + 115 + ], + "attached_deposit": 0, + "prepaid_gas": 0, + "gas_weight": 0 + } + }, + { + "AddKeyWithFunctionCall": { + "receipt_index": 0, + "public_key": "ed25519:5do5nkAEVhL8iteDvXNgxi4pWK78Y7DDadX11ArFNyrf", + "nonce": 1, + "allowance": 999, + "receiver_id": "sam", + "method_names": [ + [ + 102, + 111, + 111 + ], + [ + 98, + 97, + 114 + ] + ] + } + } + ]"#]] + .assert_eq(&serde_json::to_string_pretty(&vm_receipts(&logic_builder.ext)).unwrap()); } #[test] @@ -372,28 +574,60 @@ fn test_promise_batch_then() { .promise_batch_then(index, account_id.len, account_id.ptr) .expect("promise batch should run ok"); assert_eq!(logic.used_gas().unwrap(), 24124999601771); - let expected = serde_json::json!([ - { - "receiver_id": "rick.test", - "actions": [ - { - "FunctionCall": { - "method_name": "promise_create", - "args": "YXJncw==", - "gas": 0, - "deposit": "0" - } - } - ] - }, - { - "receiver_id": "rick.test", - "actions": [] - }, - { - "receiver_id": "rick.test", - "actions": [] - } - ]); - assert_eq!(&serde_json::to_string(&vm_receipts(&logic)).unwrap(), &expected.to_string()); + expect_test::expect![[r#" + [ + { + "CreateReceipt": { + "receipt_indices": [], + "receiver_id": "rick.test" + } + }, + { + "FunctionCallWeight": { + "receipt_index": 0, + "method_name": [ + 112, + 114, + 111, + 109, + 105, + 115, + 101, + 95, + 99, + 114, + 101, + 97, + 116, + 101 + ], + "args": [ + 97, + 114, + 103, + 115 + ], + "attached_deposit": 0, + "prepaid_gas": 0, + "gas_weight": 0 + } + }, + { + "CreateReceipt": { + "receipt_indices": [ + 0 + ], + "receiver_id": "rick.test" + } + }, + { + "CreateReceipt": { + "receipt_indices": [ + 0 + ], + "receiver_id": "rick.test" + } + } + ]"#]] + .assert_eq(&serde_json::to_string_pretty(&vm_receipts(&logic_builder.ext)).unwrap()); } diff --git a/runtime/near-vm-runner/src/logic/tests/registers.rs b/runtime/near-vm-runner/src/logic/tests/registers.rs index 2255b62b0df..9c5c6aafc42 100644 --- a/runtime/near-vm-runner/src/logic/tests/registers.rs +++ b/runtime/near-vm-runner/src/logic/tests/registers.rs @@ -1,5 +1,5 @@ use crate::logic::tests::vm_logic_builder::VMLogicBuilder; -use crate::logic::VMConfig; +use crate::logic::Config; use crate::logic::{HostError, VMLogicError}; #[test] @@ -32,9 +32,9 @@ fn test_many_registers() { let mut logic = logic_builder.build(); for i in 0..max_registers { - let value = (i * 10).to_le_bytes(); - logic.wrapped_internal_write_register(i, &value).unwrap(); - logic.assert_read_register(&(i * 10).to_le_bytes(), i); + let value = (i * 10u64).to_le_bytes(); + logic.wrapped_internal_write_register(i, &value[..]).unwrap(); + logic.assert_read_register(&value[..], i); } // One more register hits the boundary check. @@ -61,7 +61,7 @@ fn test_max_register_size() { #[test] fn test_max_register_memory_limit() { let mut logic_builder = VMLogicBuilder::free(); - let config = VMConfig::free(); + let config = Config::free(); logic_builder.config = config.clone(); let mut logic = logic_builder.build(); diff --git a/runtime/near-vm-runner/src/logic/tests/view_method.rs b/runtime/near-vm-runner/src/logic/tests/view_method.rs index 18b4b93cd0f..2a5011228ed 100644 --- a/runtime/near-vm-runner/src/logic/tests/view_method.rs +++ b/runtime/near-vm-runner/src/logic/tests/view_method.rs @@ -3,7 +3,6 @@ use crate::logic::tests::vm_logic_builder::VMLogicBuilder; macro_rules! test_prohibited { ($f: ident $(, $arg: expr )* ) => { let mut logic_builder = VMLogicBuilder::view(); - #[allow(unused_mut)] let mut logic = logic_builder.build(); let name = stringify!($f); diff --git a/runtime/near-vm-runner/src/logic/tests/vm_logic_builder.rs b/runtime/near-vm-runner/src/logic/tests/vm_logic_builder.rs index 3a4cdadd6ca..41c03a251f1 100644 --- a/runtime/near-vm-runner/src/logic/tests/vm_logic_builder.rs +++ b/runtime/near-vm-runner/src/logic/tests/vm_logic_builder.rs @@ -1,31 +1,26 @@ use crate::logic::mocks::mock_external::MockedExternal; use crate::logic::mocks::mock_memory::MockedMemory; use crate::logic::types::PromiseResult; -use crate::logic::{MemSlice, VMConfig, VMContext, VMLogic}; +use crate::logic::{Config, MemSlice, VMContext, VMLogic}; use near_primitives_core::runtime::fees::RuntimeFeesConfig; -use near_primitives_core::types::ProtocolVersion; - -pub(super) const LATEST_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::MAX; pub(super) struct VMLogicBuilder { pub ext: MockedExternal, - pub config: VMConfig, + pub config: Config, pub fees_config: RuntimeFeesConfig, pub promise_results: Vec, pub memory: MockedMemory, - pub current_protocol_version: ProtocolVersion, pub context: VMContext, } impl Default for VMLogicBuilder { fn default() -> Self { VMLogicBuilder { - config: VMConfig::test(), + config: Config::test(), fees_config: RuntimeFeesConfig::test(), ext: MockedExternal::default(), memory: MockedMemory::default(), promise_results: vec![], - current_protocol_version: LATEST_PROTOCOL_VERSION, context: get_context(), } } @@ -35,31 +30,30 @@ impl VMLogicBuilder { pub fn view() -> Self { let mut builder = Self::default(); let max_gas_burnt = builder.config.limit_config.max_gas_burnt; - builder.context.view_config = Some(crate::logic::ViewConfig { max_gas_burnt }); + builder.context.view_config = + Some(near_primitives_core::config::ViewConfig { max_gas_burnt }); builder } pub fn build(&mut self) -> TestVMLogic<'_> { let context = self.context.clone(); - TestVMLogic::from(VMLogic::new_with_protocol_version( + TestVMLogic::from(VMLogic::new( &mut self.ext, context, &self.config, &self.fees_config, &self.promise_results, &mut self.memory, - self.current_protocol_version, )) } pub fn free() -> Self { VMLogicBuilder { - config: VMConfig::free(), + config: Config::free(), fees_config: RuntimeFeesConfig::free(), ext: MockedExternal::default(), memory: MockedMemory::default(), promise_results: vec![], - current_protocol_version: LATEST_PROTOCOL_VERSION, context: get_context(), } } @@ -154,7 +148,7 @@ impl TestVMLogic<'_> { assert_eq!(want, &got[..]); } - pub fn compute_outcome_and_distribute_gas(self) -> crate::logic::VMOutcome { - self.logic.compute_outcome_and_distribute_gas() + pub fn compute_outcome(self) -> crate::logic::VMOutcome { + self.logic.compute_outcome() } } diff --git a/runtime/near-vm-runner/src/logic/vmstate.rs b/runtime/near-vm-runner/src/logic/vmstate.rs index a09ee103dee..6b8ad024c9a 100644 --- a/runtime/near-vm-runner/src/logic/vmstate.rs +++ b/runtime/near-vm-runner/src/logic/vmstate.rs @@ -1,11 +1,9 @@ use super::dependencies::{MemSlice, MemoryLike}; -use super::gas_counter::GasCounter; - use super::errors::{HostError, VMLogicError}; -use near_primitives_core::config::ExtCosts::*; -use near_primitives_core::config::VMLimitConfig; - +use super::gas_counter::GasCounter; +use crate::config::LimitConfig; use core::mem::size_of; +use near_primitives_core::config::ExtCosts::*; use std::borrow::Cow; use std::collections::hash_map::Entry; @@ -163,7 +161,7 @@ impl Registers { pub(super) fn set( &mut self, gas_counter: &mut GasCounter, - config: &VMLimitConfig, + config: &LimitConfig, register_id: u64, data: T, ) -> Result<()> @@ -194,7 +192,7 @@ impl Registers { /// into the registers. fn check_set_register<'a>( &'a mut self, - config: &VMLimitConfig, + config: &LimitConfig, register_id: u64, data_len: u64, ) -> Result>> { @@ -255,16 +253,15 @@ pub(super) fn get_memory_or_register<'a, 'b>( #[cfg(test)] mod tests { + use super::HostError; use super::Registers; - use crate::logic::gas_counter::GasCounter; - - use super::HostError; - use near_primitives_core::config::{ExtCostsConfig, VMLimitConfig}; + use crate::logic::LimitConfig; + use near_primitives_core::config::ExtCostsConfig; struct RegistersTestContext { gas: GasCounter, - cfg: VMLimitConfig, + cfg: LimitConfig, regs: Registers, } @@ -273,7 +270,7 @@ mod tests { let costs = ExtCostsConfig::test(); Self { gas: GasCounter::new(costs, u64::MAX, 0, u64::MAX, false), - cfg: VMLimitConfig::test(), + cfg: LimitConfig::test(), regs: Default::default(), } } diff --git a/runtime/near-vm-runner/src/near_vm_runner.rs b/runtime/near-vm-runner/src/near_vm_runner.rs index 7db593f2d1b..d0aae70a889 100644 --- a/runtime/near-vm-runner/src/near_vm_runner.rs +++ b/runtime/near-vm-runner/src/near_vm_runner.rs @@ -7,14 +7,13 @@ use crate::logic::errors::{ use crate::logic::gas_counter::FastGasCounter; use crate::logic::types::{PromiseResult, ProtocolVersion}; use crate::logic::{ - CompiledContract, CompiledContractCache, External, MemSlice, MemoryLike, VMConfig, VMContext, + CompiledContract, CompiledContractCache, Config, External, MemSlice, MemoryLike, VMContext, VMLogic, VMOutcome, }; use crate::prepare; use crate::runner::VMResult; -use crate::{get_contract_cache_key, imports}; +use crate::{get_contract_cache_key, imports, ContractCode}; use memoffset::offset_of; -use near_primitives_core::contract::ContractCode; use near_primitives_core::runtime::fees::RuntimeFeesConfig; use near_vm_compiler_singlepass::Singlepass; use near_vm_engine::universal::{ @@ -230,15 +229,16 @@ pub(crate) fn near_vm_vm_hash() -> u64 { pub(crate) type VMArtifact = Arc; pub(crate) struct NearVM { - pub(crate) config: VMConfig, + pub(crate) config: Config, pub(crate) engine: UniversalEngine, } impl NearVM { - pub(crate) fn new_for_target(config: VMConfig, target: near_vm_compiler::Target) -> Self { + pub(crate) fn new_for_target(config: Config, target: near_vm_compiler::Target) -> Self { // We only support singlepass compiler at the moment. assert_eq!(VM_CONFIG.compiler, NearVmCompiler::Singlepass); - let compiler = Singlepass::new(); + let mut compiler = Singlepass::new(); + compiler.set_9393_fix(!config.disable_9393_fix); // We only support universal engine at the moment. assert_eq!(VM_CONFIG.engine, NearVmEngine::Universal); @@ -276,7 +276,7 @@ impl NearVM { } } - pub(crate) fn new(config: VMConfig) -> Self { + pub(crate) fn new(config: Config) -> Self { use near_vm_compiler::{CpuFeature, Target, Triple}; let target_features = if cfg!(feature = "no_cpu_compatibility_checks") { let mut fs = CpuFeature::set(); @@ -676,21 +676,10 @@ impl crate::runner::VM for NearVM { // FIXME: this mostly duplicates the `run_module` method. // Note that we don't clone the actual backing memory, just increase the RC. let vmmemory = memory.vm(); - let mut logic = VMLogic::new_with_protocol_version( - ext, - context, - &self.config, - fees_config, - promise_results, - &mut memory, - current_protocol_version, - ); + let mut logic = + VMLogic::new(ext, context, &self.config, fees_config, promise_results, &mut memory); - let result = logic.before_loading_executable( - method_name, - current_protocol_version, - code.code().len(), - ); + let result = logic.before_loading_executable(method_name, code.code().len()); if let Err(e) = result { return Ok(VMOutcome::abort(logic, e)); } @@ -703,7 +692,7 @@ impl crate::runner::VM for NearVM { } }; - let result = logic.after_loading_executable(current_protocol_version, code.code().len()); + let result = logic.after_loading_executable(code.code().len()); if let Err(e) = result { return Ok(VMOutcome::abort(logic, e)); } @@ -714,11 +703,7 @@ impl crate::runner::VM for NearVM { artifact.engine(), ); if let Err(e) = get_entrypoint_index(&*artifact, method_name) { - return Ok(VMOutcome::abort_but_nop_outcome_in_old_protocol( - logic, - e, - current_protocol_version, - )); + return Ok(VMOutcome::abort_but_nop_outcome_in_old_protocol(logic, e)); } match self.run_method(&artifact, import, method_name)? { Ok(()) => Ok(VMOutcome::ok(logic)), diff --git a/runtime/near-vm-runner/src/prepare.rs b/runtime/near-vm-runner/src/prepare.rs index 2b132276870..9318d30757f 100644 --- a/runtime/near-vm-runner/src/prepare.rs +++ b/runtime/near-vm-runner/src/prepare.rs @@ -3,7 +3,7 @@ use crate::internal::VMKind; use crate::logic::errors::PrepareError; -use crate::logic::VMConfig; +use crate::logic::Config; mod prepare_v0; mod prepare_v1; @@ -17,12 +17,12 @@ mod prepare_v2; /// - module doesn't define an internal memory instance, /// - imported memory (if any) doesn't reserve more memory than permitted by the `config`, /// - all imported functions from the external environment matches defined by `env` module, -/// - functions number does not exceed limit specified in VMConfig, +/// - functions number does not exceed limit specified in Config, /// /// The preprocessing includes injecting code for gas metering and metering the height of stack. pub fn prepare_contract( original_code: &[u8], - config: &VMConfig, + config: &Config, kind: VMKind, ) -> Result, PrepareError> { let prepare = config.limit_config.contract_prepare_version; @@ -55,7 +55,7 @@ mod tests { use assert_matches::assert_matches; fn parse_and_prepare_wat( - config: &VMConfig, + config: &Config, vm_kind: VMKind, wat: &str, ) -> Result, PrepareError> { @@ -65,7 +65,7 @@ mod tests { #[test] fn internal_memory_declaration() { - let config = VMConfig::test(); + let config = Config::test(); with_vm_variants(&config, |kind| { let r = parse_and_prepare_wat(&config, kind, r#"(module (memory 1 1))"#); assert_matches!(r, Ok(_)); @@ -74,7 +74,7 @@ mod tests { #[test] fn memory_imports() { - let config = VMConfig::test(); + let config = Config::test(); // This test assumes that maximum page number is configured to a certain number. assert_eq!(config.limit_config.max_memory_pages, 2048); @@ -119,7 +119,7 @@ mod tests { #[test] fn multiple_valid_memory_are_disabled() { - let config = VMConfig::test(); + let config = Config::test(); with_vm_variants(&config, |kind| { // Our preparation and sanitization pass assumes a single memory, so we should fail when // there are multiple specified. @@ -146,7 +146,7 @@ mod tests { #[test] fn imports() { - let config = VMConfig::test(); + let config = Config::test(); with_vm_variants(&config, |kind| { // nothing can be imported from non-"env" module for now. let r = parse_and_prepare_wat( diff --git a/runtime/near-vm-runner/src/prepare/prepare_v0.rs b/runtime/near-vm-runner/src/prepare/prepare_v0.rs index 7bda3ab54b2..21ae2df604a 100644 --- a/runtime/near-vm-runner/src/prepare/prepare_v0.rs +++ b/runtime/near-vm-runner/src/prepare/prepare_v0.rs @@ -1,14 +1,14 @@ //! Legacy validation for very old protocol versions. use crate::logic::errors::PrepareError; -use crate::logic::VMConfig; +use crate::logic::Config; use parity_wasm_41::builder; use parity_wasm_41::elements::{self, External, MemorySection, Type}; use pwasm_utils_12 as pwasm_utils; pub(crate) fn prepare_contract( original_code: &[u8], - config: &VMConfig, + config: &Config, ) -> Result, PrepareError> { ContractModule::init(original_code, config)? .standardize_mem() @@ -21,11 +21,11 @@ pub(crate) fn prepare_contract( struct ContractModule<'a> { module: elements::Module, - config: &'a VMConfig, + config: &'a Config, } impl<'a> ContractModule<'a> { - fn init(original_code: &[u8], config: &'a VMConfig) -> Result { + fn init(original_code: &[u8], config: &'a Config) -> Result { let module = elements::deserialize_buffer(original_code).map_err(|e| { tracing::debug!(err=?e, "parity_wasm_41 failed decoding a contract"); PrepareError::Deserialization diff --git a/runtime/near-vm-runner/src/prepare/prepare_v1.rs b/runtime/near-vm-runner/src/prepare/prepare_v1.rs index 8265f2babe4..36ed2a4ff4d 100644 --- a/runtime/near-vm-runner/src/prepare/prepare_v1.rs +++ b/runtime/near-vm-runner/src/prepare/prepare_v1.rs @@ -1,7 +1,7 @@ //! Less legacy validation for old protocol versions. use crate::logic::errors::PrepareError; -use crate::logic::VMConfig; +use crate::logic::Config; use parity_wasm::builder; use parity_wasm::elements::{self, External, MemorySection}; @@ -13,12 +13,12 @@ use parity_wasm::elements::{self, External, MemorySection}; /// - module doesn't define an internal memory instance, /// - imported memory (if any) doesn't reserve more memory than permitted by the `config`, /// - all imported functions from the external environment matches defined by `env` module, -/// - functions number does not exceed limit specified in VMConfig, +/// - functions number does not exceed limit specified in Config, /// /// The preprocessing includes injecting code for gas metering and metering the height of stack. pub(crate) fn prepare_contract( original_code: &[u8], - config: &VMConfig, + config: &Config, ) -> Result, PrepareError> { ContractModule::init(original_code, config)? .scan_imports()? @@ -31,11 +31,11 @@ pub(crate) fn prepare_contract( pub(crate) struct ContractModule<'a> { module: elements::Module, - config: &'a VMConfig, + config: &'a Config, } impl<'a> ContractModule<'a> { - pub(crate) fn init(original_code: &[u8], config: &'a VMConfig) -> Result { + pub(crate) fn init(original_code: &[u8], config: &'a Config) -> Result { let module = parity_wasm::deserialize_buffer(original_code).map_err(|e| { tracing::debug!(err=?e, "parity_wasm failed decoding a contract"); PrepareError::Deserialization @@ -207,7 +207,7 @@ fn wasmparser_decode( pub(crate) fn validate_contract( code: &[u8], features: crate::features::WasmFeatures, - config: &VMConfig, + config: &Config, ) -> Result<(), PrepareError> { let (function_count, local_count) = wasmparser_decode(code, features).map_err(|e| { tracing::debug!(err=?e, "wasmparser failed decoding a contract"); @@ -234,11 +234,11 @@ pub(crate) fn validate_contract( #[cfg(test)] mod test { - use crate::logic::{ContractPrepareVersion, VMConfig}; + use crate::logic::{Config, ContractPrepareVersion}; #[test] fn v1_preparation_generates_valid_contract() { - let mut config = VMConfig::test(); + let mut config = Config::test(); let prepare_version = ContractPrepareVersion::V1; config.limit_config.contract_prepare_version = prepare_version; let features = crate::features::WasmFeatures::from(prepare_version); diff --git a/runtime/near-vm-runner/src/prepare/prepare_v2.rs b/runtime/near-vm-runner/src/prepare/prepare_v2.rs index ad6c423d283..6085736ce0b 100644 --- a/runtime/near-vm-runner/src/prepare/prepare_v2.rs +++ b/runtime/near-vm-runner/src/prepare/prepare_v2.rs @@ -1,12 +1,12 @@ use crate::internal::VMKind; use crate::logic::errors::PrepareError; -use crate::logic::VMConfig; +use crate::logic::Config; use finite_wasm::wasmparser as wp; use wasm_encoder::{Encode, Section, SectionId}; struct PrepareContext<'a> { code: &'a [u8], - config: &'a VMConfig, + config: &'a Config, output_code: Vec, function_limit: u64, local_limit: u64, @@ -16,7 +16,7 @@ struct PrepareContext<'a> { } impl<'a> PrepareContext<'a> { - fn new(code: &'a [u8], features: crate::features::WasmFeatures, config: &'a VMConfig) -> Self { + fn new(code: &'a [u8], features: crate::features::WasmFeatures, config: &'a Config) -> Self { let limits = &config.limit_config; Self { code, @@ -268,7 +268,7 @@ impl<'a> PrepareContext<'a> { pub(crate) fn prepare_contract( original_code: &[u8], features: crate::features::WasmFeatures, - config: &VMConfig, + config: &Config, kind: VMKind, ) -> Result, PrepareError> { let lightly_steamed = PrepareContext::new(original_code, features, config).run()?; @@ -363,11 +363,11 @@ impl<'a> wp::VisitOperator<'a> for SimpleGasCostCfg { #[cfg(test)] mod test { use crate::internal::VMKind; - use crate::logic::{ContractPrepareVersion, VMConfig}; + use crate::logic::{Config, ContractPrepareVersion}; #[test] fn v2_preparation_wasmtime_generates_valid_contract() { - let mut config = VMConfig::test(); + let mut config = Config::test(); let prepare_version = ContractPrepareVersion::V2; config.limit_config.contract_prepare_version = prepare_version; let features = crate::features::WasmFeatures::from(prepare_version); @@ -394,7 +394,7 @@ mod test { #[test] fn v2_preparation_near_vm_generates_valid_contract() { - let mut config = VMConfig::test(); + let mut config = Config::test(); let prepare_version = ContractPrepareVersion::V2; config.limit_config.contract_prepare_version = prepare_version; let features = crate::features::WasmFeatures::from(prepare_version); diff --git a/runtime/near-vm-runner/src/runner.rs b/runtime/near-vm-runner/src/runner.rs index 135a12c1711..031769babed 100644 --- a/runtime/near-vm-runner/src/runner.rs +++ b/runtime/near-vm-runner/src/runner.rs @@ -1,10 +1,10 @@ +use crate::config::Config; use crate::errors::ContractPrecompilatonResult; use crate::logic::errors::{CacheError, CompilationError, VMRunnerError}; use crate::logic::types::PromiseResult; use crate::logic::{CompiledContractCache, External, VMContext, VMOutcome}; use crate::vm_kind::VMKind; -use near_primitives_core::config::VMConfig; -use near_primitives_core::contract::ContractCode; +use crate::ContractCode; use near_primitives_core::runtime::fees::RuntimeFeesConfig; use near_primitives_core::types::ProtocolVersion; @@ -46,7 +46,7 @@ pub fn run( method_name: &str, ext: &mut dyn External, context: VMContext, - wasm_config: &VMConfig, + wasm_config: &Config, fees_config: &RuntimeFeesConfig, promise_results: &[PromiseResult], current_protocol_version: ProtocolVersion, @@ -114,7 +114,7 @@ pub trait VM { /// into the `cache`. /// /// Further calls to [`Self::run`] or [`Self::precompile`] with the same - /// `code`, `cache` and [`VMConfig`] may reuse the results of this + /// `code`, `cache` and [`Config`] may reuse the results of this /// precompilation step. fn precompile( &self, @@ -128,7 +128,7 @@ impl VMKind { /// /// This is not intended to be used by code other than internal tools like /// the estimator. - pub fn runtime(&self, config: VMConfig) -> Option> { + pub fn runtime(&self, config: Config) -> Option> { match self { #[cfg(all(feature = "wasmer0_vm", target_arch = "x86_64"))] Self::Wasmer0 => Some(Box::new(crate::wasmer_runner::Wasmer0VM::new(config))), diff --git a/runtime/near-vm-runner/src/tests.rs b/runtime/near-vm-runner/src/tests.rs index 44fdad6b8fd..e5a5fb16368 100644 --- a/runtime/near-vm-runner/src/tests.rs +++ b/runtime/near-vm-runner/src/tests.rs @@ -8,10 +8,10 @@ pub(crate) mod test_builder; mod ts_contract; mod wasm_validation; -use crate::logic::{VMConfig, VMContext}; -use crate::vm_kind::VMKind; #[cfg(all(feature = "near_vm", target_arch = "x86_64"))] -use near_primitives_core::config::ContractPrepareVersion; +use crate::config::ContractPrepareVersion; +use crate::logic::{Config, VMContext}; +use crate::vm_kind::VMKind; use near_primitives_core::types::ProtocolVersion; const CURRENT_ACCOUNT_ID: &str = "alice"; @@ -21,7 +21,7 @@ const PREDECESSOR_ACCOUNT_ID: &str = "carol"; const LATEST_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::MAX; -pub(crate) fn with_vm_variants(#[allow(unused)] cfg: &VMConfig, runner: impl Fn(VMKind) -> ()) { +pub(crate) fn with_vm_variants(#[allow(unused)] cfg: &Config, runner: impl Fn(VMKind) -> ()) { #[cfg(all(feature = "wasmer0_vm", target_arch = "x86_64"))] runner(VMKind::Wasmer0); @@ -57,17 +57,3 @@ fn create_context(input: Vec) -> VMContext { output_data_receivers: vec![], } } - -/// Small helper to compute expected loading gas cost charged before loading. -/// -/// Includes hard-coded value for runtime parameter values -/// `wasm_contract_loading_base` and `wasm_contract_loading_bytes` which would -/// have to be updated if they change in the future. -#[allow(unused)] -fn prepaid_loading_gas(bytes: usize) -> u64 { - if cfg!(feature = "protocol_feature_fix_contract_loading_cost") { - 35_445_963 + bytes as u64 * 21_6750 - } else { - 0 - } -} diff --git a/runtime/near-vm-runner/src/tests/cache.rs b/runtime/near-vm-runner/src/tests/cache.rs index d2960d017c0..fd8ae100ac3 100644 --- a/runtime/near-vm-runner/src/tests/cache.rs +++ b/runtime/near-vm-runner/src/tests/cache.rs @@ -5,13 +5,13 @@ use super::{create_context, with_vm_variants, LATEST_PROTOCOL_VERSION}; use crate::internal::VMKind; use crate::logic::errors::VMRunnerError; use crate::logic::mocks::mock_external::MockedExternal; -use crate::logic::VMConfig; +use crate::logic::Config; use crate::logic::{CompiledContract, CompiledContractCache}; use crate::runner::VMResult; use crate::wasmer2_runner::Wasmer2VM; +use crate::ContractCode; use crate::{prepare, MockCompiledContractCache}; use assert_matches::assert_matches; -use near_primitives_core::contract::ContractCode; use near_primitives_core::hash::CryptoHash; use near_primitives_core::runtime::fees::RuntimeFeesConfig; use std::io; @@ -21,7 +21,7 @@ use wasmer_engine::Executable; #[test] fn test_caches_compilation_error() { - let config = VMConfig::test(); + let config = Config::test(); with_vm_variants(&config, |vm_kind: VMKind| { match vm_kind { VMKind::Wasmer0 | VMKind::Wasmer2 | VMKind::NearVm => {} @@ -45,7 +45,7 @@ fn test_caches_compilation_error() { #[test] fn test_does_not_cache_io_error() { - let config = VMConfig::test(); + let config = Config::test(); with_vm_variants(&config, |vm_kind: VMKind| { match vm_kind { VMKind::Wasmer0 | VMKind::Wasmer2 | VMKind::NearVm => {} @@ -75,7 +75,7 @@ fn test_does_not_cache_io_error() { } fn make_cached_contract_call_vm( - config: &VMConfig, + config: &Config, cache: &dyn CompiledContractCache, code: &[u8], method_name: &str, @@ -132,7 +132,7 @@ fn test_wasmer2_artifact_output_stability() { for seed in seeds { let contract = ContractCode::new(near_test_contracts::arbitrary_contract(seed), None); - let config = VMConfig::test(); + let config = Config::test(); let prepared_code = prepare::prepare_contract(contract.code(), &config, VMKind::Wasmer2).unwrap(); got_prepared_hashes.push(crate::utils::stable_hash((&contract.code(), &prepared_code))); @@ -203,7 +203,7 @@ fn test_near_vm_artifact_output_stability() { for seed in seeds { let contract = ContractCode::new(near_test_contracts::arbitrary_contract(seed), None); - let config = VMConfig::test(); + let config = Config::test(); let prepared_code = prepare::prepare_contract(contract.code(), &config, VMKind::NearVm).unwrap(); got_prepared_hashes.push(crate::utils::stable_hash((&contract.code(), &prepared_code))); diff --git a/runtime/near-vm-runner/src/tests/compile_errors.rs b/runtime/near-vm-runner/src/tests/compile_errors.rs index 9c17bc61581..d70fa030eb7 100644 --- a/runtime/near-vm-runner/src/tests/compile_errors.rs +++ b/runtime/near-vm-runner/src/tests/compile_errors.rs @@ -2,6 +2,8 @@ use super::test_builder::test_builder; use expect_test::expect; use near_primitives_core::version::ProtocolFeature; +const FIX_CONTRACT_LOADING_COST: u32 = 129; + #[test] fn test_initializer_wrong_signature_contract() { test_builder() @@ -13,16 +15,12 @@ fn test_initializer_wrong_signature_contract() { (func (export "main")) )"#, ) - .protocol_features(&[ - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, - ]) + .protocol_version(FIX_CONTRACT_LOADING_COST) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 Err: PrepareError: Error happened while deserializing the module. "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 48017463 used gas 48017463 Err: PrepareError: Error happened while deserializing the module. @@ -36,16 +34,14 @@ fn test_function_not_defined_contract() { test_builder() .wat(r#"(module (export "hello" (func 0)))"#) .method("hello") - .protocol_features(&[ - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, - ]) + .protocol_version( + FIX_CONTRACT_LOADING_COST + ) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 Err: PrepareError: Error happened while deserializing the module. "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 39564213 used gas 39564213 Err: PrepareError: Error happened while deserializing the module. @@ -68,16 +64,14 @@ fn function_type_not_defined_contract(bad_type: u64) -> Vec { fn test_function_type_not_defined_contract_1() { test_builder() .wasm(&function_type_not_defined_contract(1)) - .protocol_features(&[ - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, - ]) + .protocol_version( + FIX_CONTRACT_LOADING_COST + ) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 Err: PrepareError: Error happened while deserializing the module. "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 44982963 used gas 44982963 Err: PrepareError: Error happened while deserializing the module. @@ -90,16 +84,14 @@ fn test_function_type_not_defined_contract_1() { fn test_function_type_not_defined_contract_2() { test_builder() .wasm(&function_type_not_defined_contract(0)) - .protocol_features(&[ - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, - ]) + .protocol_version( + FIX_CONTRACT_LOADING_COST + ) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 Err: PrepareError: Error happened while deserializing the module. "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 44982963 used gas 44982963 Err: PrepareError: Error happened while deserializing the module. @@ -111,16 +103,13 @@ fn test_function_type_not_defined_contract_2() { fn test_garbage_contract() { test_builder() .wasm(&[]) - .protocol_features(&[ - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, - ]) + .protocol_version(FIX_CONTRACT_LOADING_COST + ) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 Err: PrepareError: Error happened while deserializing the module. "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 35445963 used gas 35445963 Err: PrepareError: Error happened while deserializing the module. @@ -133,16 +122,14 @@ fn test_evil_function_index() { test_builder() .wat(r#"(module (func (export "main") call 4294967295))"#) .method("abort_with_zero") - .protocol_features(&[ - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, - ]) + .protocol_version( + FIX_CONTRACT_LOADING_COST + ) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 Err: PrepareError: Error happened while deserializing the module. "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 44115963 used gas 44115963 Err: PrepareError: Error happened while deserializing the module. @@ -164,9 +151,8 @@ fn test_limit_contract_functions_number() { .protocol_features(&[ ProtocolFeature::LimitContractFunctionsNumber, ProtocolFeature::PreparationV2, - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, ]) + .protocol_version(FIX_CONTRACT_LOADING_COST) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 13048032213 used gas 13048032213 @@ -177,7 +163,6 @@ fn test_limit_contract_functions_number() { expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 13054614261 used gas 13054614261 "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 13054614261 used gas 13054614261 "#]], @@ -193,9 +178,8 @@ fn test_limit_contract_functions_number() { .protocol_features(&[ ProtocolFeature::LimitContractFunctionsNumber, ProtocolFeature::PreparationV2, - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, ]) + .protocol_version(FIX_CONTRACT_LOADING_COST) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 13049332713 used gas 13049332713 @@ -208,7 +192,6 @@ fn test_limit_contract_functions_number() { VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 Err: PrepareError: Too many functions in contract. "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 13049332713 used gas 13049332713 Err: PrepareError: Too many functions in contract. @@ -226,9 +209,10 @@ fn test_limit_contract_functions_number() { ) .protocol_features(&[ ProtocolFeature::PreparationV2, - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, ]) + .protocol_version( + FIX_CONTRACT_LOADING_COST + ) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 @@ -238,7 +222,6 @@ fn test_limit_contract_functions_number() { VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 Err: PrepareError: Too many functions in contract. "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 19554433713 used gas 19554433713 Err: PrepareError: Too many functions in contract. @@ -256,9 +239,9 @@ fn test_limit_contract_functions_number() { ) .protocol_features(&[ ProtocolFeature::PreparationV2, - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, + ]) + .protocol_version(FIX_CONTRACT_LOADING_COST) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 @@ -268,7 +251,6 @@ fn test_limit_contract_functions_number() { VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 Err: PrepareError: Too many functions in contract. "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 13051283463 used gas 13051283463 Err: PrepareError: Too many functions in contract. @@ -289,9 +271,11 @@ fn test_limit_locals() { ) .protocol_features(&[ ProtocolFeature::PreparationV2, - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, ]) + + .protocol_version( + FIX_CONTRACT_LOADING_COST + ) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 @@ -301,7 +285,6 @@ fn test_limit_locals() { VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 Err: PrepareError: Error happened while deserializing the module. "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 43682463 used gas 43682463 Err: PrepareError: Error happened while deserializing the module. @@ -345,9 +328,9 @@ fn test_limit_locals_global() { .protocol_features(&[ ProtocolFeature::LimitContractLocals, ProtocolFeature::PreparationV2, - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, + ]) + .protocol_version(FIX_CONTRACT_LOADING_COST,) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 195407463 used gas 195407463 @@ -360,7 +343,6 @@ fn test_limit_locals_global() { VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 Err: PrepareError: Too many locals declared in the contract. "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 195407463 used gas 195407463 Err: PrepareError: Too many locals declared in the contract. @@ -434,12 +416,12 @@ fn test_sandbox_only_function() { .opaque_error(); #[cfg(feature = "sandbox")] - tb.expect(expect![[r#" + tb.expect(&expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 59805981 used gas 59805981 "#]]); #[cfg(not(feature = "sandbox"))] - tb.expect(expect![[r#" + tb.expect(&expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 57337713 used gas 57337713 Err: ... "#]]); @@ -447,24 +429,25 @@ fn test_sandbox_only_function() { #[test] fn extension_saturating_float_to_int() { - let tb = test_builder().wat( - r#" + test_builder() + .wat( + r#" (module (func $test_trunc (param $x f64) (result i32) (i32.trunc_sat_f64_s (local.get $x))) ) "#, - ); - - #[cfg(feature = "nightly")] - tb.expect(expect![[r#" - VMOutcome: balance 4 storage_usage 12 return data None burnt gas 48450963 used gas 48450963 - Err: PrepareError: Error happened while deserializing the module. - "#]]); - #[cfg(not(feature = "nightly"))] - tb.expect(expect![[r#" - VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 - Err: PrepareError: Error happened while deserializing the module. - "#]]); + ) + .protocol_version(FIX_CONTRACT_LOADING_COST) + .expects(&[ + expect![[r#" + VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 + Err: PrepareError: Error happened while deserializing the module. + "#]], + expect![[r#" + VMOutcome: balance 4 storage_usage 12 return data None burnt gas 48450963 used gas 48450963 + Err: PrepareError: Error happened while deserializing the module. + "#]], + ]); } #[test] diff --git a/runtime/near-vm-runner/src/tests/fuzzers.rs b/runtime/near-vm-runner/src/tests/fuzzers.rs index ffff9587e17..56f6437213a 100644 --- a/runtime/near-vm-runner/src/tests/fuzzers.rs +++ b/runtime/near-vm-runner/src/tests/fuzzers.rs @@ -2,12 +2,12 @@ use crate::internal::wasmparser::{Export, ExternalKind, Parser, Payload, TypeDef use crate::internal::VMKind; use crate::logic::errors::FunctionCallError; use crate::logic::mocks::mock_external::MockedExternal; -use crate::logic::{VMConfig, VMContext}; +use crate::logic::{Config, VMContext}; use crate::runner::VMResult; +use crate::ContractCode; use arbitrary::Arbitrary; use bolero::check; use core::fmt; -use near_primitives_core::contract::ContractCode; use near_primitives_core::runtime::fees::RuntimeFeesConfig; use near_primitives_core::version::PROTOCOL_VERSION; @@ -111,7 +111,7 @@ fn run_fuzz(code: &ContractCode, vm_kind: VMKind) -> VMResult { let mut context = create_context(vec![]); context.prepaid_gas = 10u64.pow(14); - let mut config = VMConfig::test(); + let mut config = Config::test(); config.limit_config.wasmer2_stack_limit = i32::MAX; // If we can crash wasmer2 even without the secondary stack limit it's still good to know let fees = RuntimeFeesConfig::test(); @@ -198,7 +198,7 @@ fn near_vm_is_reproducible() { bolero::check!().for_each(|data: &[u8]| { if let Ok(module) = ArbitraryModule::arbitrary(&mut arbitrary::Unstructured::new(data)) { let code = ContractCode::new(module.0.module.to_bytes(), None); - let config = VMConfig::test(); + let config = Config::test(); let mut first_hash = None; for _ in 0..3 { let vm = NearVM::new(config.clone()); diff --git a/runtime/near-vm-runner/src/tests/rs_contract.rs b/runtime/near-vm-runner/src/tests/rs_contract.rs index d7b45aa0925..b8c0e843c76 100644 --- a/runtime/near-vm-runner/src/tests/rs_contract.rs +++ b/runtime/near-vm-runner/src/tests/rs_contract.rs @@ -1,10 +1,9 @@ -use crate::logic::action::{Action, FunctionCallAction}; use crate::logic::errors::{FunctionCallError, HostError, WasmTrap}; -use crate::logic::mocks::mock_external::MockedExternal; +use crate::logic::mocks::mock_external::{MockAction, MockedExternal}; use crate::logic::types::ReturnData; -use crate::logic::{ReceiptMetadata, VMConfig}; +use crate::logic::Config; +use crate::ContractCode; use near_primitives::test_utils::encode; -use near_primitives_core::contract::ContractCode; use near_primitives_core::runtime::fees::RuntimeFeesConfig; use near_primitives_core::types::Balance; use std::mem::size_of; @@ -44,7 +43,7 @@ fn assert_run_result(result: VMResult, expected_value: u64) { #[test] pub fn test_read_write() { - let config = VMConfig::test(); + let config = Config::test(); with_vm_variants(&config, |vm_kind: VMKind| { let code = test_contract(vm_kind); let mut fake_external = MockedExternal::new(); @@ -85,7 +84,7 @@ macro_rules! def_test_ext { ($name:ident, $method:expr, $expected:expr, $input:expr, $validator:expr) => { #[test] pub fn $name() { - let config = VMConfig::test(); + let config = Config::test(); with_vm_variants(&config, |vm_kind: VMKind| { run_test_ext(&config, $method, $expected, $input, $validator, vm_kind) }); @@ -94,7 +93,7 @@ macro_rules! def_test_ext { ($name:ident, $method:expr, $expected:expr, $input:expr) => { #[test] pub fn $name() { - let config = VMConfig::test(); + let config = Config::test(); with_vm_variants(&config, |vm_kind: VMKind| { run_test_ext(&config, $method, $expected, $input, vec![], vm_kind) }); @@ -103,7 +102,7 @@ macro_rules! def_test_ext { ($name:ident, $method:expr, $expected:expr) => { #[test] pub fn $name() { - let config = VMConfig::test(); + let config = Config::test(); with_vm_variants(&config, |vm_kind: VMKind| { run_test_ext(&config, $method, $expected, &[], vec![], vm_kind) }) @@ -112,7 +111,7 @@ macro_rules! def_test_ext { } fn run_test_ext( - config: &VMConfig, + config: &Config, method: &str, expected: &[u8], input: &[u8], @@ -159,7 +158,7 @@ def_test_ext!(ext_storage_usage, "ext_storage_usage", &12u64.to_le_bytes()); #[test] pub fn ext_used_gas() { - let config = VMConfig::test(); + let config = Config::test(); with_vm_variants(&config, |vm_kind: VMKind| { // Note, the used_gas is not a global used_gas at the beginning of method, but instead a // diff in used_gas for computing fib(30) in a loop @@ -217,7 +216,7 @@ def_test_ext!( #[test] pub fn test_out_of_memory() { - let config = VMConfig::free(); + let config = Config::free(); with_vm_variants(&config, |vm_kind: VMKind| { // TODO: currently we only run this test on Wasmer. match vm_kind { @@ -260,55 +259,12 @@ fn function_call_weight_contract() -> ContractCode { ContractCode::new(near_test_contracts::rs_contract().to_vec(), None) } -#[test] -fn attach_unspent_gas_but_burn_all_gas() { - let prepaid_gas = 100 * 10u64.pow(12); - - let mut context = create_context(vec![]); - context.prepaid_gas = prepaid_gas; - - let mut config = VMConfig::test(); - config.limit_config.max_gas_burnt = context.prepaid_gas / 3; - - with_vm_variants(&config, |vm_kind: VMKind| { - let code = function_call_weight_contract(); - let mut external = MockedExternal::new(); - let fees = RuntimeFeesConfig::test(); - let runtime = vm_kind.runtime(config.clone()).expect("runtime has not been compiled"); - - let outcome = runtime - .run( - &code, - "attach_unspent_gas_but_burn_all_gas", - &mut external, - context.clone(), - &fees, - &[], - LATEST_PROTOCOL_VERSION, - None, - ) - .unwrap_or_else(|err| panic!("Failed execution: {:?}", err)); - - let err = outcome.aborted.as_ref().unwrap(); - assert!(matches!(err, FunctionCallError::HostError(HostError::GasLimitExceeded))); - match &outcome.action_receipts.as_slice() { - [(_, ReceiptMetadata { actions, .. })] => match actions.as_slice() { - [Action::FunctionCall(FunctionCallAction { gas, .. })] => { - assert!(*gas > prepaid_gas / 3); - } - other => panic!("unexpected actions: {other:?}"), - }, - other => panic!("unexpected receipts: {other:?}"), - } - }); -} - #[test] fn attach_unspent_gas_but_use_all_gas() { let mut context = create_context(vec![]); context.prepaid_gas = 100 * 10u64.pow(12); - let mut config = VMConfig::test(); + let mut config = Config::test(); config.limit_config.max_gas_burnt = context.prepaid_gas / 3; with_vm_variants(&config, |vm_kind: VMKind| { @@ -333,14 +289,9 @@ fn attach_unspent_gas_but_use_all_gas() { let err = outcome.aborted.as_ref().unwrap(); assert!(matches!(err, FunctionCallError::HostError(HostError::GasExceeded))); - match &outcome.action_receipts.as_slice() { - [(_, ReceiptMetadata { actions, .. }), _] => match actions.as_slice() { - [Action::FunctionCall(FunctionCallAction { gas, .. })] => { - assert_eq!(*gas, 0); - } - other => panic!("unexpected actions: {other:?}"), - }, - other => panic!("unexpected receipts: {other:?}"), + match &external.action_log[..] { + [_, MockAction::FunctionCallWeight { prepaid_gas: gas, .. }, _] => assert_eq!(*gas, 0), + other => panic!("unexpected actions: {other:?}"), } }); } diff --git a/runtime/near-vm-runner/src/tests/runtime_errors.rs b/runtime/near-vm-runner/src/tests/runtime_errors.rs index 66a2b4f1bd8..3e523ce209e 100644 --- a/runtime/near-vm-runner/src/tests/runtime_errors.rs +++ b/runtime/near-vm-runner/src/tests/runtime_errors.rs @@ -3,6 +3,8 @@ use expect_test::expect; use near_primitives_core::version::ProtocolFeature; use std::fmt::Write; +const FIX_CONTRACT_LOADING_COST: u32 = 129; + static INFINITE_INITIALIZER_CONTRACT: &str = r#" (module (func $start (loop (br 0))) @@ -15,7 +17,7 @@ fn test_infinite_initializer() { test_builder() .wat(INFINITE_INITIALIZER_CONTRACT) .gas(10u64.pow(10)) - .expect(expect![[r#" + .expect(&expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 10000000000 used gas 10000000000 Err: Exceeded the prepaid gas. "#]]); @@ -26,15 +28,12 @@ fn test_infinite_initializer_export_not_found() { test_builder() .wat(INFINITE_INITIALIZER_CONTRACT) .method("no-such-method") - .protocol_features(&[ - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, - ]).expects(&[ + .protocol_version(FIX_CONTRACT_LOADING_COST) + .expects(&[ expect![[r#" VMOutcome: balance 0 storage_usage 0 return data None burnt gas 0 used gas 0 Err: MethodNotFound "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 49101213 used gas 49101213 Err: MethodNotFound @@ -70,16 +69,12 @@ fn test_imported_memory() { ]) // Wasmtime classifies this error as link error at the moment. .opaque_error() - .protocol_features(&[ - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, - ]) + .protocol_version(FIX_CONTRACT_LOADING_COST) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 Err: ... "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 44982963 used gas 44982963 Err: ... @@ -92,16 +87,12 @@ fn test_multiple_memories() { test_builder() .wat("(module (memory 1 2) (memory 3 4))") .opaque_error() - .protocol_features(&[ - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, - ]) + .protocol_version(FIX_CONTRACT_LOADING_COST) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 Err: ... "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 39130713 used gas 39130713 Err: ... @@ -113,15 +104,12 @@ fn test_multiple_memories() { fn test_export_not_found() { test_builder().wat(SIMPLE_CONTRACT) .method("no-such-method") - .protocol_features(&[ - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, - ]).expects(&[ + .protocol_version(FIX_CONTRACT_LOADING_COST) + .expects(&[ expect![[r#" VMOutcome: balance 0 storage_usage 0 return data None burnt gas 0 used gas 0 Err: MethodNotFound "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 42815463 used gas 42815463 Err: MethodNotFound @@ -131,7 +119,7 @@ fn test_export_not_found() { #[test] fn test_empty_method() { - test_builder().wat(SIMPLE_CONTRACT).method("").expect(expect![[r#" + test_builder().wat(SIMPLE_CONTRACT).method("").expect(&expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 Err: MethodEmptyName "#]]); @@ -320,16 +308,12 @@ fn test_indirect_call_to_wrong_signature_contract() { fn test_wrong_signature_contract() { test_builder() .wat(r#"(module (func (export "main") (param i32)))"#) - .protocol_features(&[ - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, - ]) + .protocol_version(FIX_CONTRACT_LOADING_COST) .expects(&[ expect![[r#" VMOutcome: balance 0 storage_usage 0 return data None burnt gas 0 used gas 0 Err: MethodInvalidSignature "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 43032213 used gas 43032213 Err: MethodInvalidSignature @@ -341,16 +325,12 @@ fn test_wrong_signature_contract() { fn test_export_wrong_type() { test_builder() .wat(r#"(module (global (export "main") i32 (i32.const 123)))"#) - .protocol_features(&[ - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, - ]) + .protocol_version(FIX_CONTRACT_LOADING_COST) .expects(&[ expect![[r#" VMOutcome: balance 0 storage_usage 0 return data None burnt gas 0 used gas 0 Err: MethodNotFound "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 41298213 used gas 41298213 Err: MethodNotFound @@ -394,7 +374,7 @@ fn test_panic_re_export() { (export "main" (func $panic)) )"#, ) - .expect(expect![[r#" + .expect(&expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 312352074 used gas 312352074 Err: Smart contract panicked: explicit guest panic "#]]); @@ -445,7 +425,7 @@ fn test_stack_instrumentation_protocol_upgrade() { .opaque_error() .expects(&[ expect![[r#" - VMOutcome: balance 4 storage_usage 12 return data None burnt gas 6789985365 used gas 6789985365 + VMOutcome: balance 4 storage_usage 12 return data None burnt gas 18136872021 used gas 18136872021 Err: ... "#]], expect![[r#" @@ -480,7 +460,7 @@ fn test_stack_instrumentation_protocol_upgrade() { .skip_wasmtime() .expects(&[ expect![[r#" - VMOutcome: balance 4 storage_usage 12 return data None burnt gas 6789985365 used gas 6789985365 + VMOutcome: balance 4 storage_usage 12 return data None burnt gas 18136872021 used gas 18136872021 Err: ... "#]], expect![[r#" @@ -511,7 +491,7 @@ fn test_memory_grow() { )"#, ) .gas(10u64.pow(10)) - .expect(expect![[r#" + .expect(&expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 10000000000 used gas 10000000000 Err: Exceeded the prepaid gas. "#]]); @@ -546,16 +526,12 @@ fn bad_import_func(env: &str) -> Vec { fn test_bad_import_1() { test_builder() .wasm(&bad_import_global("no-such-module")) - .protocol_features(&[ - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, - ]) + .protocol_version(FIX_CONTRACT_LOADING_COST) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 Err: PrepareError: Error happened during instantiation. "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 50618463 used gas 50618463 Err: PrepareError: Error happened during instantiation. @@ -567,16 +543,12 @@ fn test_bad_import_1() { fn test_bad_import_2() { test_builder() .wasm(&bad_import_func("no-such-module")) - .protocol_features(&[ - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, - ]) + .protocol_version(FIX_CONTRACT_LOADING_COST) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 Err: PrepareError: Error happened during instantiation. "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 50184963 used gas 50184963 Err: PrepareError: Error happened during instantiation. @@ -591,9 +563,8 @@ fn test_bad_import_3() { .opaque_error() .protocol_features(&[ ProtocolFeature::PreparationV2, - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] - ProtocolFeature::FixContractLoadingCost, ]) + .protocol_version(FIX_CONTRACT_LOADING_COST) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 48234213 used gas 48234213 @@ -603,7 +574,6 @@ fn test_bad_import_3() { VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 Err: ... "#]], - #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 48234213 used gas 48234213 Err: ... @@ -613,7 +583,7 @@ fn test_bad_import_3() { #[test] fn test_bad_import_4() { - test_builder().wasm(&bad_import_func("env")).opaque_error().expect(expect![[r#" + test_builder().wasm(&bad_import_func("env")).opaque_error().expect(&expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 47800713 used gas 47800713 Err: ... "#]]); @@ -630,7 +600,7 @@ fn test_initializer_no_gas() { )"#, ) .gas(0) - .expect(expect![[r#" + .expect(&expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 Err: Exceeded the prepaid gas. "#]]); @@ -652,7 +622,7 @@ fn test_bad_many_imports() { )"#, )) .opaque_error() - .expect(expect![[r#" + .expect(&expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 299447463 used gas 299447463 Err: ... "#]]) @@ -684,7 +654,7 @@ fn test_external_call_ok() { #[test] fn test_external_call_error() { - test_builder().wat(EXTERNAL_CALL_CONTRACT).gas(100).expect(expect![[r#" + test_builder().wat(EXTERNAL_CALL_CONTRACT).gas(100).expect(&expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 100 used gas 100 Err: Exceeded the prepaid gas. "#]]); @@ -833,7 +803,7 @@ fn test_nan_sign() { // even load a contract. #[test] fn test_gas_exceed_loading() { - test_builder().wat(SIMPLE_CONTRACT).method("non_empty_non_existing").gas(1).expect(expect![[ + test_builder().wat(SIMPLE_CONTRACT).method("non_empty_non_existing").gas(1).expect(&expect![[ r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 1 used gas 1 Err: Exceeded the prepaid gas. @@ -854,7 +824,7 @@ fn gas_overflow_direct_call() { (call $gas (i32.const 0xffff_ffff))) )"#, ) - .expect(expect![[r#" + .expect(&expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 100000000000000 used gas 100000000000000 Err: Exceeded the maximum amount of gas allowed to burn per contract. "#]]); @@ -881,16 +851,16 @@ fn gas_overflow_indirect_call() { (i32.const 0))) )"#, ) - .expect(expect![[r#" + .expect(&expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 100000000000000 used gas 100000000000000 Err: Exceeded the maximum amount of gas allowed to burn per contract. "#]]); } -#[cfg(feature = "protocol_feature_fix_contract_loading_cost")] mod fix_contract_loading_cost_protocol_upgrade { + use near_primitives_core::config::ExtCosts; + use super::*; - use crate::tests::prepaid_loading_gas; static ALMOST_TRIVIAL_CONTRACT: &str = r#" (module @@ -908,15 +878,15 @@ mod fix_contract_loading_cost_protocol_upgrade { test_builder() .wat(ALMOST_TRIVIAL_CONTRACT) .protocol_features(&[ - ProtocolFeature::FixContractLoadingCost, ProtocolFeature::PreparationV2 ]) + .protocol_version(FIX_CONTRACT_LOADING_COST) .expects(&[ expect![[r#" - VMOutcome: balance 4 storage_usage 12 return data None burnt gas 53989035 used gas 53989035 + VMOutcome: balance 4 storage_usage 12 return data None burnt gas 47406987 used gas 47406987 "#]], expect![[r#" - VMOutcome: balance 4 storage_usage 12 return data None burnt gas 47406987 used gas 47406987 + VMOutcome: balance 4 storage_usage 12 return data None burnt gas 53989035 used gas 53989035 "#]], expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 53989035 used gas 53989035 @@ -928,42 +898,43 @@ mod fix_contract_loading_cost_protocol_upgrade { // after. Both charge the same amount of gas. #[test] fn test_fn_loading_gas_protocol_upgrade_exceed_loading() { - let tb = test_builder().wat(ALMOST_TRIVIAL_CONTRACT); - let loading_cost = prepaid_loading_gas(tb.get_wasm().len()); - tb - .gas(loading_cost) - .protocol_features(&[ProtocolFeature::FixContractLoadingCost]) - .expects(&[ - expect![[r#" - VMOutcome: balance 4 storage_usage 12 return data None burnt gas 44115963 used gas 44115963 - Err: Exceeded the prepaid gas. - "#]], - expect![[r#" - VMOutcome: balance 4 storage_usage 12 return data None burnt gas 44115963 used gas 44115963 - Err: Exceeded the prepaid gas. - "#]], - ]); + let expect = expect![[r#" + VMOutcome: balance 4 storage_usage 12 return data None burnt gas 44115963 used gas 44115963 + Err: Exceeded the prepaid gas. + "#]]; + let test_after = test_builder().wat(ALMOST_TRIVIAL_CONTRACT); + let cfg_costs = &test_after.configs().next().unwrap().wasm_config.ext_costs; + let loading_base = cfg_costs.gas_cost(ExtCosts::contract_loading_base); + let loading_byte = cfg_costs.gas_cost(ExtCosts::contract_loading_bytes); + let wasm_length = test_after.get_wasm().len(); + test_after.gas(loading_base + wasm_length as u64 * loading_byte).expect(&expect); + test_builder() + .wat(ALMOST_TRIVIAL_CONTRACT) + .only_protocol_versions(vec![FIX_CONTRACT_LOADING_COST - 1]) + .gas(loading_base + wasm_length as u64 * loading_byte) + .expect(&expect); } /// Executing with enough gas to finish loading but not to execute the full /// contract should have the same outcome before and after. #[test] fn test_fn_loading_gas_protocol_upgrade_exceed_executing() { - let tb = test_builder().wat(ALMOST_TRIVIAL_CONTRACT); - let loading_cost = prepaid_loading_gas(tb.get_wasm().len()); - tb - .gas(loading_cost + 884037) - .protocol_features(&[ProtocolFeature::FixContractLoadingCost]) - .expects(&[ - expect![[r#" - VMOutcome: balance 4 storage_usage 12 return data None burnt gas 45000000 used gas 45000000 - Err: Exceeded the prepaid gas. - "#]], - expect![[r#" - VMOutcome: balance 4 storage_usage 12 return data None burnt gas 45000000 used gas 45000000 - Err: Exceeded the prepaid gas. - "#]], - ]); + let expect = expect![[r#" + VMOutcome: balance 4 storage_usage 12 return data None burnt gas 45000000 used gas 45000000 + Err: Exceeded the prepaid gas. + "#]]; + let test_after = test_builder().wat(ALMOST_TRIVIAL_CONTRACT); + let cfg_costs = &test_after.configs().next().unwrap().wasm_config.ext_costs; + let loading_base = cfg_costs.gas_cost(ExtCosts::contract_loading_base); + let loading_byte = cfg_costs.gas_cost(ExtCosts::contract_loading_bytes); + let wasm_length = test_after.get_wasm().len(); + let prepaid_gas = loading_base + wasm_length as u64 * loading_byte + 884037; + test_after.gas(prepaid_gas).expect(&expect); + test_builder() + .wat(ALMOST_TRIVIAL_CONTRACT) + .only_protocol_versions(vec![FIX_CONTRACT_LOADING_COST - 1]) + .gas(prepaid_gas) + .expect(&expect); } /// Failure during preparation must remain free of gas charges for old versions @@ -979,7 +950,7 @@ mod fix_contract_loading_cost_protocol_upgrade { test_builder() .wat(r#"(module (export "main" (func 0)))"#) - .protocol_features(&[ProtocolFeature::FixContractLoadingCost]) + .protocol_version(FIX_CONTRACT_LOADING_COST) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 @@ -993,7 +964,7 @@ mod fix_contract_loading_cost_protocol_upgrade { test_builder() .wasm(&bad_import_global("wtf")) - .protocol_features(&[ProtocolFeature::FixContractLoadingCost]) + .protocol_version(FIX_CONTRACT_LOADING_COST) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 @@ -1007,7 +978,7 @@ mod fix_contract_loading_cost_protocol_upgrade { test_builder() .wasm(&bad_import_func("wtf")) - .protocol_features(&[ProtocolFeature::FixContractLoadingCost]) + .protocol_version(FIX_CONTRACT_LOADING_COST) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 @@ -1026,7 +997,7 @@ mod fix_contract_loading_cost_protocol_upgrade { ..Default::default() } .make()) - .protocol_features(&[ProtocolFeature::FixContractLoadingCost]) + .protocol_version(FIX_CONTRACT_LOADING_COST) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 @@ -1046,7 +1017,7 @@ mod fix_contract_loading_cost_protocol_upgrade { ..Default::default() } .make()) - .protocol_features(&[ProtocolFeature::FixContractLoadingCost]) + .protocol_version(FIX_CONTRACT_LOADING_COST) .expects(&[ expect![[r#" VMOutcome: balance 4 storage_usage 12 return data None burnt gas 0 used gas 0 @@ -1059,3 +1030,30 @@ mod fix_contract_loading_cost_protocol_upgrade { ]); } } + +#[test] +fn test_regression_9393() { + let before_builder = test_builder().only_protocol_versions(vec![62]); + let after_builder = test_builder().only_protocol_versions(vec![63]); + let before_cost = before_builder.configs().next().unwrap().wasm_config.regular_op_cost; + let after_cost = after_builder.configs().next().unwrap().wasm_config.regular_op_cost; + assert_eq!( + before_cost, after_cost, + "this test is not set up to test with different insn costs" + ); + let cost = u64::from(before_cost); + + let nops = (i32::MAX as u64 + cost - 1) / cost; + let contract = near_test_contracts::function_with_a_lot_of_nop(nops); + before_builder.wasm(&contract).only_near_vm().expects(&[ + expect![[r#" + VMOutcome: balance 4 storage_usage 12 return data None burnt gas 100000000000000 used gas 100000000000000 + Err: Exceeded the maximum amount of gas allowed to burn per contract. + "#]], + ]); + after_builder.wasm(&contract).expects(&[ + expect![[r#" + VMOutcome: balance 4 storage_usage 12 return data None burnt gas 2763981177 used gas 2763981177 + "#]], + ]); +} diff --git a/runtime/near-vm-runner/src/tests/test_builder.rs b/runtime/near-vm-runner/src/tests/test_builder.rs index 01d4530f5c8..953bc1e28eb 100644 --- a/runtime/near-vm-runner/src/tests/test_builder.rs +++ b/runtime/near-vm-runner/src/tests/test_builder.rs @@ -1,12 +1,14 @@ -use crate::internal::VMKind; -use crate::logic::{mocks::mock_external::MockedExternal, ProtocolVersion, VMContext, VMOutcome}; -use near_primitives::runtime::{config_store::RuntimeConfigStore, fees::RuntimeFeesConfig}; -use near_primitives_core::{ - contract::ContractCode, - types::Gas, - version::{ProtocolFeature, PROTOCOL_VERSION}, +use near_primitives::runtime::{ + config::RuntimeConfig, config_store::RuntimeConfigStore, fees::RuntimeFeesConfig, }; -use std::{collections::HashSet, fmt::Write}; +use near_primitives_core::types::Gas; +use near_primitives_core::version::ProtocolFeature; +use near_vm_runner::internal::VMKind; +use near_vm_runner::logic::{ + mocks::mock_external::MockedExternal, ProtocolVersion, VMContext, VMOutcome, +}; +use near_vm_runner::ContractCode; +use std::{collections::HashSet, fmt::Write, sync::Arc}; pub(crate) fn test_builder() -> TestBuilder { let context = VMContext { @@ -31,7 +33,7 @@ pub(crate) fn test_builder() -> TestBuilder { code: ContractCode::new(Vec::new(), None), context, method: "main".to_string(), - protocol_versions: vec![PROTOCOL_VERSION], + protocol_versions: vec![u32::MAX], skip: HashSet::new(), opaque_error: false, opaque_outcome: false, @@ -88,25 +90,21 @@ impl TestBuilder { // We only test trapping tests on Wasmer, as of version 0.17, when tests executed in parallel, // Wasmer signal handlers may catch signals thrown from the Wasmtime, and produce fake failing tests. - #[allow(dead_code)] pub(crate) fn skip_wasmtime(mut self) -> Self { self.skip.insert(VMKind::Wasmtime); self } - #[allow(dead_code)] pub(crate) fn skip_wasmer0(mut self) -> Self { self.skip.insert(VMKind::Wasmer0); self } - #[allow(dead_code)] pub(crate) fn skip_wasmer2(mut self) -> Self { self.skip.insert(VMKind::Wasmer2); self } - #[allow(dead_code)] pub(crate) fn skip_near_vm(mut self) -> Self { self.skip.insert(VMKind::NearVm); self @@ -117,7 +115,6 @@ impl TestBuilder { self.skip_wasmer0().skip_wasmer2().skip_near_vm() } - #[allow(dead_code)] pub(crate) fn only_wasmer0(self) -> Self { self.skip_wasmer2().skip_near_vm().skip_wasmtime() } @@ -127,13 +124,11 @@ impl TestBuilder { self.skip_wasmer0().skip_near_vm().skip_wasmtime() } - #[allow(dead_code)] pub(crate) fn only_near_vm(self) -> Self { self.skip_wasmer0().skip_wasmer2().skip_wasmtime() } - /// Run the necessary tests to check protocol upgrades for the given - /// features. + /// Add additional protocol features to this test. /// /// Tricky. Given `[feat1, feat2, feat3]`, this will run *four* tests for /// protocol versions `[feat1 - 1, feat2 - 1, feat3 - 1, PROTOCOL_VERSION]`. @@ -141,33 +136,52 @@ impl TestBuilder { /// When using this method with `n` features, be sure to pass `n + 1` /// expectations to the `expects` method. For nightly features, you can /// `cfg` the relevant features and expect. - pub(crate) fn protocol_features(self, protocol_features: &'static [ProtocolFeature]) -> Self { - let mut protocol_versions = Vec::new(); + pub(crate) fn protocol_features( + mut self, + protocol_features: &'static [ProtocolFeature], + ) -> Self { for feat in protocol_features { - protocol_versions.push(feat.protocol_version() - 1) + self = self.protocol_version(feat.protocol_version() - 1); } - protocol_versions.push(PROTOCOL_VERSION); + self + } - self.protocol_versions(protocol_versions) + /// Add a protocol version to test. + pub(crate) fn protocol_version(mut self, protocol_version: ProtocolVersion) -> Self { + self.protocol_versions.push(protocol_version - 1); + self } - /// Run the tests for each protocol version. - /// - /// Generally you should call `protocol_features` instead. - pub(crate) fn protocol_versions(mut self, protocol_versions: Vec) -> Self { + pub(crate) fn only_protocol_versions( + mut self, + protocol_versions: Vec, + ) -> Self { self.protocol_versions = protocol_versions; self } #[track_caller] - pub(crate) fn expect(self, want: expect_test::Expect) { - self.expects(&[want]) + pub(crate) fn expect(self, want: &expect_test::Expect) { + self.expects(std::iter::once(want)) } - #[track_caller] - pub(crate) fn expects(self, wants: &[expect_test::Expect]) { + pub(crate) fn configs(&self) -> impl Iterator> { let runtime_config_store = RuntimeConfigStore::new(None); + self.protocol_versions + .clone() + .into_iter() + .map(move |pv| Arc::clone(runtime_config_store.get_config(pv))) + } + #[track_caller] + pub(crate) fn expects<'a, I>(mut self, wants: I) + where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, + { + self.protocol_versions.sort(); + let runtime_config_store = RuntimeConfigStore::new(None); + let wants = wants.into_iter(); assert_eq!( wants.len(), self.protocol_versions.len(), @@ -176,7 +190,7 @@ impl TestBuilder { wants.len(), ); - for (want, &protocol_version) in wants.iter().zip(&self.protocol_versions) { + for (want, &protocol_version) in wants.zip(&self.protocol_versions) { let mut results = vec![]; for vm_kind in [VMKind::NearVm, VMKind::Wasmer2, VMKind::Wasmer0, VMKind::Wasmtime] { if self.skip.contains(&vm_kind) { @@ -188,7 +202,7 @@ impl TestBuilder { // NearVM includes a different contract preparation algorithm, that is not supported on old protocol versions if vm_kind == VMKind::NearVm && runtime_config.wasm_config.limit_config.contract_prepare_version - != near_primitives_core::config::ContractPrepareVersion::V2 + != near_vm_runner::logic::ContractPrepareVersion::V2 { continue; } @@ -201,6 +215,7 @@ impl TestBuilder { let promise_results = vec![]; let runtime = vm_kind.runtime(config).expect("runtime has not been compiled"); + println!("Running {:?} for protocol version {}", vm_kind, protocol_version); let outcome = runtime .run( &self.code, @@ -254,9 +269,9 @@ fn fmt_outcome_without_abort( out: &mut dyn std::fmt::Write, ) -> std::fmt::Result { let return_data_str = match &outcome.return_data { - crate::logic::ReturnData::None => "None".to_string(), - crate::logic::ReturnData::ReceiptIndex(_) => "Receipt".to_string(), - crate::logic::ReturnData::Value(v) => format!("Value [{} bytes]", v.len()), + near_vm_runner::logic::ReturnData::None => "None".to_string(), + near_vm_runner::logic::ReturnData::ReceiptIndex(_) => "Receipt".to_string(), + near_vm_runner::logic::ReturnData::Value(v) => format!("Value [{} bytes]", v.len()), }; write!( out, diff --git a/runtime/near-vm-runner/src/tests/ts_contract.rs b/runtime/near-vm-runner/src/tests/ts_contract.rs index be723be3648..f998895eb02 100644 --- a/runtime/near-vm-runner/src/tests/ts_contract.rs +++ b/runtime/near-vm-runner/src/tests/ts_contract.rs @@ -1,8 +1,8 @@ use crate::logic::errors::{FunctionCallError, HostError}; use crate::logic::mocks::mock_external::MockedExternal; use crate::logic::types::ReturnData; -use crate::logic::{External, StorageGetMode, VMConfig}; -use near_primitives_core::contract::ContractCode; +use crate::logic::{Config, External, StorageGetMode}; +use crate::ContractCode; use near_primitives_core::runtime::fees::RuntimeFeesConfig; use crate::tests::{create_context, with_vm_variants, LATEST_PROTOCOL_VERSION}; @@ -10,7 +10,7 @@ use crate::vm_kind::VMKind; #[test] pub fn test_ts_contract() { - let config = VMConfig::test(); + let config = Config::test(); with_vm_variants(&config, |vm_kind: VMKind| { let code = ContractCode::new(near_test_contracts::ts_contract().to_vec(), None); let mut fake_external = MockedExternal::new(); diff --git a/runtime/near-vm-runner/src/tests/wasm_validation.rs b/runtime/near-vm-runner/src/tests/wasm_validation.rs index c732b9de694..7611fc518fc 100644 --- a/runtime/near-vm-runner/src/tests/wasm_validation.rs +++ b/runtime/near-vm-runner/src/tests/wasm_validation.rs @@ -1,5 +1,5 @@ use super::test_builder::test_builder; -use crate::logic::VMConfig; +use crate::logic::Config; use crate::prepare::prepare_contract; use crate::tests::with_vm_variants; use expect_test::expect; @@ -101,7 +101,7 @@ static EXPECTED_UNSUPPORTED: &[(&str, &str)] = &[ #[test] fn ensure_fails_verification() { - let config = VMConfig::test(); + let config = Config::test(); with_vm_variants(&config, |kind| { for (feature_name, wat) in EXPECTED_UNSUPPORTED { let wasm = wat::parse_str(wat).expect("parsing test wat should succeed"); @@ -115,7 +115,7 @@ fn ensure_fails_verification() { #[test] fn ensure_fails_execution() { for (_feature_name, wat) in EXPECTED_UNSUPPORTED { - test_builder().wat(wat).opaque_error().opaque_outcome().expect(expect![[r#" + test_builder().wat(wat).opaque_error().opaque_outcome().expect(&expect![[r#" Err: ... "#]]); } diff --git a/runtime/near-vm-runner/src/wasmer2_runner.rs b/runtime/near-vm-runner/src/wasmer2_runner.rs index 1f8107dfa1b..c8f46a57874 100644 --- a/runtime/near-vm-runner/src/wasmer2_runner.rs +++ b/runtime/near-vm-runner/src/wasmer2_runner.rs @@ -7,14 +7,13 @@ use crate::logic::errors::{ use crate::logic::gas_counter::FastGasCounter; use crate::logic::types::{PromiseResult, ProtocolVersion}; use crate::logic::{ - CompiledContract, CompiledContractCache, External, MemSlice, MemoryLike, VMConfig, VMContext, + CompiledContract, CompiledContractCache, Config, External, MemSlice, MemoryLike, VMContext, VMLogic, VMOutcome, }; use crate::prepare; use crate::runner::VMResult; -use crate::{get_contract_cache_key, imports}; +use crate::{get_contract_cache_key, imports, ContractCode}; use memoffset::offset_of; -use near_primitives_core::contract::ContractCode; use near_primitives_core::runtime::fees::RuntimeFeesConfig; use std::borrow::Cow; use std::hash::Hash; @@ -231,12 +230,12 @@ pub(crate) fn wasmer2_vm_hash() -> u64 { pub(crate) type VMArtifact = Arc; pub(crate) struct Wasmer2VM { - pub(crate) config: VMConfig, + pub(crate) config: Config, pub(crate) engine: UniversalEngine, } impl Wasmer2VM { - pub(crate) fn new_for_target(config: VMConfig, target: wasmer_compiler::Target) -> Self { + pub(crate) fn new_for_target(config: Config, target: wasmer_compiler::Target) -> Self { // We only support singlepass compiler at the moment. assert_eq!(WASMER2_CONFIG.compiler, WasmerCompiler::Singlepass); let compiler = Singlepass::new(); @@ -250,7 +249,7 @@ impl Wasmer2VM { } } - pub(crate) fn new(config: VMConfig) -> Self { + pub(crate) fn new(config: Config) -> Self { use wasmer_compiler::{CpuFeature, Target, Triple}; let target_features = if cfg!(feature = "no_cpu_compatibility_checks") { let mut fs = CpuFeature::set(); @@ -580,21 +579,10 @@ impl crate::runner::VM for Wasmer2VM { // FIXME: this mostly duplicates the `run_module` method. // Note that we don't clone the actual backing memory, just increase the RC. let vmmemory = memory.vm(); - let mut logic = VMLogic::new_with_protocol_version( - ext, - context, - &self.config, - fees_config, - promise_results, - &mut memory, - current_protocol_version, - ); + let mut logic = + VMLogic::new(ext, context, &self.config, fees_config, promise_results, &mut memory); - let result = logic.before_loading_executable( - method_name, - current_protocol_version, - code.code().len(), - ); + let result = logic.before_loading_executable(method_name, code.code().len()); if let Err(e) = result { return Ok(VMOutcome::abort(logic, e)); } @@ -607,7 +595,7 @@ impl crate::runner::VM for Wasmer2VM { } }; - let result = logic.after_loading_executable(current_protocol_version, code.code().len()); + let result = logic.after_loading_executable(code.code().len()); if let Err(e) = result { return Ok(VMOutcome::abort(logic, e)); } @@ -618,11 +606,7 @@ impl crate::runner::VM for Wasmer2VM { artifact.engine(), ); if let Err(e) = get_entrypoint_index(&*artifact, method_name) { - return Ok(VMOutcome::abort_but_nop_outcome_in_old_protocol( - logic, - e, - current_protocol_version, - )); + return Ok(VMOutcome::abort_but_nop_outcome_in_old_protocol(logic, e)); } match self.run_method(&artifact, import, method_name)? { Ok(()) => Ok(VMOutcome::ok(logic)), diff --git a/runtime/near-vm-runner/src/wasmer_runner.rs b/runtime/near-vm-runner/src/wasmer_runner.rs index 2e241c80c5b..b8af3621180 100644 --- a/runtime/near-vm-runner/src/wasmer_runner.rs +++ b/runtime/near-vm-runner/src/wasmer_runner.rs @@ -1,3 +1,4 @@ +use crate::config::Config; use crate::errors::{ContractPrecompilatonResult, IntoVMError}; use crate::internal::VMKind; use crate::logic::errors::{ @@ -10,9 +11,7 @@ use crate::logic::{ use crate::memory::WasmerMemory; use crate::prepare; use crate::runner::VMResult; -use crate::{get_contract_cache_key, imports}; -use near_primitives_core::config::VMConfig; -use near_primitives_core::contract::ContractCode; +use crate::{get_contract_cache_key, imports, ContractCode}; use near_primitives_core::runtime::fees::RuntimeFeesConfig; use near_primitives_core::types::ProtocolVersion; use wasmer_runtime::{ImportObject, Module}; @@ -234,11 +233,11 @@ pub(crate) fn wasmer0_vm_hash() -> u64 { } pub(crate) struct Wasmer0VM { - config: VMConfig, + config: Config, } impl Wasmer0VM { - pub(crate) fn new(config: VMConfig) -> Self { + pub(crate) fn new(config: Config) -> Self { Self { config } } @@ -374,21 +373,10 @@ impl crate::runner::VM for Wasmer0VM { // Note that we don't clone the actual backing memory, just increase the RC. let memory_copy = memory.clone(); - let mut logic = VMLogic::new_with_protocol_version( - ext, - context, - &self.config, - fees_config, - promise_results, - &mut memory, - current_protocol_version, - ); + let mut logic = + VMLogic::new(ext, context, &self.config, fees_config, promise_results, &mut memory); - let result = logic.before_loading_executable( - method_name, - current_protocol_version, - code.code().len(), - ); + let result = logic.before_loading_executable(method_name, code.code().len()); if let Err(e) = result { return Ok(VMOutcome::abort(logic, e)); } @@ -409,7 +397,7 @@ impl crate::runner::VM for Wasmer0VM { } }; - let result = logic.after_loading_executable(current_protocol_version, code.code().len()); + let result = logic.after_loading_executable(code.code().len()); if let Err(e) = result { return Ok(VMOutcome::abort(logic, e)); } @@ -418,11 +406,7 @@ impl crate::runner::VM for Wasmer0VM { imports::wasmer::build(memory_copy, &mut logic, current_protocol_version); if let Err(e) = check_method(&module, method_name) { - return Ok(VMOutcome::abort_but_nop_outcome_in_old_protocol( - logic, - e, - current_protocol_version, - )); + return Ok(VMOutcome::abort_but_nop_outcome_in_old_protocol(logic, e)); } match run_method(&module, &import_object, method_name)? { diff --git a/runtime/near-vm-runner/src/wasmtime_runner.rs b/runtime/near-vm-runner/src/wasmtime_runner.rs index c5acdd51eb0..938485e3af0 100644 --- a/runtime/near-vm-runner/src/wasmtime_runner.rs +++ b/runtime/near-vm-runner/src/wasmtime_runner.rs @@ -5,12 +5,11 @@ use crate::logic::errors::{ VMRunnerError, WasmTrap, }; use crate::logic::types::PromiseResult; +use crate::logic::Config; use crate::logic::{ CompiledContractCache, External, MemSlice, MemoryLike, VMContext, VMLogic, VMOutcome, }; -use crate::{imports, prepare}; -use near_primitives_core::config::VMConfig; -use near_primitives_core::contract::ContractCode; +use crate::{imports, prepare, ContractCode}; use near_primitives_core::runtime::fees::RuntimeFeesConfig; use near_primitives_core::types::ProtocolVersion; use std::borrow::Cow; @@ -136,11 +135,11 @@ pub(crate) fn wasmtime_vm_hash() -> u64 { } pub(crate) struct WasmtimeVM { - config: VMConfig, + config: Config, } impl WasmtimeVM { - pub(crate) fn new(config: VMConfig) -> Self { + pub(crate) fn new(config: Config) -> Self { Self { config } } @@ -175,21 +174,10 @@ impl crate::runner::VM for WasmtimeVM { ) .unwrap(); let memory_copy = memory.0; - let mut logic = VMLogic::new_with_protocol_version( - ext, - context, - &self.config, - fees_config, - promise_results, - &mut memory, - current_protocol_version, - ); + let mut logic = + VMLogic::new(ext, context, &self.config, fees_config, promise_results, &mut memory); - let result = logic.before_loading_executable( - method_name, - current_protocol_version, - code.code().len(), - ); + let result = logic.before_loading_executable(method_name, code.code().len()); if let Err(e) = result { return Ok(VMOutcome::abort(logic, e)); } @@ -205,7 +193,7 @@ impl crate::runner::VM for WasmtimeVM { }; let mut linker = Linker::new(&engine); - let result = logic.after_loading_executable(current_protocol_version, code.code().len()); + let result = logic.after_loading_executable(code.code().len()); if let Err(e) = result { return Ok(VMOutcome::abort(logic, e)); } @@ -227,18 +215,13 @@ impl crate::runner::VM for WasmtimeVM { let err = FunctionCallError::MethodResolveError( MethodResolveError::MethodInvalidSignature, ); - return Ok(VMOutcome::abort_but_nop_outcome_in_old_protocol( - logic, - err, - current_protocol_version, - )); + return Ok(VMOutcome::abort_but_nop_outcome_in_old_protocol(logic, err)); } } _ => { return Ok(VMOutcome::abort_but_nop_outcome_in_old_protocol( logic, FunctionCallError::MethodResolveError(MethodResolveError::MethodNotFound), - current_protocol_version, )); } }, @@ -246,7 +229,6 @@ impl crate::runner::VM for WasmtimeVM { return Ok(VMOutcome::abort_but_nop_outcome_in_old_protocol( logic, FunctionCallError::MethodResolveError(MethodResolveError::MethodNotFound), - current_protocol_version, )); } } @@ -263,7 +245,6 @@ impl crate::runner::VM for WasmtimeVM { return Ok(VMOutcome::abort_but_nop_outcome_in_old_protocol( logic, FunctionCallError::MethodResolveError(MethodResolveError::MethodNotFound), - current_protocol_version, )); } }, diff --git a/runtime/near-vm/compiler-singlepass/src/codegen_x64.rs b/runtime/near-vm/compiler-singlepass/src/codegen_x64.rs index 37602fa3a97..981e6be969d 100644 --- a/runtime/near-vm/compiler-singlepass/src/codegen_x64.rs +++ b/runtime/near-vm/compiler-singlepass/src/codegen_x64.rs @@ -349,14 +349,26 @@ impl<'a> FuncGen<'a> { } fn emit_gas_const(&mut self, cost: u64) { - if let Ok(cost) = u32::try_from(cost) { - self.emit_gas(Location::Imm32(cost)); + if self.config.disable_9393_fix { + // emit_gas only supports Imm32 with an argument up-to i32::MAX, but we made *this* + // single-letter oversight at some point & the bug made its way into mainnet. Now that + // we need to maintain backwards compatibility and replayability of the old + // transactions, we end up with this wonderful and slightly horrifying monument to our + // former selves :) + if let Ok(cost) = u32::try_from(cost) { + return self.emit_gas(Location::Imm32(cost)); + } } else { - let cost_reg = self.machine.acquire_temp_gpr().unwrap(); - self.assembler.emit_mov(Size::S64, Location::Imm64(cost), Location::GPR(cost_reg)); - self.emit_gas(Location::GPR(cost_reg)); - self.machine.release_temp_gpr(cost_reg); + if let Ok(cost) = i32::try_from(cost) { + // This as `u32` cast is valid, as fallible u64->i32 conversions can’t produce a + // negative integer. + return self.emit_gas(Location::Imm32(cost as u32)); + } } + let cost_reg = self.machine.acquire_temp_gpr().unwrap(); + self.assembler.emit_mov(Size::S64, Location::Imm64(cost), Location::GPR(cost_reg)); + self.emit_gas(Location::GPR(cost_reg)); + self.machine.release_temp_gpr(cost_reg); } /// Emit a gas charge operation. The gas amount is stored in `cost_location`, which must be either an imm32 or a GPR @@ -365,10 +377,15 @@ impl<'a> FuncGen<'a> { if cost_location == Location::Imm32(0) { return; // skip, which we must do because emit_add optimizes out the add 0 which leaves CF clobbered otherwise } - assert!( - matches!(cost_location, Location::Imm32(_) | Location::GPR(_)), - "emit_gas can take only an imm32 or a gpr argument" - ); + + match cost_location { + Location::Imm32(v) if self.config.disable_9393_fix || v <= (i32::MAX as u32) => {} + Location::Imm32(v) => { + panic!("emit_gas can take only an imm32 <= 0xFFF_FFFF, got 0x{v:X}") + } + Location::GPR(_) => {} + _ => panic!("emit_gas can take only an imm32 or a gpr argument"), + } let counter_offset = offset_of!(FastGasCounter, burnt_gas) as i32; let gas_limit_offset = offset_of!(FastGasCounter, gas_limit) as i32; @@ -1678,7 +1695,7 @@ impl<'a> FuncGen<'a> { }); } - #[tracing::instrument(skip_all)] + #[tracing::instrument(target = "near_vm", level = "trace", skip_all)] pub(crate) fn new( assembler: &'a mut Assembler, module: &'a ModuleInfo, @@ -1785,7 +1802,7 @@ impl<'a> FuncGen<'a> { None } - #[tracing::instrument(skip(self))] + #[tracing::instrument(target = "near_vm", level = "trace", skip(self))] pub(crate) fn feed_operator(&mut self, op: Operator) -> Result<(), CodegenError> { assert!(self.fp_stack.len() <= self.value_stack.len()); @@ -7646,7 +7663,7 @@ impl<'a> FuncGen<'a> { Ok(()) } - #[tracing::instrument(skip_all)] + #[tracing::instrument(target = "near_vm", level = "trace", skip_all)] pub(crate) fn finalize(mut self, data: &FunctionBodyData) -> CompiledFunction { debug_assert!( self.gas_iter.next().is_none(), @@ -7759,7 +7776,7 @@ fn sort_call_movs(movs: &mut [(Location, GPR)]) { } // Standard entry trampoline. -#[tracing::instrument] +#[tracing::instrument(target = "near_vm", level = "trace")] pub(crate) fn gen_std_trampoline( sig: &FunctionType, calling_convention: CallingConvention, @@ -7852,7 +7869,7 @@ pub(crate) fn gen_std_trampoline( } /// Generates dynamic import function call trampoline for a function type. -#[tracing::instrument(skip(vmoffsets))] +#[tracing::instrument(target = "near_vm", level = "trace", skip(vmoffsets))] pub(crate) fn gen_std_dynamic_import_trampoline( vmoffsets: &VMOffsets, sig: &FunctionType, @@ -7970,7 +7987,7 @@ pub(crate) fn gen_std_dynamic_import_trampoline( } // Singlepass calls import functions through a trampoline. -#[tracing::instrument(skip(vmoffsets))] +#[tracing::instrument(target = "near_vm", level = "trace", skip(vmoffsets))] pub(crate) fn gen_import_call_trampoline( vmoffsets: &VMOffsets, index: FunctionIndex, diff --git a/runtime/near-vm/compiler-singlepass/src/compiler.rs b/runtime/near-vm/compiler-singlepass/src/compiler.rs index fbe81e9d564..5db8f193908 100644 --- a/runtime/near-vm/compiler-singlepass/src/compiler.rs +++ b/runtime/near-vm/compiler-singlepass/src/compiler.rs @@ -41,7 +41,7 @@ impl SinglepassCompiler { impl Compiler for SinglepassCompiler { /// Compile the module using Singlepass, producing a compilation result with /// associated relocations. - #[tracing::instrument(skip_all)] + #[tracing::instrument(target = "near_vm", level = "info", skip_all)] fn compile_module( &self, target: &Target, @@ -87,7 +87,7 @@ impl Compiler for SinglepassCompiler { }; let import_idxs = 0..module.import_counts.functions as usize; let import_trampolines: PrimaryMap = - tracing::info_span!("import_trampolines", n_imports = import_idxs.len()).in_scope( + tracing::debug_span!(target: "near_vm", "import_trampolines", n_imports = import_idxs.len()).in_scope( || { import_idxs .into_par_iter() @@ -111,7 +111,7 @@ impl Compiler for SinglepassCompiler { .collect::)>>() .into_par_iter() .map_init(make_assembler, |assembler, (i, input)| { - tracing::info_span!("function", i = i.index()).in_scope(|| { + tracing::debug_span!(target: "near_vm", "function", i = i.index()).in_scope(|| { let reader = near_vm_compiler::FunctionReader::new(input.module_offset, input.data); let stack_init_gas_cost = tunables @@ -154,8 +154,9 @@ impl Compiler for SinglepassCompiler { let mut operator_reader = reader.get_operators_reader()?.into_iter_with_offsets(); while generator.has_control_frames() { - let (op, pos) = tracing::info_span!("parsing-next-operator") - .in_scope(|| operator_reader.next().unwrap())?; + let (op, pos) = + tracing::debug_span!(target: "near_vm", "parsing-next-operator") + .in_scope(|| operator_reader.next().unwrap())?; generator.set_srcloc(pos as u32); generator.feed_operator(op).map_err(to_compile_error)?; } @@ -168,7 +169,7 @@ impl Compiler for SinglepassCompiler { .collect::>(); let function_call_trampolines = - tracing::info_span!("function_call_trampolines").in_scope(|| { + tracing::debug_span!(target: "near_vm", "function_call_trampolines").in_scope(|| { module .signatures .values() @@ -182,24 +183,26 @@ impl Compiler for SinglepassCompiler { .collect::>() }); - let dynamic_function_trampolines = tracing::info_span!("dynamic_function_trampolines") - .in_scope(|| { - module - .imported_function_types() - .collect::>() - .into_par_iter() - .map_init(make_assembler, |assembler, func_type| { - gen_std_dynamic_import_trampoline( - &vmoffsets, - &func_type, - calling_convention, - assembler, - ) - }) - .collect::>() - .into_iter() - .collect::>() - }); + let dynamic_function_trampolines = + tracing::debug_span!(target: "near_vm", "dynamic_function_trampolines").in_scope( + || { + module + .imported_function_types() + .collect::>() + .into_par_iter() + .map_init(make_assembler, |assembler, func_type| { + gen_std_dynamic_import_trampoline( + &vmoffsets, + &func_type, + calling_convention, + assembler, + ) + }) + .collect::>() + .into_iter() + .collect::>() + }, + ); Ok(Compilation { functions, diff --git a/runtime/near-vm/compiler-singlepass/src/config.rs b/runtime/near-vm/compiler-singlepass/src/config.rs index 41b6b6cd157..2bfff6be9f7 100644 --- a/runtime/near-vm/compiler-singlepass/src/config.rs +++ b/runtime/near-vm/compiler-singlepass/src/config.rs @@ -24,6 +24,7 @@ pub(crate) struct Intrinsic { pub struct Singlepass { pub(crate) enable_nan_canonicalization: bool, pub(crate) enable_stack_check: bool, + pub(crate) disable_9393_fix: bool, /// Compiler intrinsics. pub(crate) intrinsics: Vec, } @@ -35,6 +36,7 @@ impl Singlepass { Self { enable_nan_canonicalization: true, enable_stack_check: false, + disable_9393_fix: false, intrinsics: vec![Intrinsic { kind: IntrinsicKind::Gas, name: "gas".to_string(), @@ -59,6 +61,10 @@ impl Singlepass { self.enable_nan_canonicalization = true; } + pub fn set_9393_fix(&mut self, enable: bool) { + self.disable_9393_fix = !enable; + } + pub fn canonicalize_nans(&mut self, enable: bool) -> &mut Self { self.enable_nan_canonicalization = enable; self diff --git a/runtime/near-vm/compiler-singlepass/src/emitter_x64.rs b/runtime/near-vm/compiler-singlepass/src/emitter_x64.rs index c3def7a1c76..630585f68c3 100644 --- a/runtime/near-vm/compiler-singlepass/src/emitter_x64.rs +++ b/runtime/near-vm/compiler-singlepass/src/emitter_x64.rs @@ -62,7 +62,6 @@ pub(crate) enum Size { } #[derive(Copy, Clone, Debug, Eq, PartialEq)] -#[allow(dead_code)] pub(crate) enum XMMOrMemory { XMM(XMM), Memory(GPR, i32), diff --git a/runtime/near-vm/compiler/src/translator/environ.rs b/runtime/near-vm/compiler/src/translator/environ.rs index e6125c8fcc0..e10fb409429 100644 --- a/runtime/near-vm/compiler/src/translator/environ.rs +++ b/runtime/near-vm/compiler/src/translator/environ.rs @@ -59,7 +59,7 @@ impl<'data> ModuleEnvironment<'data> { /// Translate a wasm module using this environment. This consumes the /// `ModuleEnvironment` and produces a `ModuleInfoTranslation`. - #[tracing::instrument(skip_all)] + #[tracing::instrument(target = "near_vm", level = "trace", skip_all)] pub fn translate(mut self, data: &'data [u8]) -> WasmResult> { assert!(self.module_translation_state.is_none()); let module_translation_state = translate_module(data, &mut self)?; diff --git a/runtime/near-vm/compiler/src/translator/module.rs b/runtime/near-vm/compiler/src/translator/module.rs index 84ba7968385..1bf29118f84 100644 --- a/runtime/near-vm/compiler/src/translator/module.rs +++ b/runtime/near-vm/compiler/src/translator/module.rs @@ -15,7 +15,7 @@ use wasmparser::{NameSectionReader, Parser, Payload}; /// Translate a sequence of bytes forming a valid Wasm binary into a /// parsed ModuleInfo `ModuleTranslationState`. -#[tracing::instrument(skip_all)] +#[tracing::instrument(target = "near_vm", level = "trace", skip_all)] pub fn translate_module<'data>( data: &'data [u8], environ: &mut ModuleEnvironment<'data>, diff --git a/runtime/near-vm/compiler/src/translator/state.rs b/runtime/near-vm/compiler/src/translator/state.rs index 91bdc0bd017..e9b73ae33d6 100644 --- a/runtime/near-vm/compiler/src/translator/state.rs +++ b/runtime/near-vm/compiler/src/translator/state.rs @@ -35,7 +35,7 @@ impl ModuleTranslationState { } /// Build map of imported functions names for intrinsification. - #[tracing::instrument(skip_all)] + #[tracing::instrument(target = "near_vm", level = "trace", skip_all)] pub fn build_import_map(&mut self, module: &ModuleInfo) { for key in module.imports.keys() { let value = &module.imports[key]; diff --git a/runtime/near-vm/engine/src/universal/code_memory.rs b/runtime/near-vm/engine/src/universal/code_memory.rs index f4b4c6a2407..501e37b3d2e 100644 --- a/runtime/near-vm/engine/src/universal/code_memory.rs +++ b/runtime/near-vm/engine/src/universal/code_memory.rs @@ -228,6 +228,7 @@ impl Drop for CodeMemory { unsafe { if let Err(e) = mm::munmap(self.map.cast(), self.size) { tracing::error!( + target: "near_vm", message="could not unmap mapping", map=?self.map, size=self.size, error=%e ); diff --git a/runtime/near-vm/engine/src/universal/engine.rs b/runtime/near-vm/engine/src/universal/engine.rs index bcd0fbb5c2f..b7874fcf495 100644 --- a/runtime/near-vm/engine/src/universal/engine.rs +++ b/runtime/near-vm/engine/src/universal/engine.rs @@ -91,7 +91,7 @@ impl UniversalEngine { } /// Compile a WebAssembly binary - #[tracing::instrument(skip_all)] + #[tracing::instrument(target = "near_vm", level = "trace", skip_all)] pub fn compile_universal( &self, binary: &[u8], @@ -181,7 +181,7 @@ impl UniversalEngine { } /// Load a [`UniversalExecutable`](crate::UniversalExecutable) with this engine. - #[tracing::instrument(skip_all)] + #[tracing::instrument(target = "near_vm", level = "trace", skip_all)] pub fn load_universal_executable( &self, executable: &UniversalExecutable, @@ -470,7 +470,7 @@ impl UniversalEngine { } /// Validates a WebAssembly module - #[tracing::instrument(skip_all)] + #[tracing::instrument(target = "near_vm", level = "trace", skip_all)] pub fn validate(&self, binary: &[u8]) -> Result<(), CompileError> { self.inner().validate(binary) } diff --git a/runtime/near-vm/engine/src/universal/link.rs b/runtime/near-vm/engine/src/universal/link.rs index 30ba0df9e8a..064b54db281 100644 --- a/runtime/near-vm/engine/src/universal/link.rs +++ b/runtime/near-vm/engine/src/universal/link.rs @@ -165,7 +165,7 @@ fn apply_relocation( /// Links a module, patching the allocated functions with the /// required relocations and jump tables. -#[tracing::instrument(skip_all)] +#[tracing::instrument(target = "near_vm", level = "trace", skip_all)] pub fn link_module( allocated_functions: &PrimaryMap, jt_offsets: impl Fn(LocalFunctionIndex, JumpTable) -> near_vm_compiler::CodeOffset, diff --git a/runtime/near-vm/test-api/src/sys/externals/function.rs b/runtime/near-vm/test-api/src/sys/externals/function.rs index 2b5b6f3c222..95eeb9d6b6e 100644 --- a/runtime/near-vm/test-api/src/sys/externals/function.rs +++ b/runtime/near-vm/test-api/src/sys/externals/function.rs @@ -126,7 +126,7 @@ where Box::into_raw(Box::new(env_ref.clone())) as _ }; let host_env_drop_fn = |ptr: *mut c_void| { - unsafe { Box::from_raw(ptr.cast::()) }; + drop(unsafe { Box::from_raw(ptr.cast::()) }); }; let env = Box::into_raw(Box::new(env)) as _; diff --git a/runtime/near-vm/test-api/src/sys/instance.rs b/runtime/near-vm/test-api/src/sys/instance.rs index aad589112db..1df6969d9d1 100644 --- a/runtime/near-vm/test-api/src/sys/instance.rs +++ b/runtime/near-vm/test-api/src/sys/instance.rs @@ -102,7 +102,7 @@ impl Instance { /// Those are, as defined by the spec: /// * Link errors that happen when plugging the imports into the instance /// * Runtime errors that happen when running the module `start` function. - #[tracing::instrument(skip_all)] + #[tracing::instrument(target = "near_vm", level = "trace", skip_all)] pub fn new_with_config( module: &Module, config: InstanceConfig, diff --git a/runtime/near-vm/test-api/src/sys/module.rs b/runtime/near-vm/test-api/src/sys/module.rs index 157e12e55f5..68ebd754074 100644 --- a/runtime/near-vm/test-api/src/sys/module.rs +++ b/runtime/near-vm/test-api/src/sys/module.rs @@ -1,3 +1,5 @@ +#![allow(clippy::arc_with_non_send_sync)] + use super::instance::InstantiationError; use super::store::Store; use near_vm_compiler::CompileError; @@ -96,7 +98,7 @@ impl Module { /// # } /// ``` #[allow(unreachable_code)] - #[tracing::instrument(skip_all)] + #[tracing::instrument(target = "near_vm", level = "trace", skip_all)] pub fn new(store: &Store, bytes: impl AsRef<[u8]>) -> Result { #[cfg(feature = "wat")] let bytes = wat::parse_bytes(bytes.as_ref()).map_err(|e| { @@ -111,7 +113,7 @@ impl Module { /// Opposed to [`Module::new`], this function is not compatible with /// the WebAssembly text format (if the "wat" feature is enabled for /// this crate). - #[tracing::instrument(skip_all)] + #[tracing::instrument(target = "near_vm", level = "trace", skip_all)] pub(crate) fn from_binary(store: &Store, binary: &[u8]) -> Result { let engine = store.engine(); engine.validate(binary)?; diff --git a/runtime/near-vm/test-api/src/sys/ptr.rs b/runtime/near-vm/test-api/src/sys/ptr.rs index 1188baf58c9..736ec2371d9 100644 --- a/runtime/near-vm/test-api/src/sys/ptr.rs +++ b/runtime/near-vm/test-api/src/sys/ptr.rs @@ -37,7 +37,7 @@ unsafe impl ValueType for WasmPtr {} impl Clone for WasmPtr { fn clone(&self) -> Self { - Self { offset: self.offset, _phantom: PhantomData } + *self } } diff --git a/runtime/near-vm/test-api/src/sys/store.rs b/runtime/near-vm/test-api/src/sys/store.rs index 97993826a54..ebbd1d171db 100644 --- a/runtime/near-vm/test-api/src/sys/store.rs +++ b/runtime/near-vm/test-api/src/sys/store.rs @@ -70,7 +70,6 @@ impl Default for Store { // We store them on a function that returns to make // sure this function doesn't emit a compile error even if // more than one compiler is enabled. - #[allow(unreachable_code)] fn get_config() -> impl CompilerConfig + 'static { cfg_if::cfg_if! { if #[cfg(feature = "default-singlepass")] { @@ -81,7 +80,7 @@ impl Default for Store { } } - #[allow(unreachable_code, unused_mut)] + #[allow(unused_mut)] fn get_engine(mut config: impl CompilerConfig + 'static) -> UniversalEngine { cfg_if::cfg_if! { if #[cfg(feature = "default-universal")] { diff --git a/runtime/near-vm/types/src/entity/mod.rs b/runtime/near-vm/types/src/entity/mod.rs index f654bdaff21..9412fa76c12 100644 --- a/runtime/near-vm/types/src/entity/mod.rs +++ b/runtime/near-vm/types/src/entity/mod.rs @@ -40,14 +40,12 @@ macro_rules! entity_impl { impl $entity { /// Create a new instance from a `u32`. - #[allow(dead_code)] pub fn from_u32(x: u32) -> Self { debug_assert!(x < $crate::lib::std::u32::MAX); $entity(x) } /// Return the underlying index value as a `u32`. - #[allow(dead_code)] pub fn as_u32(self) -> u32 { self.0 } diff --git a/runtime/near-vm/vm/src/instance/mod.rs b/runtime/near-vm/vm/src/instance/mod.rs index 57b340ed597..2e3f467a5e9 100644 --- a/runtime/near-vm/vm/src/instance/mod.rs +++ b/runtime/near-vm/vm/src/instance/mod.rs @@ -1146,7 +1146,7 @@ impl InstanceHandle { /// visible to code in `near_vm_vm`, so it's the caller's responsibility to ensure these /// functions are called with the correct type. /// - `instance_ptr` must point to a valid `near_vm_test_api::Instance`. -#[tracing::instrument(skip_all)] +#[tracing::instrument(target = "near_vm", level = "trace", skip_all)] pub unsafe fn initialize_host_envs( handle: &std::sync::Mutex, instance_ptr: *const ffi::c_void, diff --git a/runtime/near-vm/vm/src/vmoffsets.rs b/runtime/near-vm/vm/src/vmoffsets.rs index 7c7e5ba909e..5bf502793b4 100644 --- a/runtime/near-vm/vm/src/vmoffsets.rs +++ b/runtime/near-vm/vm/src/vmoffsets.rs @@ -116,7 +116,7 @@ impl VMOffsets { } /// Add imports and locals from the provided ModuleInfo. - #[tracing::instrument(skip_all)] + #[tracing::instrument(target = "near_vm", level = "trace", skip_all)] pub fn with_module_info(mut self, module: &ModuleInfo) -> Self { self.num_imported_functions = module.import_counts.functions; self.num_imported_tables = module.import_counts.tables; diff --git a/runtime/runtime-params-estimator/README.md b/runtime/runtime-params-estimator/README.md index 34d19d1a05b..4c9ff748c97 100644 --- a/runtime/runtime-params-estimator/README.md +++ b/runtime/runtime-params-estimator/README.md @@ -52,7 +52,7 @@ cargo run -p runtime-params-estimator -- replay my_trace.log cache-stats STORAGE WRITE 151412 requests for a total of 2512012 B TRIE NODES 8878276 /375708 /27383 (chunk-cache/shard-cache/DB) SHARD CACHE 93.21% hit rate, 93.21% if removing 15 too large nodes from total - CHUNK CACHE 95.66% hit rate, 99.69% if removing 375708 shard cache hits from total + ACCOUNTING CACHE 95.66% hit rate, 99.69% if removing 375708 shard cache hits from total ``` For a list of all options, run `cargo run -p runtime-params-estimator -- replay --help`. diff --git a/runtime/runtime-params-estimator/emu-cost/Dockerfile b/runtime/runtime-params-estimator/emu-cost/Dockerfile index 8fca02119f4..de6ed0d9732 100644 --- a/runtime/runtime-params-estimator/emu-cost/Dockerfile +++ b/runtime/runtime-params-estimator/emu-cost/Dockerfile @@ -1,5 +1,5 @@ # our local base image -FROM rust:1.71.0 +FROM rust:1.72.0 LABEL description="Container for builds" diff --git a/runtime/runtime-params-estimator/src/action_costs.rs b/runtime/runtime-params-estimator/src/action_costs.rs index baecb21d3e5..542e9e881ef 100644 --- a/runtime/runtime-params-estimator/src/action_costs.rs +++ b/runtime/runtime-params-estimator/src/action_costs.rs @@ -686,10 +686,10 @@ fn create_transfer_action() -> Action { } fn stake_action() -> Action { - Action::Stake(near_primitives::transaction::StakeAction { + Action::Stake(Box::new(near_primitives::transaction::StakeAction { stake: 5u128.pow(28), // some arbitrary positive number public_key: PublicKey::from_seed(KeyType::ED25519, "seed"), - }) + })) } fn delete_account_action() -> Action { @@ -705,10 +705,10 @@ fn deploy_action(size: ActionSize) -> Action { } fn add_full_access_key_action() -> Action { - Action::AddKey(near_primitives::transaction::AddKeyAction { + Action::AddKey(Box::new(near_primitives::transaction::AddKeyAction { public_key: PublicKey::from_seed(KeyType::ED25519, "full-access-key-seed"), access_key: AccessKey { nonce: 0, permission: AccessKeyPermission::FullAccess }, - }) + })) } fn add_fn_access_key_action(size: ActionSize) -> Action { @@ -716,7 +716,7 @@ fn add_fn_access_key_action(size: ActionSize) -> Action { let method_names = vec!["foo".to_owned(); size.key_methods_list() as usize / 4]; // This is charged flat, therefore it should always be max len. let receiver_id = "a".repeat(AccountId::MAX_LEN).parse().unwrap(); - Action::AddKey(near_primitives::transaction::AddKeyAction { + Action::AddKey(Box::new(near_primitives::transaction::AddKeyAction { public_key: PublicKey::from_seed(KeyType::ED25519, "seed"), access_key: AccessKey { nonce: 0, @@ -726,13 +726,13 @@ fn add_fn_access_key_action(size: ActionSize) -> Action { method_names, }), }, - }) + })) } fn delete_key_action() -> Action { - Action::DeleteKey(near_primitives::transaction::DeleteKeyAction { + Action::DeleteKey(Box::new(near_primitives::transaction::DeleteKeyAction { public_key: PublicKey::from_seed(KeyType::ED25519, "seed"), - }) + })) } fn transfer_action() -> Action { @@ -744,12 +744,12 @@ fn function_call_action(size: ActionSize) -> Action { let method_len = 4.min(total_size) as usize; let method_name: String = "noop".chars().take(method_len).collect(); let arg_len = total_size as usize - method_len; - Action::FunctionCall(near_primitives::transaction::FunctionCallAction { + Action::FunctionCall(Box::new(near_primitives::transaction::FunctionCallAction { method_name, args: vec![1u8; arg_len], gas: 3 * 10u64.pow(12), // 3 Tgas, to allow 100 copies in the same receipt deposit: 10u128.pow(24), - }) + })) } pub(crate) fn empty_delegate_action( @@ -757,7 +757,7 @@ pub(crate) fn empty_delegate_action( receiver_id: AccountId, sender_id: AccountId, ) -> Action { - use near_primitives::delegate_action::DelegateAction; + use near_primitives::action::delegate::DelegateAction; use near_primitives::signable_message::{SignableMessage, SignableMessageType}; use near_primitives::test_utils::create_user_test_signer; @@ -772,10 +772,10 @@ pub(crate) fn empty_delegate_action( }; let signature = SignableMessage::new(&delegate_action, SignableMessageType::DelegateAction).sign(&signer); - Action::Delegate(near_primitives::delegate_action::SignedDelegateAction { + Action::Delegate(Box::new(near_primitives::action::delegate::SignedDelegateAction { delegate_action, signature, - }) + })) } /// Helper enum to select how large an action should be generated. diff --git a/runtime/runtime-params-estimator/src/config.rs b/runtime/runtime-params-estimator/src/config.rs index 3f6497dd0b8..22aaded03c1 100644 --- a/runtime/runtime-params-estimator/src/config.rs +++ b/runtime/runtime-params-estimator/src/config.rs @@ -44,4 +44,6 @@ pub struct Config { pub drop_os_cache: bool, /// Use in-memory test DB, useful to avoid variance caused by DB. pub in_memory_db: bool, + /// If false, only runs a minimal check that's faster than trying to get accurate results. + pub accurate: bool, } 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 f7f79460da3..63a24338b5e 100644 --- a/runtime/runtime-params-estimator/src/costs_to_runtime_config.rs +++ b/runtime/runtime-params-estimator/src/costs_to_runtime_config.rs @@ -1,8 +1,9 @@ +use near_primitives::config::{ActionCosts, ExtCosts, ExtCostsConfig, ParameterCost}; use near_primitives::runtime::config::AccountCreationConfig; use near_primitives::runtime::config_store::RuntimeConfigStore; use near_primitives::runtime::fees::{Fee, RuntimeFeesConfig}; use near_primitives::version::PROTOCOL_VERSION; -use near_vm_runner::logic::{ActionCosts, ExtCosts, ExtCostsConfig, ParameterCost, VMConfig}; +use near_vm_runner::logic::{Config as VMConfig, StorageGetMode}; use node_runtime::config::RuntimeConfig; use anyhow::Context; @@ -34,7 +35,11 @@ pub fn costs_to_runtime_config(cost_table: &CostTable) -> anyhow::Result EstimatorContext<'c> { RuntimeConfigStore::new(None).get_config(PROTOCOL_VERSION).as_ref().clone(); // Override vm limits config to simplify block processing. - runtime_config.wasm_config.limit_config = VMLimitConfig { + runtime_config.wasm_config.limit_config = LimitConfig { max_total_log_length: u64::MAX, max_number_registers: u64::MAX, max_gas_burnt: u64::MAX, @@ -116,7 +117,7 @@ impl<'c> EstimatorContext<'c> { max_total_prepaid_gas: u64::MAX, - ..VMLimitConfig::test() + ..LimitConfig::test() }; runtime_config.account_creation_config.min_allowed_top_level_account_length = 0; @@ -178,7 +179,7 @@ impl<'c> EstimatorContext<'c> { flat_storage .add_delta(FlatStateDelta { changes: FlatStateChanges::from(random_data), - metadata: FlatStateDeltaMetadata { block }, + metadata: FlatStateDeltaMetadata { block, prev_block_with_changes: None }, }) .unwrap(); } diff --git a/runtime/runtime-params-estimator/src/function_call.rs b/runtime/runtime-params-estimator/src/function_call.rs index c576a5b44a4..96cbe76d894 100644 --- a/runtime/runtime-params-estimator/src/function_call.rs +++ b/runtime/runtime-params-estimator/src/function_call.rs @@ -1,13 +1,13 @@ use crate::config::{Config, GasMetric}; use crate::gas_cost::{GasCost, LeastSquaresTolerance}; use crate::vm_estimator::create_context; -use near_primitives::contract::ContractCode; use near_primitives::runtime::config_store::RuntimeConfigStore; use near_primitives::types::ProtocolVersion; use near_store::StoreCompiledContractCache; use near_vm_runner::internal::VMKind; use near_vm_runner::logic::mocks::mock_external::MockedExternal; use near_vm_runner::logic::CompiledContractCache; +use near_vm_runner::ContractCode; use std::fmt::Write; /// Estimates linear cost curve for a function call execution cost per byte of diff --git a/runtime/runtime-params-estimator/src/gas_cost.rs b/runtime/runtime-params-estimator/src/gas_cost.rs index 2f352c75d81..aac1062d54f 100644 --- a/runtime/runtime-params-estimator/src/gas_cost.rs +++ b/runtime/runtime-params-estimator/src/gas_cost.rs @@ -78,7 +78,9 @@ impl GasCost { /// Like [`std::cmp::Ord::min`] but operates on heterogenous types ([`GasCost`] + [`Gas`]). pub(crate) fn min_gas(mut self, gas: Gas) -> Self { - let Some(to_add) = gas.checked_sub(self.to_gas()) else { return self; }; + let Some(to_add) = gas.checked_sub(self.to_gas()) else { + return self; + }; if let Some(qemu) = &mut self.qemu { // QEMU gas is split across multiple components (instructions // and IO). When rounding up to an amount of gas, the assumption diff --git a/runtime/runtime-params-estimator/src/gas_metering.rs b/runtime/runtime-params-estimator/src/gas_metering.rs index 0101e933923..d19d0fb173b 100644 --- a/runtime/runtime-params-estimator/src/gas_metering.rs +++ b/runtime/runtime-params-estimator/src/gas_metering.rs @@ -1,13 +1,13 @@ use crate::config::Config; use crate::gas_cost::{GasCost, LeastSquaresTolerance}; use crate::vm_estimator::create_context; -use near_primitives::config::VMConfig; -use near_primitives::contract::ContractCode; use near_primitives::runtime::config_store::RuntimeConfigStore; use near_primitives::version::PROTOCOL_VERSION; use near_store::StoreCompiledContractCache; use near_vm_runner::logic::mocks::mock_external::MockedExternal; use near_vm_runner::logic::CompiledContractCache; +use near_vm_runner::logic::Config as VMConfig; +use near_vm_runner::ContractCode; use std::fmt::Write; pub(crate) fn gas_metering_cost(config: &Config) -> (GasCost, GasCost) { diff --git a/runtime/runtime-params-estimator/src/lib.rs b/runtime/runtime-params-estimator/src/lib.rs index 35e37370f33..b215a393cda 100644 --- a/runtime/runtime-params-estimator/src/lib.rs +++ b/runtime/runtime-params-estimator/src/lib.rs @@ -92,7 +92,7 @@ use gas_cost::{LeastSquaresTolerance, NonNegativeTolerance}; use gas_metering::gas_metering_cost; use near_crypto::{KeyType, SecretKey}; use near_primitives::account::{AccessKey, AccessKeyPermission, FunctionCallPermission}; -use near_primitives::contract::ContractCode; +use near_primitives::config::ExtCosts; use near_primitives::runtime::fees::RuntimeFeesConfig; use near_primitives::transaction::{ Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeleteKeyAction, @@ -101,7 +101,8 @@ use near_primitives::transaction::{ use near_primitives::types::AccountId; use near_primitives::version::PROTOCOL_VERSION; use near_vm_runner::logic::mocks::mock_external::MockedExternal; -use near_vm_runner::logic::{ExtCosts, VMConfig}; +use near_vm_runner::logic::Config as VMConfig; +use near_vm_runner::ContractCode; use near_vm_runner::MockCompiledContractCache; use serde_json::json; use utils::{ @@ -522,7 +523,7 @@ fn add_key_transaction( tb.transaction_from_actions( sender, receiver, - vec![Action::AddKey(AddKeyAction { public_key, access_key })], + vec![Action::AddKey(Box::new(AddKeyAction { public_key, access_key }))], ) } @@ -532,9 +533,9 @@ fn action_delete_key(ctx: &mut EstimatorContext) -> GasCost { let sender = tb.random_unused_account(); let receiver = sender.clone(); - let actions = vec![Action::DeleteKey(DeleteKeyAction { + let actions = vec![Action::DeleteKey(Box::new(DeleteKeyAction { public_key: SecretKey::from_seed(KeyType::ED25519, sender.as_ref()).public_key(), - })]; + }))]; tb.transaction_from_actions(sender, receiver, actions) }; transaction_cost(ctx, &mut make_transaction) @@ -551,10 +552,10 @@ fn action_stake(ctx: &mut EstimatorContext) -> GasCost { let sender = tb.random_unused_account(); let receiver = sender.clone(); - let actions = vec![Action::Stake(StakeAction { + let actions = vec![Action::Stake(Box::new(StakeAction { stake: 1, public_key: "22skMptHjFWNyuEWY22ftn2AbLPSYpmYwGJRGwpNHbTV".parse().unwrap(), - })]; + }))]; tb.transaction_from_actions(sender, receiver, actions) }; transaction_cost(ctx, &mut make_transaction) @@ -1192,7 +1193,7 @@ fn read_cached_trie_node(ctx: &mut EstimatorContext) -> GasCost { let mut testbed = ctx.testbed(); let results = (0..(warmup_iters + iters)) - .map(|_| trie::read_node_from_chunk_cache(&mut testbed)) + .map(|_| trie::read_node_from_accounting_cache(&mut testbed)) .skip(warmup_iters) .collect::>(); average_cost(results) diff --git a/runtime/runtime-params-estimator/src/main.rs b/runtime/runtime-params-estimator/src/main.rs index f4953d5bebc..83edea33026 100644 --- a/runtime/runtime-params-estimator/src/main.rs +++ b/runtime/runtime-params-estimator/src/main.rs @@ -102,6 +102,9 @@ struct CliArgs { /// Use in-memory test DB, useful to avoid variance caused by DB. #[clap(long)] pub in_memory_db: bool, + /// If false, only runs a minimal check that's faster than trying to get accurate results. + #[clap(long, default_value_t = false)] + pub accurate: bool, /// Extra configuration parameters for RocksDB specific estimations #[clap(flatten)] db_test_config: RocksDBTestConfig, @@ -301,6 +304,7 @@ fn run_estimation(cli_args: CliArgs) -> anyhow::Result> { json_output: cli_args.json_output, drop_os_cache: cli_args.drop_os_cache, in_memory_db: cli_args.in_memory_db, + accurate: cli_args.accurate, }; let cost_table = runtime_params_estimator::run(config); Ok(Some(cost_table)) @@ -527,6 +531,7 @@ mod tests { in_memory_db: false, db_test_config: clap::Parser::parse_from(std::iter::empty::()), sub_cmd: None, + accurate: true, // we run a small number of estimations, no need to take more shortcuts }; run_estimation(args).unwrap(); } diff --git a/runtime/runtime-params-estimator/src/replay.rs b/runtime/runtime-params-estimator/src/replay.rs index 3a2ab4588c8..35d6f67599c 100644 --- a/runtime/runtime-params-estimator/src/replay.rs +++ b/runtime/runtime-params-estimator/src/replay.rs @@ -277,7 +277,7 @@ GET State "'stateKey10'" size=500 } #[test] - fn test_chunk_cache_stats() { + fn test_accounting_cache_stats() { check_replay_mode(ReplayMode::ChunkCacheStats); } diff --git a/runtime/runtime-params-estimator/src/replay/cache_stats.rs b/runtime/runtime-params-estimator/src/replay/cache_stats.rs index 3acfce466bd..b2b94ee88a2 100644 --- a/runtime/runtime-params-estimator/src/replay/cache_stats.rs +++ b/runtime/runtime-params-estimator/src/replay/cache_stats.rs @@ -23,14 +23,14 @@ pub(super) struct CacheStats { /// Sum of all storage writes sizes. (can only be from inside guest program) total_size_write: u64, - /// Hits in the chunk cache. (can only be from inside guest program) - num_tn_chunk_cache_hit: u64, + /// Hits in the accounting cache. (can only be from inside guest program) + num_tn_accounting_cache_hit: u64, /// Hits in the shard cache, from inside guest program. num_tn_shard_cache_hit_guest: u64, /// Misses in the shard cache, from inside guest program. num_tn_shard_cache_miss_guest: u64, /// All trie node accesses that the user pays for as being fetched from DB. - /// Includes shard cache misses and hits, but no chunk cache hits. + /// Includes shard cache misses and hits, but no accounting cache hits. num_tn_db_paid: u64, /// Hits in the shard cache, requested by host. @@ -110,7 +110,7 @@ impl CacheStats { _ => {} } - self.num_tn_chunk_cache_hit += tn_mem_reads; + self.num_tn_accounting_cache_hit += tn_mem_reads; self.num_tn_shard_cache_hit_guest += tn_shard_cache_hits; self.num_tn_db_paid += tn_db_reads; self.num_tn_shard_cache_too_large += tn_shard_cache_too_large; @@ -132,7 +132,7 @@ impl CacheStats { let tn_shard_cache_too_large = dict.get("shard_cache_too_large").map(|s| s.parse().unwrap()).unwrap_or(0); - // there is no chunk cache update here, as we are not in a smart contract execution + // there is no accounting cache update here, as we are not in a smart contract execution self.num_tn_shard_cache_hit_host += tn_shard_cache_hits; self.num_tn_shard_cache_too_large += tn_shard_cache_too_large; self.num_tn_shard_cache_miss_host += tn_shard_cache_misses; @@ -163,7 +163,7 @@ impl CacheStats { out, "{:indent$}TRIE NODES (guest) {:>4} /{:>4} /{:>4} (chunk-cache/shard-cache/DB)", "", - self.num_tn_chunk_cache_hit, + self.num_tn_accounting_cache_hit, self.num_tn_shard_cache_hit_guest, self.num_tn_shard_cache_miss_guest )?; @@ -180,11 +180,12 @@ impl CacheStats { self.num_tn_shard_cache_miss_guest + self.num_tn_shard_cache_miss_host, Some((self.num_tn_shard_cache_too_large, "too large nodes")), )?; + // TODO(#9054): Rename this to ACCOUNTING CACHE. Self::print_cache_rate( out, indent, "CHUNK CACHE", - self.num_tn_chunk_cache_hit, + self.num_tn_accounting_cache_hit, self.num_tn_db_paid, None, )?; diff --git a/runtime/runtime-params-estimator/src/rocksdb.rs b/runtime/runtime-params-estimator/src/rocksdb.rs index cc8dce2a5c1..4be7064d01f 100644 --- a/runtime/runtime-params-estimator/src/rocksdb.rs +++ b/runtime/runtime-params-estimator/src/rocksdb.rs @@ -71,7 +71,7 @@ pub(crate) fn rocks_db_inserts_cost(config: &Config) -> GasCost { let db_config = &config.rocksdb_test_config; let data = input_data(db_config, INPUT_DATA_BUFFER_SIZE); let tmp_dir = tempfile::TempDir::new().expect("Failed to create directory for temp DB"); - let db = new_test_db(&tmp_dir, &data, &db_config); + let db = new_test_db(&tmp_dir, &data, &db_config, config.accurate); if db_config.debug_rocksdb { eprintln!("# {:?}", db_config); @@ -79,21 +79,24 @@ pub(crate) fn rocks_db_inserts_cost(config: &Config) -> GasCost { print_levels_info(&db); } + let setup_insertions = if config.accurate { db_config.setup_insertions } else { 1 }; + let op_count = if config.accurate { db_config.op_count } else { 1 }; + let gas_counter = GasCost::measure(config.metric); if db_config.sequential_keys { sequential_inserts( - db_config.op_count, + op_count, db_config.value_size, &data, - db_config.setup_insertions, + setup_insertions, &db, db_config.force_compaction, db_config.force_flush, ); } else { prandom_inserts( - db_config.op_count, + op_count, db_config.value_size, &data, ANOTHER_PRANDOM_SEED, @@ -124,7 +127,7 @@ pub(crate) fn rocks_db_read_cost(config: &Config) -> GasCost { let db_config = &config.rocksdb_test_config; let tmp_dir = tempfile::TempDir::new().expect("Failed to create directory for temp DB"); let data = input_data(db_config, INPUT_DATA_BUFFER_SIZE); - let db = new_test_db(&tmp_dir, &data, &db_config); + let db = new_test_db(&tmp_dir, &data, &db_config, config.accurate); if db_config.debug_rocksdb { eprintln!("# {:?}", db_config); @@ -132,9 +135,11 @@ pub(crate) fn rocks_db_read_cost(config: &Config) -> GasCost { print_levels_info(&db); } + let setup_insertions = if config.accurate { db_config.setup_insertions } else { 1 }; + let op_count = if config.accurate { db_config.op_count } else { 1 }; + let mut prng: XorShiftRng = rand::SeedableRng::seed_from_u64(SETUP_PRANDOM_SEED); - let mut keys: Vec = - iter::repeat_with(|| prng.gen()).take(db_config.setup_insertions).collect(); + let mut keys: Vec = iter::repeat_with(|| prng.gen()).take(setup_insertions).collect(); if db_config.sequential_keys { keys.sort(); } else { @@ -144,7 +149,7 @@ pub(crate) fn rocks_db_read_cost(config: &Config) -> GasCost { let gas_counter = GasCost::measure(config.metric); - for i in 0..db_config.op_count { + for i in 0..op_count { let key = keys[i as usize % keys.len()]; db.get(&key.to_string()).unwrap(); } @@ -254,6 +259,7 @@ fn new_test_db( db_dir: impl AsRef, data: &[u8], db_config: &RocksDBTestConfig, + accurate: bool, ) -> DB { let mut opts = rocksdb::Options::default(); @@ -277,9 +283,10 @@ fn new_test_db( } let db = rocksdb::DB::open(&opts, db_dir).expect("Failed to create RocksDB"); + let setup_insertions = if accurate { db_config.setup_insertions } else { 1 }; prandom_inserts( - db_config.setup_insertions, + setup_insertions, db_config.value_size, &data, SETUP_PRANDOM_SEED, diff --git a/runtime/runtime-params-estimator/src/transaction_builder.rs b/runtime/runtime-params-estimator/src/transaction_builder.rs index 8c18d5e2f7a..d19040f3936 100644 --- a/runtime/runtime-params-estimator/src/transaction_builder.rs +++ b/runtime/runtime-params-estimator/src/transaction_builder.rs @@ -72,12 +72,12 @@ impl TransactionBuilder { args: Vec, ) -> SignedTransaction { let receiver = sender.clone(); - let actions = vec![Action::FunctionCall(FunctionCallAction { + let actions = vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: method.to_string(), args, gas: 10u64.pow(18), deposit: 0, - })]; + }))]; self.transaction_from_actions(sender, receiver, actions) } diff --git a/runtime/runtime-params-estimator/src/trie.rs b/runtime/runtime-params-estimator/src/trie.rs index ca606bd96ff..c06896fdacd 100644 --- a/runtime/runtime-params-estimator/src/trie.rs +++ b/runtime/runtime-params-estimator/src/trie.rs @@ -1,10 +1,10 @@ use crate::estimator_context::{EstimatorContext, Testbed}; use crate::gas_cost::{GasCost, NonNegativeTolerance}; use crate::utils::{aggregate_per_block_measurements, overhead_per_measured_block, percentiles}; +use near_primitives::config::ExtCosts; use near_primitives::hash::hash; -use near_primitives::types::TrieCacheMode; -use near_store::{TrieCachingStorage, TrieStorage}; -use near_vm_runner::logic::ExtCosts; +use near_store::trie::accounting_cache::TrieAccountingCache; +use near_store::TrieCachingStorage; use std::sync::atomic::{AtomicUsize, Ordering}; static SINK: AtomicUsize = AtomicUsize::new(0); @@ -68,14 +68,14 @@ pub(crate) fn write_node( cost } -pub(crate) fn read_node_from_chunk_cache(testbed: &mut Testbed) -> GasCost { +pub(crate) fn read_node_from_accounting_cache(testbed: &mut Testbed) -> GasCost { let debug = testbed.config.debug; let iters = 200; let percentiles_of_interest = &[0.5, 0.9, 0.99, 0.999]; // Worst-case // - L3 CPU cache is filled with dummy data before measuring - let spoil_l3 = true; + let spoil_l3 = testbed.config.accurate; // - Completely cold cache let warmups = 0; // - Single node read, no amortization possible @@ -89,7 +89,7 @@ pub(crate) fn read_node_from_chunk_cache(testbed: &mut Testbed) -> GasCost { num_warmup_values: usize, data_spread_factor: usize, spoil_l3: bool| { - let results = read_node_from_chunk_cache_ext( + let results = read_node_from_accounting_cache_ext( testbed, iters, num_values, @@ -189,7 +189,7 @@ pub(crate) fn read_node_from_chunk_cache(testbed: &mut Testbed) -> GasCost { base_case } -fn read_node_from_chunk_cache_ext( +fn read_node_from_accounting_cache_ext( testbed: &mut Testbed, iters: usize, // How many values are read after each other. The higher the number, the @@ -248,8 +248,13 @@ fn read_node_from_chunk_cache_ext( // Create a new cache and load nodes into it as preparation. let caching_storage = testbed.trie_caching_storage(); - caching_storage.set_mode(TrieCacheMode::CachingChunk); - let _dummy_sum = read_raw_nodes_from_storage(&caching_storage, &all_value_hashes); + let mut accounting_cache = TrieAccountingCache::new(None); + accounting_cache.set_enabled(true); + let _dummy_sum = read_raw_nodes_from_storage( + &caching_storage, + &mut accounting_cache, + &all_value_hashes, + ); // Remove trie nodes from CPU caches by filling the caches with useless data. // (To measure latency from main memory, not CPU caches) @@ -261,11 +266,19 @@ fn read_node_from_chunk_cache_ext( // Read some nodes from the cache, to warm up caches again. (We only // want the trie node to come from main memory, the data structures // around that are expected to always be in cache) - let dummy_sum = read_raw_nodes_from_storage(&caching_storage, unmeasured_value_hashes); + let dummy_sum = read_raw_nodes_from_storage( + &caching_storage, + &mut accounting_cache, + unmeasured_value_hashes, + ); SINK.fetch_add(dummy_sum, Ordering::SeqCst); let start = GasCost::measure(testbed.config.metric); - let dummy_sum = read_raw_nodes_from_storage(&caching_storage, &measured_value_hashes); + let dummy_sum = read_raw_nodes_from_storage( + &caching_storage, + &mut accounting_cache, + &measured_value_hashes, + ); let cost = start.elapsed(); SINK.fetch_add(dummy_sum, Ordering::SeqCst); @@ -280,11 +293,13 @@ fn read_node_from_chunk_cache_ext( /// compiler. fn read_raw_nodes_from_storage( caching_storage: &TrieCachingStorage, + accounting_cache: &mut TrieAccountingCache, keys: &[near_primitives::hash::CryptoHash], ) -> usize { keys.iter() .map(|key| { - let bytes = caching_storage.retrieve_raw_bytes(key).unwrap(); + let bytes = + accounting_cache.retrieve_raw_bytes_with_accounting(key, caching_storage).unwrap(); near_store::estimator::decode_extension_node(&bytes).len() }) .sum() diff --git a/runtime/runtime-params-estimator/src/utils.rs b/runtime/runtime-params-estimator/src/utils.rs index 9604edba5cc..0cace3080ba 100644 --- a/runtime/runtime-params-estimator/src/utils.rs +++ b/runtime/runtime-params-estimator/src/utils.rs @@ -2,11 +2,12 @@ use crate::apply_block_cost; use crate::estimator_context::EstimatorContext; use crate::gas_cost::{GasCost, NonNegativeTolerance}; use crate::transaction_builder::TransactionBuilder; +use near_primitives::config::ExtCosts; use near_primitives::transaction::{ Action, DeployContractAction, FunctionCallAction, SignedTransaction, }; use near_vm_runner::internal::VMKind; -use near_vm_runner::logic::{ExtCosts, VMConfig}; +use near_vm_runner::logic::Config as VMConfig; use rand::distributions::Alphanumeric; use rand::Rng; use rand_xorshift::XorShiftRng; @@ -121,7 +122,12 @@ pub(crate) fn fn_cost_count( ext_cost: ExtCosts, block_latency: usize, ) -> (GasCost, u64) { - let block_size = 20; + // Block size: 20 is a good number if you want to reduce the effect of + // constant-per-block overhead and the tx takes less than 50 Tgas to + // execute. It's hard-coded because the range of values supported depends on + // each estimation. For a check-only run, a single tx per block is faster + // and good enough. + let block_size = if ctx.config.accurate { 20 } else { 1 }; let mut make_transaction = |tb: &mut TransactionBuilder| -> SignedTransaction { let sender = tb.random_unused_account(); tb.transaction_from_function_call(sender, method, Vec::new()) @@ -295,12 +301,12 @@ pub(crate) fn fn_cost_in_contract( } fn function_call_action(method_name: String) -> Action { - Action::FunctionCall(FunctionCallAction { + Action::FunctionCall(Box::new(FunctionCallAction { method_name, args: Vec::new(), gas: 10u64.pow(15), deposit: 0, - }) + })) } /// Takes a list of measurements of input blocks and returns the cost for a diff --git a/runtime/runtime-params-estimator/src/vm_estimator.rs b/runtime/runtime-params-estimator/src/vm_estimator.rs index 12d7b5c311c..6e5238138a8 100644 --- a/runtime/runtime-params-estimator/src/vm_estimator.rs +++ b/runtime/runtime-params-estimator/src/vm_estimator.rs @@ -1,7 +1,6 @@ use crate::config::GasMetric; use crate::gas_cost::{GasCost, LeastSquaresTolerance}; use crate::{utils::read_resource, REAL_CONTRACTS_SAMPLE}; -use near_primitives::contract::ContractCode; use near_primitives::hash::CryptoHash; use near_primitives::runtime::config_store::RuntimeConfigStore; use near_primitives::version::PROTOCOL_VERSION; @@ -9,6 +8,7 @@ use near_store::StoreCompiledContractCache; use near_vm_runner::internal::VMKind; use near_vm_runner::logic::VMContext; use near_vm_runner::logic::{CompiledContract, CompiledContractCache}; +use near_vm_runner::ContractCode; const CURRENT_ACCOUNT_ID: &str = "alice"; const SIGNER_ACCOUNT_ID: &str = "bob"; diff --git a/runtime/runtime/Cargo.toml b/runtime/runtime/Cargo.toml index 5d343c17dd4..c37b83ca308 100644 --- a/runtime/runtime/Cargo.toml +++ b/runtime/runtime/Cargo.toml @@ -27,6 +27,7 @@ near-chain-configs.workspace = true near-crypto.workspace = true near-o11y.workspace = true near-primitives.workspace = true +near-primitives-core.workspace = true near-store.workspace = true near-vm-runner.workspace = true @@ -36,21 +37,23 @@ protocol_feature_restrict_tla = [ ] nightly = [ "nightly_protocol", + "protocol_feature_restrict_tla", + "protocol_feature_restrict_tla", "near-chain-configs/nightly", "near-o11y/nightly", + "near-primitives-core/nightly", "near-primitives/nightly", "near-store/nightly", "near-vm-runner/nightly", - "protocol_feature_restrict_tla", ] default = [] nightly_protocol = [ "near-chain-configs/nightly_protocol", "near-o11y/nightly_protocol", + "near-primitives-core/nightly_protocol", "near-primitives/nightly_protocol", "near-store/nightly_protocol", "near-vm-runner/nightly_protocol", - "protocol_feature_restrict_tla", ] no_cpu_compatibility_checks = ["near-vm-runner/no_cpu_compatibility_checks"] diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index dadcb2a12ab..ab90d862420 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -3,14 +3,14 @@ use crate::config::{ total_prepaid_send_fees, RuntimeConfig, }; use crate::ext::{ExternalError, RuntimeExt}; +use crate::receipt_manager::ReceiptManager; use crate::{metrics, ActionResult, ApplyState}; use borsh::BorshSerialize; use near_crypto::PublicKey; use near_primitives::account::{AccessKey, AccessKeyPermission, Account}; +use near_primitives::action::delegate::{DelegateAction, SignedDelegateAction}; use near_primitives::checked_feature; use near_primitives::config::ViewConfig; -use near_primitives::contract::ContractCode; -use near_primitives::delegate_action::{DelegateAction, SignedDelegateAction}; use near_primitives::errors::{ActionError, ActionErrorKind, InvalidAccessKeyError, RuntimeError}; use near_primitives::hash::CryptoHash; use near_primitives::receipt::{ActionReceipt, Receipt, ReceiptEnum}; @@ -26,6 +26,7 @@ use near_primitives::utils::create_random_seed; use near_primitives::version::{ ProtocolFeature, ProtocolVersion, DELETE_KEY_STORAGE_USAGE_PROTOCOL_VERSION, }; +use near_primitives_core::config::ActionCosts; use near_store::{ get_access_key, get_code, remove_access_key, remove_account, set_access_key, set_code, StorageError, TrieUpdate, @@ -34,8 +35,9 @@ use near_vm_runner::logic::errors::{ CompilationError, FunctionCallError, InconsistentStateError, VMRunnerError, }; use near_vm_runner::logic::types::PromiseResult; -use near_vm_runner::logic::{ActionCosts, VMContext, VMOutcome}; +use near_vm_runner::logic::{VMContext, VMOutcome}; use near_vm_runner::precompile_contract; +use near_vm_runner::ContractCode; /// Runs given function call with given context / apply state. pub(crate) fn execute_function_call( @@ -94,7 +96,7 @@ pub(crate) fn execute_function_call( attached_deposit: function_call.deposit, prepaid_gas: function_call.gas, random_seed, - view_config, + view_config: view_config.clone(), output_data_receivers, }; @@ -117,6 +119,7 @@ pub(crate) fn execute_function_call( apply_state.current_protocol_version, apply_state.cache.as_deref(), ); + if checked_feature!("stable", ChunkNodesCache, protocol_version) { runtime_ext.set_trie_cache_mode(TrieCacheMode::CachingShard); } @@ -127,7 +130,7 @@ pub(crate) fn execute_function_call( // than leaking the exact details further up. // Note that this does not include errors caused by user code / input, those are // stored in outcome.aborted. - result.map_err(|e| match e { + let mut outcome = result.map_err(|e| match e { VMRunnerError::ExternalError(any_err) => { let err: ExternalError = any_err.downcast().expect("Downcasting AnyError should not fail"); @@ -152,7 +155,14 @@ pub(crate) fn execute_function_call( VMRunnerError::WasmUnknownError { debug_message } => { panic!("Wasmer returned unknown message: {}", debug_message) } - }) + })?; + + if !view_config.is_some() { + let unused_gas = function_call.gas.saturating_sub(outcome.used_gas); + let distributed = runtime_ext.receipt_manager.distribute_gas(unused_gas)?; + outcome.used_gas = safe_add_gas(outcome.used_gas, distributed)?; + } + Ok(outcome) } pub(crate) fn action_function_call( @@ -176,8 +186,10 @@ pub(crate) fn action_function_call( ) .into()); } + let mut receipt_manager = ReceiptManager::default(); let mut runtime_ext = RuntimeExt::new( state_update, + &mut receipt_manager, account_id, action_hash, &apply_state.epoch_id, @@ -255,7 +267,7 @@ pub(crate) fn action_function_call( result.logs.extend(outcome.logs); result.profile.merge(&outcome.profile); if execution_succeeded { - let new_receipts: Vec<_> = outcome + let new_receipts: Vec<_> = receipt_manager .action_receipts .into_iter() .map(|(receiver_id, receipt)| Receipt { @@ -684,11 +696,7 @@ pub(crate) fn apply_delegate_action( // Some contracts refund the deposit. Usually they refund the deposit to the predecessor and this is sender_id/Sender from DelegateAction. // Therefore Relayer should verify DelegateAction before submitting it because it spends the attached deposit. - let prepaid_send_fees = total_prepaid_send_fees( - &apply_state.config.fees, - &action_receipt.actions, - apply_state.current_protocol_version, - )?; + let prepaid_send_fees = total_prepaid_send_fees(&apply_state.config, &action_receipt.actions)?; let required_gas = receipt_required_gas(apply_state, &new_receipt)?; // This gas will be burnt by the receiver of the created receipt, result.gas_used = safe_add_gas(result.gas_used, required_gas)?; @@ -709,10 +717,9 @@ fn receipt_required_gas(apply_state: &ApplyState, receipt: &Receipt) -> Result { let mut required_gas = safe_add_gas( total_prepaid_exec_fees( - &apply_state.config.fees, + &apply_state.config, &action_receipt.actions, &receipt.receiver_id, - apply_state.current_protocol_version, )?, total_prepaid_gas(&action_receipt.actions)?, )?; @@ -884,7 +891,7 @@ pub(crate) fn check_account_existence( action: &Action, account: &mut Option, account_id: &AccountId, - current_protocol_version: ProtocolVersion, + config: &RuntimeConfig, is_the_only_action: bool, is_refund: bool, ) -> Result<(), ActionError> { @@ -896,9 +903,8 @@ pub(crate) fn check_account_existence( } .into()); } else { - if checked_feature!("stable", ImplicitAccountCreation, current_protocol_version) - && account_id.is_implicit() - { + // TODO: this should be `config.implicit_account_creation`. + if config.wasm_config.implicit_account_creation && account_id.is_implicit() { // If the account doesn't exist and it's 64-length hex account ID, then you // should only be able to create it using single transfer action. // Because you should not be able to add another access key to the account in @@ -917,11 +923,8 @@ pub(crate) fn check_account_existence( } Action::Transfer(_) => { if account.is_none() { - return if checked_feature!( - "stable", - ImplicitAccountCreation, - current_protocol_version - ) && is_the_only_action + return if config.wasm_config.implicit_account_creation + && is_the_only_action && account_id.is_implicit() && !is_refund { @@ -971,7 +974,7 @@ mod tests { use super::*; use crate::near_primitives::shard_layout::ShardUId; use near_primitives::account::FunctionCallPermission; - use near_primitives::delegate_action::NonDelegateAction; + use near_primitives::action::delegate::NonDelegateAction; use near_primitives::errors::InvalidAccessKeyError; use near_primitives::hash::hash; use near_primitives::runtime::migration_data::MigrationFlags; @@ -1204,12 +1207,12 @@ mod tests { actions: vec![ non_delegate_action( Action::FunctionCall( - FunctionCallAction { + Box::new(FunctionCallAction { method_name: "ft_transfer".parse().unwrap(), args: vec![123, 34, 114, 101, 99, 101, 105, 118, 101, 114, 95, 105, 100, 34, 58, 34, 106, 97, 110, 101, 46, 116, 101, 115, 116, 46, 110, 101, 97, 114, 34, 44, 34, 97, 109, 111, 117, 110, 116, 34, 58, 34, 52, 34, 125], gas: 30000000000000, deposit: 1, - } + }) ) ) ], @@ -1226,7 +1229,7 @@ mod tests { gas_price: 1, output_data_receivers: Vec::new(), input_data_ids: Vec::new(), - actions: vec![Action::Delegate(signed_delegate_action.clone())], + actions: vec![Action::Delegate(Box::new(signed_delegate_action.clone()))], }; (action_receipt, signed_delegate_action) @@ -1407,10 +1410,10 @@ mod tests { // Sender account doesn't exist. Must fail. assert_eq!( check_account_existence( - &Action::Delegate(signed_delegate_action), + &Action::Delegate(Box::new(signed_delegate_action)), &mut None, &sender_id, - 1, + &RuntimeConfig::test(), false, false ), @@ -1603,12 +1606,12 @@ mod tests { let mut delegate_action = signed_delegate_action.delegate_action; delegate_action.actions = - vec![non_delegate_action(Action::FunctionCall(FunctionCallAction { + vec![non_delegate_action(Action::FunctionCall(Box::new(FunctionCallAction { args: Vec::new(), deposit: 0, gas: 300, method_name: "test_method".parse().unwrap(), - }))]; + })))]; let result = test_delegate_action_key_permissions(&access_key, &delegate_action); assert!(result.result.is_ok(), "Result error {:?}", result.result); } @@ -1654,18 +1657,18 @@ mod tests { let mut delegate_action = signed_delegate_action.delegate_action; delegate_action.actions = vec![ - non_delegate_action(Action::FunctionCall(FunctionCallAction { + non_delegate_action(Action::FunctionCall(Box::new(FunctionCallAction { args: Vec::new(), deposit: 0, gas: 300, method_name: "test_method".parse().unwrap(), - })), - non_delegate_action(Action::FunctionCall(FunctionCallAction { + }))), + non_delegate_action(Action::FunctionCall(Box::new(FunctionCallAction { args: Vec::new(), deposit: 0, gas: 300, method_name: "test_method".parse().unwrap(), - })), + }))), ]; let result = test_delegate_action_key_permissions(&access_key, &delegate_action); @@ -1693,12 +1696,12 @@ mod tests { let mut delegate_action = signed_delegate_action.delegate_action; delegate_action.actions = - vec![non_delegate_action(Action::FunctionCall(FunctionCallAction { + vec![non_delegate_action(Action::FunctionCall(Box::new(FunctionCallAction { args: Vec::new(), deposit: 1, gas: 300, method_name: "test_method".parse().unwrap(), - }))]; + })))]; let result = test_delegate_action_key_permissions(&access_key, &delegate_action); @@ -1725,12 +1728,12 @@ mod tests { let mut delegate_action = signed_delegate_action.delegate_action; delegate_action.actions = - vec![non_delegate_action(Action::FunctionCall(FunctionCallAction { + vec![non_delegate_action(Action::FunctionCall(Box::new(FunctionCallAction { args: Vec::new(), deposit: 0, gas: 300, method_name: "test_method".parse().unwrap(), - }))]; + })))]; let result = test_delegate_action_key_permissions(&access_key, &delegate_action); @@ -1760,12 +1763,12 @@ mod tests { let mut delegate_action = signed_delegate_action.delegate_action; delegate_action.actions = - vec![non_delegate_action(Action::FunctionCall(FunctionCallAction { + vec![non_delegate_action(Action::FunctionCall(Box::new(FunctionCallAction { args: Vec::new(), deposit: 0, gas: 300, method_name: "test_method".parse().unwrap(), - }))]; + })))]; let result = test_delegate_action_key_permissions(&access_key, &delegate_action); diff --git a/runtime/runtime/src/adapter.rs b/runtime/runtime/src/adapter.rs index be3d8f04c2c..6bee01cca86 100644 --- a/runtime/runtime/src/adapter.rs +++ b/runtime/runtime/src/adapter.rs @@ -1,13 +1,13 @@ use crate::near_primitives::shard_layout::ShardUId; use near_crypto::PublicKey; use near_primitives::account::{AccessKey, Account}; -use near_primitives::contract::ContractCode; use near_primitives::hash::CryptoHash; use near_primitives::types::{ AccountId, BlockHeight, EpochHeight, EpochId, EpochInfoProvider, MerkleHash, }; use near_primitives::version::ProtocolVersion; use near_primitives::views::ViewStateResult; +use near_vm_runner::ContractCode; /// Adapter for querying runtime. pub trait ViewRuntimeAdapter { diff --git a/runtime/runtime/src/balance_checker.rs b/runtime/runtime/src/balance_checker.rs index ce50f108871..a8da12381c0 100644 --- a/runtime/runtime/src/balance_checker.rs +++ b/runtime/runtime/src/balance_checker.rs @@ -1,21 +1,19 @@ -use crate::safe_add_balance_apply; - use crate::config::{ safe_add_balance, safe_add_gas, safe_gas_to_balance, total_deposit, total_prepaid_exec_fees, total_prepaid_gas, total_prepaid_send_fees, }; +use crate::safe_add_balance_apply; use crate::{ApplyStats, DelayedReceiptIndices, ValidatorAccountsUpdate}; use near_primitives::errors::{ BalanceMismatchError, IntegerOverflowError, RuntimeError, StorageError, }; use near_primitives::receipt::{Receipt, ReceiptEnum}; -use near_primitives::runtime::fees::RuntimeFeesConfig; +use near_primitives::runtime::config::RuntimeConfig; use near_primitives::transaction::SignedTransaction; use near_primitives::trie_key::TrieKey; use near_primitives::types::{AccountId, Balance}; -use near_primitives::version::ProtocolVersion; +use near_primitives_core::config::ActionCosts; use near_store::{get, get_account, get_postponed_receipt, TrieAccess, TrieUpdate}; -use near_vm_runner::logic::ActionCosts; use std::collections::HashSet; /// Returns delayed receipts with given range of indices. @@ -37,8 +35,7 @@ fn get_delayed_receipts( /// Calculates and returns cost of a receipt. fn receipt_cost( - transaction_costs: &RuntimeFeesConfig, - current_protocol_version: ProtocolVersion, + config: &RuntimeConfig, receipt: &Receipt, ) -> Result { Ok(match &receipt.receipt { @@ -46,22 +43,13 @@ fn receipt_cost( let mut total_cost = total_deposit(&action_receipt.actions)?; if !AccountId::is_system(&receipt.predecessor_id) { let mut total_gas = safe_add_gas( - transaction_costs.fee(ActionCosts::new_action_receipt).exec_fee(), - total_prepaid_exec_fees( - transaction_costs, - &action_receipt.actions, - &receipt.receiver_id, - current_protocol_version, - )?, + config.fees.fee(ActionCosts::new_action_receipt).exec_fee(), + total_prepaid_exec_fees(config, &action_receipt.actions, &receipt.receiver_id)?, )?; total_gas = safe_add_gas(total_gas, total_prepaid_gas(&action_receipt.actions)?)?; total_gas = safe_add_gas( total_gas, - total_prepaid_send_fees( - transaction_costs, - &action_receipt.actions, - current_protocol_version, - )?, + total_prepaid_send_fees(config, &action_receipt.actions)?, )?; let total_gas_cost = safe_gas_to_balance(action_receipt.gas_price, total_gas)?; total_cost = safe_add_balance(total_cost, total_gas_cost)?; @@ -74,12 +62,11 @@ fn receipt_cost( /// Calculates and returns total cost of all the receipts. fn total_receipts_cost( - transaction_costs: &RuntimeFeesConfig, - current_protocol_version: ProtocolVersion, + config: &RuntimeConfig, receipts: &[Receipt], ) -> Result { receipts.iter().try_fold(0, |accumulator, receipt| { - let cost = receipt_cost(transaction_costs, current_protocol_version, receipt)?; + let cost = receipt_cost(config, receipt)?; safe_add_balance(accumulator, cost) }) } @@ -101,29 +88,27 @@ fn total_accounts_balance( /// Calculates and returns total costs of all the postponed receipts. fn total_postponed_receipts_cost( state: &dyn TrieAccess, - transaction_costs: &RuntimeFeesConfig, - current_protocol_version: ProtocolVersion, + config: &RuntimeConfig, receipt_ids: &HashSet<(AccountId, crate::CryptoHash)>, ) -> Result { receipt_ids.iter().try_fold(0, |total, item| { let (account_id, receipt_id) = item; let cost = match get_postponed_receipt(state, account_id, *receipt_id)? { None => return Ok(total), - Some(receipt) => receipt_cost(transaction_costs, current_protocol_version, &receipt)?, + Some(receipt) => receipt_cost(config, &receipt)?, }; safe_add_balance(total, cost).map_err(|_| RuntimeError::UnexpectedIntegerOverflow) }) } pub(crate) fn check_balance( - transaction_costs: &RuntimeFeesConfig, + config: &RuntimeConfig, final_state: &TrieUpdate, validator_accounts_update: &Option, incoming_receipts: &[Receipt], transactions: &[SignedTransaction], outgoing_receipts: &[Receipt], stats: &ApplyStats, - current_protocol_version: ProtocolVersion, ) -> Result<(), RuntimeError> { let initial_state = final_state.trie(); @@ -173,7 +158,7 @@ pub(crate) fn check_balance( let final_accounts_balance = total_accounts_balance(final_state, &all_accounts_ids)?; // Receipts let receipts_cost = |receipts: &[Receipt]| -> Result { - total_receipts_cost(transaction_costs, current_protocol_version, receipts) + total_receipts_cost(config, receipts) }; let incoming_receipts_balance = receipts_cost(incoming_receipts)?; let outgoing_receipts_balance = receipts_cost(outgoing_receipts)?; @@ -208,18 +193,10 @@ pub(crate) fn check_balance( }) .collect::, StorageError>>()?; - let initial_postponed_receipts_balance = total_postponed_receipts_cost( - initial_state, - transaction_costs, - current_protocol_version, - &all_potential_postponed_receipt_ids, - )?; - let final_postponed_receipts_balance = total_postponed_receipts_cost( - final_state, - transaction_costs, - current_protocol_version, - &all_potential_postponed_receipt_ids, - )?; + let initial_postponed_receipts_balance = + total_postponed_receipts_cost(initial_state, config, &all_potential_postponed_receipt_ids)?; + let final_postponed_receipts_balance = + total_postponed_receipts_cost(final_state, config, &all_potential_postponed_receipt_ids)?; // Sum it up let initial_balance = safe_add_balance_apply!( @@ -268,7 +245,6 @@ mod tests { use near_crypto::{InMemorySigner, KeyType}; use near_primitives::hash::{hash, CryptoHash}; use near_primitives::receipt::ActionReceipt; - use near_primitives::runtime::fees::RuntimeFeesConfig; use near_primitives::test_utils::account_new; use near_primitives::transaction::{Action, TransferAction}; use near_primitives::types::{MerkleHash, StateChangeCause}; @@ -278,7 +254,6 @@ mod tests { use crate::near_primitives::shard_layout::ShardUId; use assert_matches::assert_matches; - use near_primitives::version::PROTOCOL_VERSION; /// Initial balance used in tests. pub const TESTING_INIT_BALANCE: Balance = 1_000_000_000 * NEAR_BASE; @@ -291,16 +266,14 @@ mod tests { let tries = create_tries(); let root = MerkleHash::default(); let final_state = tries.new_trie_update(ShardUId::single_shard(), root); - let transaction_costs = RuntimeFeesConfig::test(); check_balance( - &transaction_costs, + &RuntimeConfig::test(), &final_state, &None, &[], &[], &[], &ApplyStats::default(), - PROTOCOL_VERSION, ) .unwrap(); } @@ -310,16 +283,14 @@ mod tests { let tries = create_tries(); let root = MerkleHash::default(); let final_state = tries.new_trie_update(ShardUId::single_shard(), root); - let transaction_costs = RuntimeFeesConfig::test(); let err = check_balance( - &transaction_costs, + &RuntimeConfig::test(), &final_state, &None, &[Receipt::new_balance_refund(&alice_account(), 1000)], &[], &[], &ApplyStats::default(), - PROTOCOL_VERSION, ) .unwrap_err(); assert_matches!(err, RuntimeError::BalanceMismatchError(_)); @@ -371,16 +342,14 @@ mod tests { }, ); - let transaction_costs = RuntimeFeesConfig::test(); check_balance( - &transaction_costs, + &RuntimeConfig::test(), &final_state, &None, &[Receipt::new_balance_refund(&account_id, refund_balance)], &[], &[], &ApplyStats::default(), - PROTOCOL_VERSION, ) .unwrap(); } @@ -392,13 +361,14 @@ mod tests { let initial_balance = TESTING_INIT_BALANCE / 2; let deposit = 500_000_000; let gas_price = 100; - let cfg = RuntimeFeesConfig::test(); - let exec_gas = cfg.fee(ActionCosts::new_action_receipt).exec_fee() - + cfg.fee(ActionCosts::transfer).exec_fee(); - let send_gas = cfg.fee(ActionCosts::new_action_receipt).send_fee(false) - + cfg.fee(ActionCosts::transfer).send_fee(false); - let contract_reward = send_gas as u128 * *cfg.burnt_gas_reward.numer() as u128 * gas_price - / (*cfg.burnt_gas_reward.denom() as u128); + let cfg = RuntimeConfig::test(); + let fees = &cfg.fees; + let exec_gas = fees.fee(ActionCosts::new_action_receipt).exec_fee() + + fees.fee(ActionCosts::transfer).exec_fee(); + let send_gas = fees.fee(ActionCosts::new_action_receipt).send_fee(false) + + fees.fee(ActionCosts::transfer).send_fee(false); + let contract_reward = send_gas as u128 * *fees.burnt_gas_reward.numer() as u128 * gas_price + / (*fees.burnt_gas_reward.denom() as u128); let total_validator_reward = send_gas as Balance * gas_price - contract_reward; let final_state = prepare_state_change( @@ -453,7 +423,6 @@ mod tests { other_burnt_amount: 0, slashed_burnt_amount: 0, }, - PROTOCOL_VERSION, ) .unwrap(); } @@ -495,17 +464,15 @@ mod tests { }), }; - let transaction_costs = RuntimeFeesConfig::test(); assert_eq!( check_balance( - &transaction_costs, + &RuntimeConfig::test(), &initial_state, &None, &[receipt], &[tx], &[], &ApplyStats::default(), - PROTOCOL_VERSION, ), Err(RuntimeError::UnexpectedIntegerOverflow) ); diff --git a/runtime/runtime/src/config.rs b/runtime/runtime/src/config.rs index 095274d4146..cf6f6cc2e99 100644 --- a/runtime/runtime/src/config.rs +++ b/runtime/runtime/src/config.rs @@ -1,21 +1,17 @@ //! Settings of the parameters of the runtime. -use near_vm_runner::logic::ActionCosts; +use near_primitives::account::AccessKeyPermission; +use near_primitives::errors::IntegerOverflowError; +use near_primitives_core::config::ActionCosts; use num_bigint::BigUint; use num_traits::cast::ToPrimitive; use num_traits::pow::Pow; - -use near_primitives::account::AccessKeyPermission; -use near_primitives::errors::IntegerOverflowError; // Just re-exporting RuntimeConfig for backwards compatibility. pub use near_primitives::num_rational::Rational32; pub use near_primitives::runtime::config::RuntimeConfig; -use near_primitives::runtime::fees::{transfer_exec_fee, transfer_send_fee, RuntimeFeesConfig}; -use near_primitives::transaction::{ - Action, AddKeyAction, DeployContractAction, FunctionCallAction, Transaction, -}; +use near_primitives::runtime::fees::{transfer_exec_fee, transfer_send_fee}; +use near_primitives::transaction::{Action, DeployContractAction, Transaction}; use near_primitives::types::{AccountId, Balance, Compute, Gas}; -use near_primitives::version::{checked_feature, ProtocolVersion}; /// Describes the cost of converting this transaction into a receipt. #[derive(Debug)] @@ -73,41 +69,39 @@ macro_rules! safe_add_balance_apply { /// Total sum of gas that needs to be burnt to send these actions. pub fn total_send_fees( - config: &RuntimeFeesConfig, + config: &RuntimeConfig, sender_is_receiver: bool, actions: &[Action], receiver_id: &AccountId, - current_protocol_version: ProtocolVersion, ) -> Result { let mut result = 0; + let fees = &config.fees; for action in actions { use Action::*; let delta = match action { - CreateAccount(_) => { - config.fee(ActionCosts::create_account).send_fee(sender_is_receiver) - } + CreateAccount(_) => fees.fee(ActionCosts::create_account).send_fee(sender_is_receiver), DeployContract(DeployContractAction { code }) => { let num_bytes = code.len() as u64; - config.fee(ActionCosts::deploy_contract_base).send_fee(sender_is_receiver) - + config.fee(ActionCosts::deploy_contract_byte).send_fee(sender_is_receiver) + fees.fee(ActionCosts::deploy_contract_base).send_fee(sender_is_receiver) + + fees.fee(ActionCosts::deploy_contract_byte).send_fee(sender_is_receiver) * num_bytes } - FunctionCall(FunctionCallAction { method_name, args, .. }) => { - let num_bytes = method_name.as_bytes().len() as u64 + args.len() as u64; - config.fee(ActionCosts::function_call_base).send_fee(sender_is_receiver) - + config.fee(ActionCosts::function_call_byte).send_fee(sender_is_receiver) + FunctionCall(function_call_action) => { + let num_bytes = function_call_action.method_name.as_bytes().len() as u64 + + function_call_action.args.len() as u64; + fees.fee(ActionCosts::function_call_base).send_fee(sender_is_receiver) + + fees.fee(ActionCosts::function_call_byte).send_fee(sender_is_receiver) * num_bytes } Transfer(_) => { // Account for implicit account creation let is_receiver_implicit = - checked_feature!("stable", ImplicitAccountCreation, current_protocol_version) - && receiver_id.is_implicit(); - transfer_send_fee(config, sender_is_receiver, is_receiver_implicit) + config.wasm_config.implicit_account_creation && receiver_id.is_implicit(); + transfer_send_fee(fees, sender_is_receiver, is_receiver_implicit) } - Stake(_) => config.fee(ActionCosts::stake).send_fee(sender_is_receiver), - AddKey(AddKeyAction { access_key, .. }) => match &access_key.permission { + Stake(_) => fees.fee(ActionCosts::stake).send_fee(sender_is_receiver), + AddKey(add_key_action) => match &add_key_action.access_key.permission { AccessKeyPermission::FunctionCall(call_perm) => { let num_bytes = call_perm .method_names @@ -115,22 +109,20 @@ pub fn total_send_fees( // Account for null-terminating characters. .map(|name| name.as_bytes().len() as u64 + 1) .sum::(); - config.fee(ActionCosts::add_function_call_key_base).send_fee(sender_is_receiver) + fees.fee(ActionCosts::add_function_call_key_base).send_fee(sender_is_receiver) + num_bytes - * config + * fees .fee(ActionCosts::add_function_call_key_byte) .send_fee(sender_is_receiver) } AccessKeyPermission::FullAccess => { - config.fee(ActionCosts::add_full_access_key).send_fee(sender_is_receiver) + fees.fee(ActionCosts::add_full_access_key).send_fee(sender_is_receiver) } }, - DeleteKey(_) => config.fee(ActionCosts::delete_key).send_fee(sender_is_receiver), - DeleteAccount(_) => { - config.fee(ActionCosts::delete_account).send_fee(sender_is_receiver) - } + DeleteKey(_) => fees.fee(ActionCosts::delete_key).send_fee(sender_is_receiver), + DeleteAccount(_) => fees.fee(ActionCosts::delete_account).send_fee(sender_is_receiver), Delegate(signed_delegate_action) => { - let delegate_cost = config.fee(ActionCosts::delegate).send_fee(sender_is_receiver); + let delegate_cost = fees.fee(ActionCosts::delegate).send_fee(sender_is_receiver); let delegate_action = &signed_delegate_action.delegate_action; delegate_cost @@ -139,7 +131,6 @@ pub fn total_send_fees( sender_is_receiver, &delegate_action.get_actions(), &delegate_action.receiver_id, - current_protocol_version, )? } }; @@ -154,12 +145,10 @@ pub fn total_send_fees( /// need to be prepaid. All other actions burn send fees directly, so calling this function /// with other actions will return 0. pub fn total_prepaid_send_fees( - config: &RuntimeFeesConfig, + config: &RuntimeConfig, actions: &[Action], - current_protocol_version: ProtocolVersion, ) -> Result { let mut result = 0; - for action in actions { use Action::*; let delta = match action { @@ -172,7 +161,6 @@ pub fn total_prepaid_send_fees( sender_is_receiver, &delegate_action.get_actions(), &delegate_action.receiver_id, - current_protocol_version, )? } _ => 0, @@ -182,35 +170,30 @@ pub fn total_prepaid_send_fees( Ok(result) } -pub fn exec_fee( - config: &RuntimeFeesConfig, - action: &Action, - receiver_id: &AccountId, - current_protocol_version: ProtocolVersion, -) -> Gas { +pub fn exec_fee(config: &RuntimeConfig, action: &Action, receiver_id: &AccountId) -> Gas { use Action::*; - + let fees = &config.fees; match action { - CreateAccount(_) => config.fee(ActionCosts::create_account).exec_fee(), + CreateAccount(_) => fees.fee(ActionCosts::create_account).exec_fee(), DeployContract(DeployContractAction { code }) => { let num_bytes = code.len() as u64; - config.fee(ActionCosts::deploy_contract_base).exec_fee() - + config.fee(ActionCosts::deploy_contract_byte).exec_fee() * num_bytes + fees.fee(ActionCosts::deploy_contract_base).exec_fee() + + fees.fee(ActionCosts::deploy_contract_byte).exec_fee() * num_bytes } - FunctionCall(FunctionCallAction { method_name, args, .. }) => { - let num_bytes = method_name.as_bytes().len() as u64 + args.len() as u64; - config.fee(ActionCosts::function_call_base).exec_fee() - + config.fee(ActionCosts::function_call_byte).exec_fee() * num_bytes + FunctionCall(function_call_action) => { + let num_bytes = function_call_action.method_name.as_bytes().len() as u64 + + function_call_action.args.len() as u64; + fees.fee(ActionCosts::function_call_base).exec_fee() + + fees.fee(ActionCosts::function_call_byte).exec_fee() * num_bytes } Transfer(_) => { // Account for implicit account creation let is_receiver_implicit = - checked_feature!("stable", ImplicitAccountCreation, current_protocol_version) - && receiver_id.is_implicit(); - transfer_exec_fee(config, is_receiver_implicit) + config.wasm_config.implicit_account_creation && receiver_id.is_implicit(); + transfer_exec_fee(fees, is_receiver_implicit) } - Stake(_) => config.fee(ActionCosts::stake).exec_fee(), - AddKey(AddKeyAction { access_key, .. }) => match &access_key.permission { + Stake(_) => fees.fee(ActionCosts::stake).exec_fee(), + AddKey(add_key_action) => match &add_key_action.access_key.permission { AccessKeyPermission::FunctionCall(call_perm) => { let num_bytes = call_perm .method_names @@ -218,29 +201,28 @@ pub fn exec_fee( // Account for null-terminating characters. .map(|name| name.as_bytes().len() as u64 + 1) .sum::(); - config.fee(ActionCosts::add_function_call_key_base).exec_fee() - + num_bytes * config.fee(ActionCosts::add_function_call_key_byte).exec_fee() + fees.fee(ActionCosts::add_function_call_key_base).exec_fee() + + num_bytes * fees.fee(ActionCosts::add_function_call_key_byte).exec_fee() } AccessKeyPermission::FullAccess => { - config.fee(ActionCosts::add_full_access_key).exec_fee() + fees.fee(ActionCosts::add_full_access_key).exec_fee() } }, - DeleteKey(_) => config.fee(ActionCosts::delete_key).exec_fee(), - DeleteAccount(_) => config.fee(ActionCosts::delete_account).exec_fee(), - Delegate(_) => config.fee(ActionCosts::delegate).exec_fee(), + DeleteKey(_) => fees.fee(ActionCosts::delete_key).exec_fee(), + DeleteAccount(_) => fees.fee(ActionCosts::delete_account).exec_fee(), + Delegate(_) => fees.fee(ActionCosts::delegate).exec_fee(), } } /// Returns transaction costs for a given transaction. pub fn tx_cost( - config: &RuntimeFeesConfig, + config: &RuntimeConfig, transaction: &Transaction, gas_price: Balance, sender_is_receiver: bool, - current_protocol_version: ProtocolVersion, ) -> Result { - let mut gas_burnt: Gas = - config.fee(ActionCosts::new_action_receipt).send_fee(sender_is_receiver); + let fees = &config.fees; + let mut gas_burnt: Gas = fees.fee(ActionCosts::new_action_receipt).send_fee(sender_is_receiver); gas_burnt = safe_add_gas( gas_burnt, total_send_fees( @@ -248,17 +230,16 @@ pub fn tx_cost( sender_is_receiver, &transaction.actions, &transaction.receiver_id, - current_protocol_version, )?, )?; let prepaid_gas = safe_add_gas( total_prepaid_gas(&transaction.actions)?, - total_prepaid_send_fees(config, &transaction.actions, current_protocol_version)?, + total_prepaid_send_fees(config, &transaction.actions)?, )?; // If signer is equals to receiver the receipt will be processed at the same block as this // transaction. Otherwise it will processed in the next block and the gas might be inflated. let initial_receipt_hop = if transaction.signer_id == transaction.receiver_id { 0 } else { 1 }; - let minimum_new_receipt_gas = config.min_receipt_with_function_call_gas(); + let minimum_new_receipt_gas = fees.min_receipt_with_function_call_gas(); // In case the config is free, we don't care about the maximum depth. let receipt_gas_price = if gas_price == 0 { 0 @@ -269,21 +250,16 @@ pub fn tx_cost( .map_err(|_| IntegerOverflowError {})?; safe_gas_price_inflated( gas_price, - config.pessimistic_gas_price_inflation_ratio, + fees.pessimistic_gas_price_inflation_ratio, inflation_exponent, )? }; let mut gas_remaining = - safe_add_gas(prepaid_gas, config.fee(ActionCosts::new_action_receipt).exec_fee())?; + safe_add_gas(prepaid_gas, fees.fee(ActionCosts::new_action_receipt).exec_fee())?; gas_remaining = safe_add_gas( gas_remaining, - total_prepaid_exec_fees( - config, - &transaction.actions, - &transaction.receiver_id, - current_protocol_version, - )?, + total_prepaid_exec_fees(config, &transaction.actions, &transaction.receiver_id)?, )?; let burnt_amount = safe_gas_to_balance(gas_price, gas_burnt)?; let remaining_gas_amount = safe_gas_to_balance(receipt_gas_price, gas_remaining)?; @@ -294,12 +270,12 @@ pub fn tx_cost( /// Total sum of gas that would need to be burnt before we start executing the given actions. pub fn total_prepaid_exec_fees( - config: &RuntimeFeesConfig, + config: &RuntimeConfig, actions: &[Action], receiver_id: &AccountId, - current_protocol_version: ProtocolVersion, ) -> Result { let mut result = 0; + let fees = &config.fees; for action in actions { let mut delta; // In case of Action::Delegate it's needed to add Gas which is required for the inner actions. @@ -309,20 +285,14 @@ pub fn total_prepaid_exec_fees( config, &actions, &signed_delegate_action.delegate_action.receiver_id, - current_protocol_version, )?; delta = safe_add_gas( delta, - exec_fee( - config, - action, - &signed_delegate_action.delegate_action.receiver_id, - current_protocol_version, - ), + exec_fee(config, action, &signed_delegate_action.delegate_action.receiver_id), )?; - delta = safe_add_gas(delta, config.fee(ActionCosts::new_action_receipt).exec_fee())?; + delta = safe_add_gas(delta, fees.fee(ActionCosts::new_action_receipt).exec_fee())?; } else { - delta = exec_fee(config, action, receiver_id, current_protocol_version); + delta = exec_fee(config, action, receiver_id); } result = safe_add_gas(result, delta)?; diff --git a/runtime/runtime/src/ext.rs b/runtime/runtime/src/ext.rs index 242d506f5bb..3790c3bd557 100644 --- a/runtime/runtime/src/ext.rs +++ b/runtime/runtime/src/ext.rs @@ -1,18 +1,21 @@ -use near_primitives::contract::ContractCode; +use crate::receipt_manager::ReceiptManager; 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, TrieCacheMode, TrieNodesCount, + AccountId, Balance, EpochId, EpochInfoProvider, Gas, TrieCacheMode, TrieNodesCount, }; use near_primitives::utils::create_data_id; use near_primitives::version::ProtocolVersion; use near_store::{get_code, KeyLookupMode, TrieUpdate, TrieUpdateValuePtr}; use near_vm_runner::logic::errors::{AnyError, VMLogicError}; +use near_vm_runner::logic::types::ReceiptIndex; use near_vm_runner::logic::{External, StorageGetMode, ValuePtr}; +use near_vm_runner::ContractCode; pub struct RuntimeExt<'a> { trie_update: &'a mut TrieUpdate, + pub(crate) receipt_manager: &'a mut ReceiptManager, account_id: &'a AccountId, action_hash: &'a CryptoHash, data_count: u64, @@ -54,6 +57,7 @@ impl<'a> ValuePtr for RuntimeExtValuePtr<'a> { impl<'a> RuntimeExt<'a> { pub fn new( trie_update: &'a mut TrieUpdate, + receipt_manager: &'a mut ReceiptManager, account_id: &'a AccountId, action_hash: &'a CryptoHash, epoch_id: &'a EpochId, @@ -64,6 +68,7 @@ impl<'a> RuntimeExt<'a> { ) -> Self { RuntimeExt { trie_update, + receipt_manager, account_id, action_hash, data_count: 0, @@ -195,4 +200,118 @@ impl<'a> External for RuntimeExt<'a> { .validator_total_stake(self.epoch_id, self.prev_block_hash) .map_err(|e| ExternalError::ValidatorError(e).into()) } + + fn create_receipt( + &mut self, + receipt_indices: Vec, + receiver_id: AccountId, + ) -> Result { + let data_ids = std::iter::from_fn(|| Some(self.generate_data_id())) + .take(receipt_indices.len()) + .collect(); + self.receipt_manager.create_receipt(data_ids, receipt_indices, receiver_id) + } + + fn append_action_create_account( + &mut self, + receipt_index: ReceiptIndex, + ) -> Result<(), VMLogicError> { + self.receipt_manager.append_action_create_account(receipt_index) + } + + fn append_action_deploy_contract( + &mut self, + receipt_index: ReceiptIndex, + code: Vec, + ) -> Result<(), VMLogicError> { + self.receipt_manager.append_action_deploy_contract(receipt_index, code) + } + + fn append_action_function_call_weight( + &mut self, + receipt_index: ReceiptIndex, + method_name: Vec, + args: Vec, + attached_deposit: Balance, + prepaid_gas: Gas, + gas_weight: near_primitives::types::GasWeight, + ) -> Result<(), VMLogicError> { + self.receipt_manager.append_action_function_call_weight( + receipt_index, + method_name, + args, + attached_deposit, + prepaid_gas, + gas_weight, + ) + } + + fn append_action_transfer( + &mut self, + receipt_index: ReceiptIndex, + deposit: Balance, + ) -> Result<(), VMLogicError> { + self.receipt_manager.append_action_transfer(receipt_index, deposit) + } + + fn append_action_stake( + &mut self, + receipt_index: ReceiptIndex, + stake: Balance, + public_key: near_crypto::PublicKey, + ) { + self.receipt_manager.append_action_stake(receipt_index, stake, public_key) + } + + fn append_action_add_key_with_full_access( + &mut self, + receipt_index: ReceiptIndex, + public_key: near_crypto::PublicKey, + nonce: near_primitives::types::Nonce, + ) { + self.receipt_manager.append_action_add_key_with_full_access( + receipt_index, + public_key, + nonce, + ) + } + + fn append_action_add_key_with_function_call( + &mut self, + receipt_index: ReceiptIndex, + public_key: near_crypto::PublicKey, + nonce: near_primitives::types::Nonce, + allowance: Option, + receiver_id: AccountId, + method_names: Vec>, + ) -> Result<(), VMLogicError> { + self.receipt_manager.append_action_add_key_with_function_call( + receipt_index, + public_key, + nonce, + allowance, + receiver_id, + method_names, + ) + } + + fn append_action_delete_key( + &mut self, + receipt_index: ReceiptIndex, + public_key: near_crypto::PublicKey, + ) { + self.receipt_manager.append_action_delete_key(receipt_index, public_key) + } + + fn append_action_delete_account( + &mut self, + receipt_index: ReceiptIndex, + beneficiary_id: AccountId, + ) -> Result<(), VMLogicError> { + self.receipt_manager.append_action_delete_account(receipt_index, beneficiary_id) + } + + fn get_receipt_receiver(&self, receipt_index: ReceiptIndex) -> &AccountId { + self.receipt_manager.get_receipt_receiver(receipt_index) + } } diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index fbd15f6059e..5aa1d73e447 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -14,7 +14,6 @@ pub use near_crypto; pub use near_primitives; use near_primitives::account::Account; use near_primitives::checked_feature; -use near_primitives::contract::ContractCode; use near_primitives::errors::{ActionError, ActionErrorKind, RuntimeError, TxExecutionError}; use near_primitives::hash::CryptoHash; use near_primitives::profile::ProfileDataV3; @@ -22,7 +21,7 @@ use near_primitives::receipt::{ ActionReceipt, DataReceipt, DelayedReceiptIndices, Receipt, ReceiptEnum, ReceivedData, }; pub use near_primitives::runtime::apply_state::ApplyState; -use near_primitives::runtime::fees::RuntimeFeesConfig; +use near_primitives::runtime::config::RuntimeConfig; use near_primitives::runtime::migration_data::{MigrationData, MigrationFlags}; use near_primitives::sandbox::state_patch::SandboxStatePatch; use near_primitives::state_record::StateRecord; @@ -39,6 +38,7 @@ use near_primitives::utils::{ create_action_hash, create_receipt_id_from_receipt, create_receipt_id_from_transaction, }; use near_primitives::version::{ProtocolFeature, ProtocolVersion}; +use near_primitives_core::config::ActionCosts; use near_store::{ get, get_account, get_postponed_receipt, get_received_data, remove_postponed_receipt, set, set_account, set_delayed_receipt, set_postponed_receipt, set_received_data, PartialStorage, @@ -46,8 +46,9 @@ use near_store::{ }; use near_store::{set_access_key, set_code}; use near_vm_runner::logic::types::PromiseResult; -use near_vm_runner::logic::{ActionCosts, ReturnData}; +use near_vm_runner::logic::ReturnData; pub use near_vm_runner::with_ext_cost_counter; +use near_vm_runner::ContractCode; use std::cmp::max; use std::collections::{HashMap, HashSet}; use std::sync::Arc; @@ -60,6 +61,7 @@ pub mod config; pub mod ext; mod metrics; mod prefetch; +pub mod receipt_manager; pub mod state_viewer; mod verifier; @@ -297,12 +299,7 @@ impl Runtime { actions: &[Action], epoch_info_provider: &dyn EpochInfoProvider, ) -> Result { - let exec_fees = exec_fee( - &apply_state.config.fees, - action, - &receipt.receiver_id, - apply_state.current_protocol_version, - ); + let exec_fees = exec_fee(&apply_state.config, action, &receipt.receiver_id); let mut result = ActionResult::default(); result.gas_used = exec_fees; result.gas_burnt = exec_fees; @@ -316,7 +313,7 @@ impl Runtime { action, account, account_id, - apply_state.current_protocol_version, + &apply_state.config, is_the_only_action, is_refund, ) { @@ -383,11 +380,7 @@ impl Runtime { } } else { // Implicit account creation - debug_assert!(checked_feature!( - "stable", - ImplicitAccountCreation, - apply_state.current_protocol_version - )); + debug_assert!(apply_state.config.wasm_config.implicit_account_creation); debug_assert!(!is_refund); action_implicit_account_creation_transfer( state_update, @@ -613,8 +606,7 @@ impl Runtime { receipt, action_receipt, &mut result, - apply_state.current_protocol_version, - &apply_state.config.fees, + &apply_state.config, )? }; stats.gas_deficit_amount = safe_add_balance(stats.gas_deficit_amount, gas_deficit_amount)?; @@ -772,26 +764,16 @@ impl Runtime { receipt: &Receipt, action_receipt: &ActionReceipt, result: &mut ActionResult, - current_protocol_version: ProtocolVersion, - transaction_costs: &RuntimeFeesConfig, + config: &RuntimeConfig, ) -> Result { let total_deposit = total_deposit(&action_receipt.actions)?; let prepaid_gas = safe_add_gas( total_prepaid_gas(&action_receipt.actions)?, - total_prepaid_send_fees( - transaction_costs, - &action_receipt.actions, - current_protocol_version, - )?, + total_prepaid_send_fees(config, &action_receipt.actions)?, )?; let prepaid_exec_gas = safe_add_gas( - total_prepaid_exec_fees( - transaction_costs, - &action_receipt.actions, - &receipt.receiver_id, - current_protocol_version, - )?, - transaction_costs.fee(ActionCosts::new_action_receipt).exec_fee(), + total_prepaid_exec_fees(config, &action_receipt.actions, &receipt.receiver_id)?, + config.fees.fee(ActionCosts::new_action_receipt).exec_fee(), )?; let deposit_refund = if result.result.is_err() { total_deposit } else { 0 }; let gas_refund = if result.result.is_err() { @@ -1463,14 +1445,13 @@ impl Runtime { } check_balance( - &apply_state.config.fees, + &apply_state.config, &state_update, validator_accounts_update, incoming_receipts, transactions, &outgoing_receipts, &stats, - apply_state.current_protocol_version, )?; state_update.commit(StateChangeCause::UpdatedDelayedReceipts); @@ -1549,9 +1530,9 @@ mod tests { }; use near_primitives::types::MerkleHash; use near_primitives::version::PROTOCOL_VERSION; + use near_primitives_core::config::{ExtCosts, ParameterCost}; use near_store::test_utils::create_tries; use near_store::{set_access_key, ShardTries, StoreCompiledContractCache}; - use near_vm_runner::logic::{ExtCosts, ParameterCost}; use testlib::runtime_utils::{alice_account, bob_account}; use super::*; @@ -2245,22 +2226,16 @@ mod tests { let gas = 2 * 10u64.pow(14); let gas_price = GAS_PRICE / 10; - let actions = vec![Action::FunctionCall(FunctionCallAction { + let actions = vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "hello".to_string(), args: b"world".to_vec(), gas, deposit: 0, - })]; + }))]; let expected_gas_burnt = safe_add_gas( apply_state.config.fees.fee(ActionCosts::new_action_receipt).exec_fee(), - total_prepaid_exec_fees( - &apply_state.config.fees, - &actions, - &alice_account(), - PROTOCOL_VERSION, - ) - .unwrap(), + total_prepaid_exec_fees(&apply_state.config, &actions, &alice_account()).unwrap(), ) .unwrap(); let receipts = vec![Receipt { @@ -2314,22 +2289,16 @@ mod tests { let gas = 1_000_000; let gas_price = GAS_PRICE / 10; - let actions = vec![Action::FunctionCall(FunctionCallAction { + let actions = vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "hello".to_string(), args: b"world".to_vec(), gas, deposit: 0, - })]; + }))]; let expected_gas_burnt = safe_add_gas( apply_state.config.fees.fee(ActionCosts::new_action_receipt).exec_fee(), - total_prepaid_exec_fees( - &apply_state.config.fees, - &actions, - &alice_account(), - PROTOCOL_VERSION, - ) - .unwrap(), + total_prepaid_exec_fees(&apply_state.config, &actions, &alice_account()).unwrap(), ) .unwrap(); let receipts = vec![Receipt { @@ -2376,11 +2345,11 @@ mod tests { let initial_account_state = get_account(&state_update, &alice_account()).unwrap().unwrap(); let actions = vec![ - Action::DeleteKey(DeleteKeyAction { public_key: signer.public_key() }), - Action::AddKey(AddKeyAction { + Action::DeleteKey(Box::new(DeleteKeyAction { public_key: signer.public_key() })), + Action::AddKey(Box::new(AddKeyAction { public_key: signer.public_key(), access_key: AccessKey::full_access(), - }), + })), ]; let receipts = vec![create_receipt_with_actions(alice_account(), signer, actions)]; @@ -2427,7 +2396,8 @@ mod tests { let root = tries.apply_all(&trie_changes, ShardUId::single_shard(), &mut store_update); store_update.commit().unwrap(); - let actions = vec![Action::DeleteKey(DeleteKeyAction { public_key: signer.public_key() })]; + let actions = + vec![Action::DeleteKey(Box::new(DeleteKeyAction { public_key: signer.public_key() }))]; let receipts = vec![create_receipt_with_actions(alice_account(), signer, actions)]; @@ -2487,7 +2457,7 @@ mod tests { tries.apply_all(&apply_result.trie_changes, ShardUId::single_shard(), &mut store_update); store_update.commit().unwrap(); - let contract_code = near_primitives::contract::ContractCode::new(wasm_code, None); + let contract_code = near_vm_runner::ContractCode::new(wasm_code, None); let vm_kind = near_vm_runner::internal::VMKind::for_protocol_version( apply_state.current_protocol_version, ); @@ -2530,23 +2500,23 @@ mod tests { let first_call_receipt = create_receipt_with_actions( alice_account(), signer.clone(), - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "ext_sha256".to_string(), args: b"first".to_vec(), gas: sha256_cost.gas, deposit: 0, - })], + }))], ); let second_call_receipt = create_receipt_with_actions( alice_account(), signer, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "ext_sha256".to_string(), args: b"second".to_vec(), gas: sha256_cost.gas, deposit: 0, - })], + }))], ); let apply_result = runtime @@ -2617,12 +2587,12 @@ mod tests { let first_call_receipt = create_receipt_with_actions( alice_account(), signer, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "ext_sha256".to_string(), args: b"first".to_vec(), gas: 1, deposit: 0, - })], + }))], ); let apply_result = runtime diff --git a/runtime/runtime/src/prefetch.rs b/runtime/runtime/src/prefetch.rs index 3e959759eca..91c7de9b2dc 100644 --- a/runtime/runtime/src/prefetch.rs +++ b/runtime/runtime/src/prefetch.rs @@ -63,7 +63,7 @@ pub(crate) struct TriePrefetcher { impl TriePrefetcher { pub(crate) fn new_if_enabled(trie: &Trie) -> Option { - if let Some(caching_storage) = trie.storage.as_caching_storage() { + if let Some(caching_storage) = trie.internal_get_storage_as_caching_storage() { if let Some(prefetch_api) = caching_storage.prefetch_api().clone() { let trie_root = *trie.get_root(); let shard_uid = prefetch_api.shard_uid; @@ -320,7 +320,7 @@ mod tests { } let root = test_populate_trie(&tries, &Trie::EMPTY_ROOT, ShardUId::single_shard(), kvs); let trie = tries.get_trie_for_shard(ShardUId::single_shard(), root); - trie.storage.as_caching_storage().unwrap().clear_cache(); + trie.internal_get_storage_as_caching_storage().unwrap().clear_cache(); let prefetcher = TriePrefetcher::new_if_enabled(&trie).expect("caching storage should have prefetcher"); diff --git a/runtime/near-vm-runner/src/logic/receipt_manager.rs b/runtime/runtime/src/receipt_manager.rs similarity index 60% rename from runtime/near-vm-runner/src/logic/receipt_manager.rs rename to runtime/runtime/src/receipt_manager.rs index ac09c47def5..b3f57e53af2 100644 --- a/runtime/near-vm-runner/src/logic/receipt_manager.rs +++ b/runtime/runtime/src/receipt_manager.rs @@ -1,18 +1,20 @@ -use super::action::{ +use near_crypto::PublicKey; +use near_primitives::action::{ Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeleteKeyAction, DeployContractAction, FunctionCallAction, StakeAction, TransferAction, }; -use super::errors::HostError; -use super::logic; -use super::types::ReceiptIndex; -use super::DataReceiver; -use super::External; -use near_crypto::PublicKey; +use near_primitives::errors::RuntimeError; +use near_primitives::receipt::DataReceiver; use near_primitives_core::account::{AccessKey, AccessKeyPermission, FunctionCallPermission}; use near_primitives_core::hash::CryptoHash; -use near_primitives_core::types::{AccountId, Gas}; -use near_primitives_core::types::{Balance, Nonce}; -use near_primitives_core::types::{GasDistribution, GasWeight}; +use near_primitives_core::types::{AccountId, Balance, Gas, GasWeight, Nonce}; +use near_vm_runner::logic::HostError; +use near_vm_runner::logic::VMLogicError; + +use crate::config::safe_add_gas; + +/// near_vm_runner::types is not public. +type ReceiptIndex = u64; type ActionReceipts = Vec<(AccountId, ReceiptMetadata)>; @@ -31,36 +33,18 @@ pub struct ReceiptMetadata { } #[derive(Default, Clone, PartialEq)] -pub(super) struct ReceiptManager { +pub struct ReceiptManager { pub(super) action_receipts: ActionReceipts, - gas_weights: Vec<(FunctionCallActionIndex, GasWeight)>, + pub(super) gas_weights: Vec<(FunctionCallActionIndex, GasWeight)>, } /// Indexes the [`ReceiptManager`]'s action receipts and actions. #[derive(Debug, Clone, Copy, PartialEq)] -struct FunctionCallActionIndex { +pub(super) struct FunctionCallActionIndex { /// Index of [`ReceiptMetadata`] in the action receipts of [`ReceiptManager`]. - receipt_index: usize, + pub(super) receipt_index: usize, /// Index of the [`Action`] within the [`ReceiptMetadata`]. - action_index: usize, -} - -fn get_fuction_call_action_mut( - action_receipts: &mut ActionReceipts, - index: FunctionCallActionIndex, -) -> &mut FunctionCallAction { - let FunctionCallActionIndex { receipt_index, action_index } = index; - if let Some(Action::FunctionCall(action)) = action_receipts - .get_mut(receipt_index) - .and_then(|(_, receipt)| receipt.actions.get_mut(action_index)) - { - action - } else { - panic!( - "Invalid function call index \ - (promise_index={receipt_index}, action_index={action_index})", - ); - } + pub(super) action_index: usize, } impl ReceiptManager { @@ -99,20 +83,18 @@ impl ReceiptManager { /// * `receiver_id` - account id of the receiver of the receipt created pub(super) fn create_receipt( &mut self, - ext: &mut dyn External, + input_data_ids: Vec, receipt_indices: Vec, receiver_id: AccountId, - ) -> logic::Result { - let mut input_data_ids = vec![]; - for receipt_index in receipt_indices { - let data_id = ext.generate_data_id(); + ) -> Result { + assert_eq!(input_data_ids.len(), receipt_indices.len()); + for (data_id, receipt_index) in input_data_ids.iter().zip(receipt_indices.into_iter()) { self.action_receipts .get_mut(receipt_index as usize) .ok_or_else(|| HostError::InvalidReceiptIndex { receipt_index })? .1 .output_data_receivers - .push(DataReceiver { data_id, receiver_id: receiver_id.clone() }); - input_data_ids.push(data_id); + .push(DataReceiver { data_id: *data_id, receiver_id: receiver_id.clone() }); } let new_receipt = @@ -134,7 +116,7 @@ impl ReceiptManager { pub(super) fn append_action_create_account( &mut self, receipt_index: ReceiptIndex, - ) -> logic::Result<()> { + ) -> Result<(), VMLogicError> { self.append_action(receipt_index, Action::CreateAccount(CreateAccountAction {})); Ok(()) } @@ -153,7 +135,7 @@ impl ReceiptManager { &mut self, receipt_index: ReceiptIndex, code: Vec, - ) -> logic::Result<()> { + ) -> Result<(), VMLogicError> { self.append_action(receipt_index, Action::DeployContract(DeployContractAction { code })); Ok(()) } @@ -186,16 +168,16 @@ impl ReceiptManager { attached_deposit: Balance, prepaid_gas: Gas, gas_weight: GasWeight, - ) -> logic::Result<()> { + ) -> Result<(), VMLogicError> { let action_index = self.append_action( receipt_index, - Action::FunctionCall(FunctionCallAction { + Action::FunctionCall(Box::new(FunctionCallAction { method_name: String::from_utf8(method_name) .map_err(|_| HostError::InvalidMethodName)?, args, gas: prepaid_gas, deposit: attached_deposit, - }), + })), ); if gas_weight.0 > 0 { @@ -222,7 +204,7 @@ impl ReceiptManager { &mut self, receipt_index: ReceiptIndex, deposit: Balance, - ) -> logic::Result<()> { + ) -> Result<(), VMLogicError> { self.append_action(receipt_index, Action::Transfer(TransferAction { deposit })); Ok(()) } @@ -244,7 +226,10 @@ impl ReceiptManager { stake: Balance, public_key: PublicKey, ) { - self.append_action(receipt_index, Action::Stake(StakeAction { stake, public_key })); + self.append_action( + receipt_index, + Action::Stake(Box::new(StakeAction { stake, public_key })), + ); } /// Attach the [`AddKeyAction`] action to an existing receipt. @@ -266,10 +251,10 @@ impl ReceiptManager { ) { self.append_action( receipt_index, - Action::AddKey(AddKeyAction { + Action::AddKey(Box::new(AddKeyAction { public_key, access_key: AccessKey { nonce, permission: AccessKeyPermission::FullAccess }, - }), + })), ); } @@ -298,10 +283,10 @@ impl ReceiptManager { allowance: Option, receiver_id: AccountId, method_names: Vec>, - ) -> logic::Result<()> { + ) -> Result<(), VMLogicError> { self.append_action( receipt_index, - Action::AddKey(AddKeyAction { + Action::AddKey(Box::new(AddKeyAction { public_key, access_key: AccessKey { nonce, @@ -317,7 +302,7 @@ impl ReceiptManager { .collect::, _>>()?, }), }, - }), + })), ); Ok(()) } @@ -337,7 +322,10 @@ impl ReceiptManager { receipt_index: ReceiptIndex, public_key: PublicKey, ) { - self.append_action(receipt_index, Action::DeleteKey(DeleteKeyAction { public_key })); + self.append_action( + receipt_index, + Action::DeleteKey(Box::new(DeleteKeyAction { public_key })), + ); } /// Attach the [`DeleteAccountAction`] action to an existing receipt @@ -354,7 +342,7 @@ impl ReceiptManager { &mut self, receipt_index: ReceiptIndex, beneficiary_id: AccountId, - ) -> logic::Result<()> { + ) -> Result<(), VMLogicError> { self.append_action( receipt_index, Action::DeleteAccount(DeleteAccountAction { beneficiary_id }), @@ -362,53 +350,149 @@ impl ReceiptManager { Ok(()) } - /// Distribute the gas among the scheduled function calls that specify a gas weight. - /// - /// Distributes the gas passed in by splitting it among weights defined in `gas_weights`. - /// This will sum all weights, retrieve the gas per weight, then update each function - /// to add the respective amount of gas. Once all gas is distributed, the remainder of - /// the gas not assigned due to precision loss is added to the last function with a weight. - /// - /// # Arguments - /// - /// * `gas` - amount of unused gas to distribute - /// - /// # Returns - /// - /// Function returns a [GasDistribution] that indicates how the gas was distributed. - pub(super) fn distribute_unused_gas(&mut self, unused_gas: Gas) -> GasDistribution { - let gas_weight_sum: u128 = - self.gas_weights.iter().map(|(_, GasWeight(weight))| *weight as u128).sum(); - - if gas_weight_sum == 0 { - return GasDistribution::NoRatios; + /// Distribute the provided `gas` between receipts managed by this `ReceiptManager` according + /// to their assigned weights. + /// + /// Returns the amount of gas distributed (either `0` or `unused_gas`.) + pub(super) fn distribute_gas(&mut self, unused_gas: Gas) -> Result { + let ReceiptManager { action_receipts, gas_weights } = self; + let gas_weight_sum: u128 = gas_weights.iter().map(|(_, gv)| u128::from(gv.0)).sum(); + if gas_weight_sum == 0 || unused_gas == 0 { + return Ok(0); } + let mut distributed = 0u64; + let mut gas_weight_iterator = gas_weights.iter().peekable(); + loop { + let Some((index, weight)) = gas_weight_iterator.next() else { break }; + let FunctionCallActionIndex { receipt_index, action_index } = *index; + let Some(Action::FunctionCall(action)) = action_receipts + .get_mut(receipt_index) + .and_then(|(_, receipt)| receipt.actions.get_mut(action_index)) + else { + panic!( + "Invalid function call index (promise_index={receipt_index}, action_index={action_index})", + ); + }; + let to_assign = (unused_gas as u128 * weight.0 as u128 / gas_weight_sum) as u64; + action.gas = safe_add_gas(action.gas, to_assign)?; + distributed = distributed + .checked_add(to_assign) + .unwrap_or_else(|| panic!("gas computation overflowed")); + if gas_weight_iterator.peek().is_none() { + let remainder = unused_gas.wrapping_sub(distributed); + distributed = distributed + .checked_add(remainder) + .unwrap_or_else(|| panic!("gas computation overflowed")); + action.gas = safe_add_gas(action.gas, remainder)?; + } + } + assert_eq!(unused_gas, distributed); + Ok(distributed) + } +} - let mut distribute_gas = |index: &FunctionCallActionIndex, assigned_gas: Gas| { - let FunctionCallAction { gas, .. } = - get_fuction_call_action_mut(&mut self.action_receipts, *index); - - // Operation cannot overflow because the amount of assigned gas is a fraction of - // the unused gas and is using floor division. - *gas += assigned_gas; - }; +#[cfg(test)] +mod tests { + use near_primitives::transaction::Action; + use near_primitives_core::types::{Gas, GasWeight}; - let mut distributed = 0; - for (action_index, GasWeight(weight)) in &self.gas_weights { - // Multiplication is done in u128 with max values of u64::MAX so this cannot overflow. - // Division result can be truncated to 64 bits because gas_weight_sum >= weight. - let assigned_gas = (unused_gas as u128 * *weight as u128 / gas_weight_sum) as u64; + #[track_caller] + fn function_call_weight_verify(function_calls: &[(Gas, u64, Gas)], after_distribute: bool) { + let mut gas_limit = 10_000_000_000u64; - distribute_gas(action_index, assigned_gas as u64); + // Schedule all function calls + let mut receipt_manager = super::ReceiptManager::default(); + for &(static_gas, gas_weight, _) in function_calls { + let index = receipt_manager + .create_receipt(vec![], vec![], "rick.test".parse().unwrap()) + .unwrap(); + gas_limit = gas_limit.saturating_sub(static_gas); + receipt_manager + .append_action_function_call_weight( + index, + vec![], + vec![], + 0, + static_gas, + GasWeight(gas_weight), + ) + .unwrap(); + } + let accessor: fn(&(Gas, u64, Gas)) -> Gas = if after_distribute { + receipt_manager.distribute_gas(gas_limit).unwrap(); + |(_, _, expected)| *expected + } else { + |(static_gas, _, _)| *static_gas + }; - distributed += assigned_gas + // Assert expected amount of gas was associated with the action + let mut function_call_gas = 0; + let mut function_calls_iter = function_calls.iter(); + for (_, receipt) in receipt_manager.action_receipts { + for action in receipt.actions { + if let Action::FunctionCall(function_call_action) = action { + let reference = function_calls_iter.next().unwrap(); + assert_eq!(function_call_action.gas, accessor(reference)); + function_call_gas += function_call_action.gas; + } + } } - // Distribute remaining gas to final action. - if let Some((last_idx, _)) = self.gas_weights.last() { - distribute_gas(last_idx, unused_gas - distributed); + if after_distribute { + // Verify that all gas was consumed (assumes at least one ratio is provided) + assert_eq!(function_call_gas, 10_000_000_000u64); } - self.gas_weights.clear(); - GasDistribution::All + } + + #[track_caller] + fn function_call_weight_check(function_calls: &[(Gas, u64, Gas)]) { + function_call_weight_verify(function_calls, false); + function_call_weight_verify(function_calls, true); + } + + #[test] + fn function_call_weight_basic_cases_test() { + // Following tests input are in the format (static gas, gas weight, expected gas) + // and the gas limit is `10_000_000_000` + + // Single function call + function_call_weight_check(&[(0, 1, 10_000_000_000)]); + + // Single function with static gas + function_call_weight_check(&[(888, 1, 10_000_000_000)]); + + // Large weight + function_call_weight_check(&[(0, 88888, 10_000_000_000)]); + + // Weight larger than gas limit + function_call_weight_check(&[(0, 11u64.pow(14), 10_000_000_000)]); + + // Split two + function_call_weight_check(&[(0, 3, 6_000_000_000), (0, 2, 4_000_000_000)]); + + // Split two with static gas + function_call_weight_check(&[(1_000_000, 3, 5_998_600_000), (3_000_000, 2, 4_001_400_000)]); + + // Many different gas weights + function_call_weight_check(&[ + (1_000_000, 3, 2_699_800_000), + (3_000_000, 2, 1_802_200_000), + (0, 1, 899_600_000), + (1_000_000_000, 0, 1_000_000_000), + (0, 4, 3_598_400_000), + ]); + + // Weight over u64 bounds + function_call_weight_check(&[(0, u64::MAX, 9_999_999_999), (0, 1000, 1)]); + + // Weight over gas limit with three function calls + function_call_weight_check(&[ + (0, 10_000_000_000, 4_999_999_999), + (0, 1, 0), + (0, 10_000_000_000, 5_000_000_001), + ]); + + // Weights with one zero and one non-zero + function_call_weight_check(&[(0, 0, 0), (0, 1, 10_000_000_000)]) } } diff --git a/runtime/runtime/src/state_viewer/mod.rs b/runtime/runtime/src/state_viewer/mod.rs index 5c50f6e935a..e47031896de 100644 --- a/runtime/runtime/src/state_viewer/mod.rs +++ b/runtime/runtime/src/state_viewer/mod.rs @@ -1,24 +1,22 @@ use crate::near_primitives::version::PROTOCOL_VERSION; +use crate::receipt_manager::ReceiptManager; use crate::{actions::execute_function_call, ext::RuntimeExt}; use near_crypto::{KeyType, PublicKey}; +use near_primitives::account::{AccessKey, Account}; +use near_primitives::borsh::BorshDeserialize; +use near_primitives::hash::CryptoHash; +use near_primitives::receipt::ActionReceipt; +use near_primitives::runtime::apply_state::ApplyState; use near_primitives::runtime::config_store::RuntimeConfigStore; -use near_primitives::{ - account::{AccessKey, Account}, - borsh::BorshDeserialize, - contract::ContractCode, - hash::CryptoHash, - receipt::ActionReceipt, - runtime::{ - apply_state::ApplyState, - migration_data::{MigrationData, MigrationFlags}, - }, - transaction::FunctionCallAction, - trie_key::trie_key_parsers, - types::{AccountId, EpochInfoProvider, Gas}, - views::{StateItem, ViewApplyState, ViewStateResult}, -}; +use near_primitives::runtime::migration_data::{MigrationData, MigrationFlags}; +use near_primitives::transaction::FunctionCallAction; +use near_primitives::trie_key::trie_key_parsers; +use near_primitives::types::{AccountId, EpochInfoProvider, Gas}; +use near_primitives::views::{StateItem, ViewApplyState, ViewStateResult}; +use near_primitives_core::config::ViewConfig; use near_store::{get_access_key, get_account, get_code, TrieUpdate}; -use near_vm_runner::logic::{ReturnData, ViewConfig}; +use near_vm_runner::logic::ReturnData; +use near_vm_runner::ContractCode; use std::{str, sync::Arc, time::Instant}; use tracing::debug; @@ -175,8 +173,10 @@ impl TrieViewer { let originator_id = contract_id; let public_key = PublicKey::empty(KeyType::ED25519); let empty_hash = CryptoHash::default(); + let mut receipt_manager = ReceiptManager::default(); let mut runtime_ext = RuntimeExt::new( &mut state_update, + &mut receipt_manager, contract_id, &empty_hash, &view_state.epoch_id, diff --git a/runtime/runtime/src/verifier.rs b/runtime/runtime/src/verifier.rs index 8cbc784a9f7..e9dacb4cae4 100644 --- a/runtime/runtime/src/verifier.rs +++ b/runtime/runtime/src/verifier.rs @@ -1,32 +1,28 @@ +use crate::config::{total_prepaid_gas, tx_cost, TransactionCost}; +use crate::near_primitives::account::Account; +use crate::VerificationResult; use near_crypto::key_conversion::is_valid_staking_key; +use near_primitives::account::AccessKeyPermission; +use near_primitives::action::delegate::SignedDelegateAction; use near_primitives::checked_feature; -use near_primitives::delegate_action::SignedDelegateAction; +use near_primitives::errors::{ + ActionsValidationError, InvalidAccessKeyError, InvalidTxError, ReceiptValidationError, + RuntimeError, +}; +use near_primitives::receipt::{ActionReceipt, DataReceipt, Receipt, ReceiptEnum}; use near_primitives::runtime::config::RuntimeConfig; use near_primitives::transaction::DeleteAccountAction; +use near_primitives::transaction::{ + Action, AddKeyAction, DeployContractAction, FunctionCallAction, SignedTransaction, StakeAction, +}; +use near_primitives::types::{AccountId, Balance}; use near_primitives::types::{BlockHeight, StorageUsage}; use near_primitives::version::ProtocolFeature; -use near_primitives::{ - account::AccessKeyPermission, - config::VMLimitConfig, - errors::{ - ActionsValidationError, InvalidAccessKeyError, InvalidTxError, ReceiptValidationError, - RuntimeError, - }, - receipt::{ActionReceipt, DataReceipt, Receipt, ReceiptEnum}, - transaction::{ - Action, AddKeyAction, DeployContractAction, FunctionCallAction, SignedTransaction, - StakeAction, - }, - types::{AccountId, Balance}, - version::ProtocolVersion, -}; +use near_primitives::version::ProtocolVersion; use near_store::{ get_access_key, get_account, set_access_key, set_account, StorageError, TrieUpdate, }; - -use crate::config::{total_prepaid_gas, tx_cost, TransactionCost}; -use crate::near_primitives::account::Account; -use crate::VerificationResult; +use near_vm_runner::logic::LimitConfig; pub const ZERO_BALANCE_ACCOUNT_STORAGE_LIMIT: StorageUsage = 770; @@ -123,7 +119,7 @@ pub fn validate_transaction( let sender_is_receiver = &transaction.receiver_id == signer_id; - tx_cost(&config.fees, transaction, gas_price, sender_is_receiver, current_protocol_version) + tx_cost(&config, transaction, gas_price, sender_is_receiver) .map_err(|_| InvalidTxError::CostOverflow.into()) } @@ -135,7 +131,7 @@ pub fn verify_and_charge_transaction( gas_price: Balance, signed_transaction: &SignedTransaction, verify_signature: bool, - #[allow(unused)] block_height: Option, + block_height: Option, current_protocol_version: ProtocolVersion, ) -> Result { let TransactionCost { gas_burnt, gas_remaining, receipt_gas_price, total_cost, burnt_amount } = @@ -280,7 +276,7 @@ pub fn verify_and_charge_transaction( /// Validates a given receipt. Checks validity of the Action or Data receipt. pub(crate) fn validate_receipt( - limit_config: &VMLimitConfig, + limit_config: &LimitConfig, receipt: &Receipt, current_protocol_version: ProtocolVersion, ) -> Result<(), ReceiptValidationError> { @@ -306,7 +302,7 @@ pub(crate) fn validate_receipt( /// Validates given ActionReceipt. Checks validity of the number of input data dependencies and all actions. fn validate_action_receipt( - limit_config: &VMLimitConfig, + limit_config: &LimitConfig, receipt: &ActionReceipt, current_protocol_version: ProtocolVersion, ) -> Result<(), ReceiptValidationError> { @@ -322,7 +318,7 @@ fn validate_action_receipt( /// Validates given data receipt. Checks validity of the length of the returned data. fn validate_data_receipt( - limit_config: &VMLimitConfig, + limit_config: &LimitConfig, receipt: &DataReceipt, ) -> Result<(), ReceiptValidationError> { let data_len = receipt.data.as_ref().map(|data| data.len()).unwrap_or(0); @@ -343,7 +339,7 @@ fn validate_data_receipt( /// - Validates each individual action. /// - Checks that the total prepaid gas doesn't exceed the limit. pub(crate) fn validate_actions( - limit_config: &VMLimitConfig, + limit_config: &LimitConfig, actions: &[Action], current_protocol_version: ProtocolVersion, ) -> Result<(), ActionsValidationError> { @@ -392,7 +388,7 @@ pub(crate) fn validate_actions( /// Validates a single given action. Checks limits if applicable. pub fn validate_action( - limit_config: &VMLimitConfig, + limit_config: &LimitConfig, action: &Action, current_protocol_version: ProtocolVersion, ) -> Result<(), ActionsValidationError> { @@ -410,7 +406,7 @@ pub fn validate_action( } fn validate_delegate_action( - limit_config: &VMLimitConfig, + limit_config: &LimitConfig, signed_delegate_action: &SignedDelegateAction, current_protocol_version: ProtocolVersion, ) -> Result<(), ActionsValidationError> { @@ -421,7 +417,7 @@ fn validate_delegate_action( /// Validates `DeployContractAction`. Checks that the given contract size doesn't exceed the limit. fn validate_deploy_contract_action( - limit_config: &VMLimitConfig, + limit_config: &LimitConfig, action: &DeployContractAction, ) -> Result<(), ActionsValidationError> { if action.code.len() as u64 > limit_config.max_contract_size { @@ -437,7 +433,7 @@ fn validate_deploy_contract_action( /// Validates `FunctionCallAction`. Checks that the method name length doesn't exceed the limit and /// the length of the arguments doesn't exceed the limit. fn validate_function_call_action( - limit_config: &VMLimitConfig, + limit_config: &LimitConfig, action: &FunctionCallAction, ) -> Result<(), ActionsValidationError> { if action.gas == 0 { @@ -476,15 +472,15 @@ fn validate_stake_action(action: &StakeAction) -> Result<(), ActionsValidationEr /// total number of bytes of the method names doesn't exceed the limit and /// every method name length doesn't exceed the limit. fn validate_add_key_action( - limit_config: &VMLimitConfig, + limit_config: &LimitConfig, action: &AddKeyAction, ) -> Result<(), ActionsValidationError> { if let AccessKeyPermission::FunctionCall(fc) = &action.access_key.permission { // Check whether `receiver_id` is a valid account_id. Historically, we // allowed arbitrary strings there! match limit_config.account_id_validity_rules_version { - near_vm_runner::logic::AccountIdValidityRulesVersion::V0 => (), - near_vm_runner::logic::AccountIdValidityRulesVersion::V1 => { + near_primitives_core::config::AccountIdValidityRulesVersion::V0 => (), + near_primitives_core::config::AccountIdValidityRulesVersion::V1 => { if let Err(_) = fc.receiver_id.parse::() { return Err(ActionsValidationError::InvalidAccountId { account_id: truncate_string(&fc.receiver_id, AccountId::MAX_LEN * 2), @@ -562,7 +558,7 @@ mod tests { use crate::near_primitives::borsh::BorshSerialize; use near_crypto::{InMemorySigner, KeyType, PublicKey, Signature, Signer}; use near_primitives::account::{AccessKey, FunctionCallPermission}; - use near_primitives::delegate_action::{DelegateAction, NonDelegateAction}; + use near_primitives::action::delegate::{DelegateAction, NonDelegateAction}; use near_primitives::hash::{hash, CryptoHash}; use near_primitives::test_utils::account_new; use near_primitives::transaction::{ @@ -576,9 +572,9 @@ mod tests { use crate::near_primitives::shard_layout::ShardUId; use super::*; - use crate::near_primitives::contract::ContractCode; use crate::near_primitives::trie_key::TrieKey; use near_store::{set, set_code}; + use near_vm_runner::ContractCode; /// Initial balance used in tests. const TESTING_INIT_BALANCE: Balance = 1_000_000_000 * NEAR_BASE; @@ -939,12 +935,12 @@ mod tests { alice_account(), bob_account(), &*signer, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "hello".to_string(), args: b"abc".to_vec(), gas: 200, deposit: 0, - })], + }))], CryptoHash::default(), ), RuntimeError::InvalidTxError(InvalidTxError::ActionsValidation( @@ -1101,12 +1097,12 @@ mod tests { alice_account(), bob_account(), &*signer, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "hello".to_string(), args: b"abc".to_vec(), gas: 300, deposit: 0, - })], + }))], CryptoHash::default(), ), true, @@ -1234,12 +1230,12 @@ mod tests { bob_account(), &*signer, vec![ - Action::FunctionCall(FunctionCallAction { + Action::FunctionCall(Box::new(FunctionCallAction { method_name: "hello".to_string(), args: b"abc".to_vec(), gas: 100, deposit: 0, - }), + })), Action::CreateAccount(CreateAccountAction {}) ], CryptoHash::default(), @@ -1327,12 +1323,12 @@ mod tests { alice_account(), eve_dot_alice_account(), &*signer, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "hello".to_string(), args: b"abc".to_vec(), gas: 100, deposit: 0, - }),], + })),], CryptoHash::default(), ), true, @@ -1375,12 +1371,12 @@ mod tests { alice_account(), bob_account(), &*signer, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "hello".to_string(), args: b"abc".to_vec(), gas: 100, deposit: 0, - }),], + })),], CryptoHash::default(), ), true, @@ -1420,12 +1416,12 @@ mod tests { alice_account(), bob_account(), &*signer, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "hello".to_string(), args: b"abc".to_vec(), gas: 100, deposit: 100, - }),], + })),], CryptoHash::default(), ), true, @@ -1492,7 +1488,7 @@ mod tests { #[test] fn test_validate_receipt_valid() { - let limit_config = VMLimitConfig::test(); + let limit_config = LimitConfig::test(); validate_receipt( &limit_config, &Receipt::new_balance_refund(&alice_account(), 10), @@ -1503,7 +1499,7 @@ mod tests { #[test] fn test_validate_action_receipt_too_many_input_deps() { - let mut limit_config = VMLimitConfig::test(); + let mut limit_config = LimitConfig::test(); limit_config.max_number_input_data_dependencies = 1; assert_eq!( validate_action_receipt( @@ -1530,7 +1526,7 @@ mod tests { #[test] fn test_validate_data_receipt_valid() { - let limit_config = VMLimitConfig::test(); + let limit_config = LimitConfig::test(); validate_data_receipt( &limit_config, &DataReceipt { data_id: CryptoHash::default(), data: None }, @@ -1546,7 +1542,7 @@ mod tests { #[test] fn test_validate_data_receipt_too_much_data() { - let mut limit_config = VMLimitConfig::test(); + let mut limit_config = LimitConfig::test(); let data = b"hello".to_vec(); limit_config.max_length_returned_data = data.len() as u64 - 1; assert_eq!( @@ -1566,21 +1562,21 @@ mod tests { #[test] fn test_validate_actions_empty() { - let limit_config = VMLimitConfig::test(); + let limit_config = LimitConfig::test(); validate_actions(&limit_config, &[], PROTOCOL_VERSION).expect("empty actions"); } #[test] fn test_validate_actions_valid_function_call() { - let limit_config = VMLimitConfig::test(); + let limit_config = LimitConfig::test(); validate_actions( &limit_config, - &[Action::FunctionCall(FunctionCallAction { + &[Action::FunctionCall(Box::new(FunctionCallAction { method_name: "hello".to_string(), args: b"abc".to_vec(), gas: 100, deposit: 0, - })], + }))], PROTOCOL_VERSION, ) .expect("valid function call action"); @@ -1588,24 +1584,24 @@ mod tests { #[test] fn test_validate_actions_too_much_gas() { - let mut limit_config = VMLimitConfig::test(); + let mut limit_config = LimitConfig::test(); limit_config.max_total_prepaid_gas = 220; assert_eq!( validate_actions( &limit_config, &[ - Action::FunctionCall(FunctionCallAction { + Action::FunctionCall(Box::new(FunctionCallAction { method_name: "hello".to_string(), args: b"abc".to_vec(), gas: 100, deposit: 0, - }), - Action::FunctionCall(FunctionCallAction { + })), + Action::FunctionCall(Box::new(FunctionCallAction { method_name: "hello".to_string(), args: b"abc".to_vec(), gas: 150, deposit: 0, - }) + })) ], PROTOCOL_VERSION, ) @@ -1616,24 +1612,24 @@ mod tests { #[test] fn test_validate_actions_gas_overflow() { - let mut limit_config = VMLimitConfig::test(); + let mut limit_config = LimitConfig::test(); limit_config.max_total_prepaid_gas = 220; assert_eq!( validate_actions( &limit_config, &[ - Action::FunctionCall(FunctionCallAction { + Action::FunctionCall(Box::new(FunctionCallAction { method_name: "hello".to_string(), args: b"abc".to_vec(), gas: u64::max_value() / 2 + 1, deposit: 0, - }), - Action::FunctionCall(FunctionCallAction { + })), + Action::FunctionCall(Box::new(FunctionCallAction { method_name: "hello".to_string(), args: b"abc".to_vec(), gas: u64::max_value() / 2 + 1, deposit: 0, - }) + })) ], PROTOCOL_VERSION, ) @@ -1644,7 +1640,7 @@ mod tests { #[test] fn test_validate_actions_num_actions() { - let mut limit_config = VMLimitConfig::test(); + let mut limit_config = LimitConfig::test(); limit_config.max_actions_per_receipt = 1; assert_eq!( validate_actions( @@ -1665,7 +1661,7 @@ mod tests { #[test] fn test_validate_delete_must_be_final() { - let mut limit_config = VMLimitConfig::test(); + let mut limit_config = LimitConfig::test(); limit_config.max_actions_per_receipt = 3; assert_eq!( validate_actions( @@ -1685,7 +1681,7 @@ mod tests { #[test] fn test_validate_delete_must_work_if_its_final() { - let mut limit_config = VMLimitConfig::test(); + let mut limit_config = LimitConfig::test(); limit_config.max_actions_per_receipt = 3; assert_eq!( validate_actions( @@ -1707,7 +1703,7 @@ mod tests { #[test] fn test_validate_action_valid_create_account() { validate_action( - &VMLimitConfig::test(), + &LimitConfig::test(), &Action::CreateAccount(CreateAccountAction {}), PROTOCOL_VERSION, ) @@ -1717,13 +1713,13 @@ mod tests { #[test] fn test_validate_action_valid_function_call() { validate_action( - &VMLimitConfig::test(), - &Action::FunctionCall(FunctionCallAction { + &LimitConfig::test(), + &Action::FunctionCall(Box::new(FunctionCallAction { method_name: "hello".to_string(), args: b"abc".to_vec(), gas: 100, deposit: 0, - }), + })), PROTOCOL_VERSION, ) .expect("valid action"); @@ -1733,13 +1729,13 @@ mod tests { fn test_validate_action_invalid_function_call_zero_gas() { assert_eq!( validate_action( - &VMLimitConfig::test(), - &Action::FunctionCall(FunctionCallAction { + &LimitConfig::test(), + &Action::FunctionCall(Box::new(FunctionCallAction { method_name: "new".to_string(), args: vec![], gas: 0, deposit: 0, - }), + })), PROTOCOL_VERSION, ) .expect_err("expected an error"), @@ -1750,7 +1746,7 @@ mod tests { #[test] fn test_validate_action_valid_transfer() { validate_action( - &VMLimitConfig::test(), + &LimitConfig::test(), &Action::Transfer(TransferAction { deposit: 10 }), PROTOCOL_VERSION, ) @@ -1760,11 +1756,11 @@ mod tests { #[test] fn test_validate_action_valid_stake() { validate_action( - &VMLimitConfig::test(), - &Action::Stake(StakeAction { + &LimitConfig::test(), + &Action::Stake(Box::new(StakeAction { stake: 100, public_key: "ed25519:KuTCtARNzxZQ3YvXDeLjx83FDqxv2SdQTSbiq876zR7".parse().unwrap(), - }), + })), PROTOCOL_VERSION, ) .expect("valid action"); @@ -1774,11 +1770,11 @@ mod tests { fn test_validate_action_invalid_staking_key() { assert_eq!( validate_action( - &VMLimitConfig::test(), - &Action::Stake(StakeAction { + &LimitConfig::test(), + &Action::Stake(Box::new(StakeAction { stake: 100, public_key: PublicKey::empty(KeyType::ED25519), - }), + })), PROTOCOL_VERSION, ) .expect_err("Expected an error"), @@ -1791,11 +1787,11 @@ mod tests { #[test] fn test_validate_action_valid_add_key_full_permission() { validate_action( - &VMLimitConfig::test(), - &Action::AddKey(AddKeyAction { + &LimitConfig::test(), + &Action::AddKey(Box::new(AddKeyAction { public_key: PublicKey::empty(KeyType::ED25519), access_key: AccessKey::full_access(), - }), + })), PROTOCOL_VERSION, ) .expect("valid action"); @@ -1804,8 +1800,8 @@ mod tests { #[test] fn test_validate_action_valid_add_key_function_call() { validate_action( - &VMLimitConfig::test(), - &Action::AddKey(AddKeyAction { + &LimitConfig::test(), + &Action::AddKey(Box::new(AddKeyAction { public_key: PublicKey::empty(KeyType::ED25519), access_key: AccessKey { nonce: 0, @@ -1815,7 +1811,7 @@ mod tests { method_names: vec!["hello".to_string(), "world".to_string()], }), }, - }), + })), PROTOCOL_VERSION, ) .expect("valid action"); @@ -1824,8 +1820,10 @@ mod tests { #[test] fn test_validate_action_valid_delete_key() { validate_action( - &VMLimitConfig::test(), - &Action::DeleteKey(DeleteKeyAction { public_key: PublicKey::empty(KeyType::ED25519) }), + &LimitConfig::test(), + &Action::DeleteKey(Box::new(DeleteKeyAction { + public_key: PublicKey::empty(KeyType::ED25519), + })), PROTOCOL_VERSION, ) .expect("valid action"); @@ -1834,7 +1832,7 @@ mod tests { #[test] fn test_validate_action_valid_delete_account() { validate_action( - &VMLimitConfig::test(), + &LimitConfig::test(), &Action::DeleteAccount(DeleteAccountAction { beneficiary_id: alice_account() }), PROTOCOL_VERSION, ) @@ -1859,10 +1857,10 @@ mod tests { }; assert_eq!( validate_actions( - &VMLimitConfig::test(), + &LimitConfig::test(), &[ - Action::Delegate(signed_delegate_action.clone()), - Action::Delegate(signed_delegate_action.clone()), + Action::Delegate(Box::new(signed_delegate_action.clone())), + Action::Delegate(Box::new(signed_delegate_action.clone())), ], PROTOCOL_VERSION, ), @@ -1870,18 +1868,18 @@ mod tests { ); assert_eq!( validate_actions( - &&VMLimitConfig::test(), - &[Action::Delegate(signed_delegate_action.clone()),], + &&LimitConfig::test(), + &[Action::Delegate(Box::new(signed_delegate_action.clone())),], PROTOCOL_VERSION, ), Ok(()), ); assert_eq!( validate_actions( - &VMLimitConfig::test(), + &LimitConfig::test(), &[ Action::CreateAccount(CreateAccountAction {}), - Action::Delegate(signed_delegate_action), + Action::Delegate(Box::new(signed_delegate_action)), ], PROTOCOL_VERSION, ), diff --git a/runtime/runtime/tests/runtime_group_tools/mod.rs b/runtime/runtime/tests/runtime_group_tools/mod.rs index 15ae2a30e3e..4c4fba335c4 100644 --- a/runtime/runtime/tests/runtime_group_tools/mod.rs +++ b/runtime/runtime/tests/runtime_group_tools/mod.rs @@ -10,10 +10,10 @@ use near_primitives::test_utils::MockEpochInfoProvider; use near_primitives::transaction::{ExecutionOutcomeWithId, SignedTransaction}; use near_primitives::types::{AccountId, AccountInfo, Balance}; use near_primitives::version::PROTOCOL_VERSION; +use near_primitives_core::config::ActionCosts; use near_store::genesis::GenesisStateApplier; use near_store::test_utils::create_tries; use near_store::ShardTries; -use near_vm_runner::logic::ActionCosts; use node_runtime::{ApplyState, Runtime}; use random_config::random_config; use std::collections::{HashMap, HashSet}; diff --git a/runtime/runtime/tests/test_async_calls.rs b/runtime/runtime/tests/test_async_calls.rs index d86ec0e6f3e..7890e2fb96b 100644 --- a/runtime/runtime/tests/test_async_calls.rs +++ b/runtime/runtime/tests/test_async_calls.rs @@ -30,12 +30,12 @@ fn test_simple_func_call() { signer_sender.account_id.clone(), signer_receiver.account_id, &signer_sender, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "sum_n".to_string(), args: 10u64.to_le_bytes().to_vec(), gas: GAS_1, deposit: 0, - })], + }))], CryptoHash::default(), ); @@ -49,7 +49,7 @@ fn test_simple_func_call() { assert_receipts!(group, "near_0" => r0 @ "near_1", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{..}), {} + a0, Action::FunctionCall(_function_call_action), {} => [ref1] ); assert_refund!(group, ref1 @ "near_0"); } @@ -76,12 +76,12 @@ fn test_single_promise_no_callback() { signer_sender.account_id.clone(), signer_receiver.account_id, &signer_sender, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "call_promise".to_string(), args: serde_json::to_vec(&data).unwrap(), gas: GAS_1, deposit: 0, - })], + }))], CryptoHash::default(), ); @@ -95,17 +95,17 @@ fn test_single_promise_no_callback() { assert_receipts!(group, "near_0" => r0 @ "near_1", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_1); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_1); + assert_eq!(function_call_action.deposit, 0); } => [r1, ref0] ); assert_receipts!(group, "near_1" => r1 @ "near_2", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_2); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_2); + assert_eq!(function_call_action.deposit, 0); } => [ref1]); assert_refund!(group, ref0 @ "near_0"); @@ -142,12 +142,12 @@ fn test_single_promise_with_callback() { signer_sender.account_id.clone(), signer_receiver.account_id, &signer_sender, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "call_promise".to_string(), args: serde_json::to_vec(&data).unwrap(), gas: GAS_1, deposit: 0, - })], + }))], CryptoHash::default(), ); @@ -161,9 +161,9 @@ fn test_single_promise_with_callback() { assert_receipts!(group, "near_0" => r0 @ "near_1", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_1); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_1); + assert_eq!(function_call_action.deposit, 0); } => [r1, r2, ref0] ); let data_id; @@ -173,9 +173,9 @@ fn test_single_promise_with_callback() { data_id = output_data_receivers[0].data_id; }, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_2); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_2); + assert_eq!(function_call_action.deposit, 0); } => [ref1]); assert_receipts!(group, "near_1" => r2 @ "near_3", @@ -184,9 +184,9 @@ fn test_single_promise_with_callback() { assert_eq!(data_id, input_data_ids[0].clone()); }, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_2); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_2); + assert_eq!(function_call_action.deposit, 0); } => [ref2]); @@ -226,12 +226,12 @@ fn test_two_promises_no_callbacks() { signer_sender.account_id.clone(), signer_receiver.account_id, &signer_sender, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "call_promise".to_string(), args: serde_json::to_vec(&data).unwrap(), gas: GAS_1, deposit: 0, - })], + }))], CryptoHash::default(), ); @@ -245,25 +245,25 @@ fn test_two_promises_no_callbacks() { assert_receipts!(group, "near_0" => r0 @ "near_1", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_1); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_1); + assert_eq!(function_call_action.deposit, 0); } => [r1, ref0] ); assert_receipts!(group, "near_1" => r1 @ "near_2", ReceiptEnum::Action(ActionReceipt{actions, ..}), { }, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_2); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_2); + assert_eq!(function_call_action.deposit, 0); } => [r2, ref1]); assert_receipts!(group, "near_2" => r2 @ "near_3", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_3); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_3); + assert_eq!(function_call_action.deposit, 0); } => [ref2]); @@ -320,12 +320,12 @@ fn test_two_promises_with_two_callbacks() { signer_sender.account_id.clone(), signer_receiver.account_id, &signer_sender, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "call_promise".to_string(), args: serde_json::to_vec(&data).unwrap(), gas: GAS_1, deposit: 0, - })], + }))], CryptoHash::default(), ); @@ -339,41 +339,41 @@ fn test_two_promises_with_two_callbacks() { assert_receipts!(group, "near_0" => r0 @ "near_1", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_1); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_1); + assert_eq!(function_call_action.deposit, 0); } => [r1, cb1, ref0] ); assert_receipts!(group, "near_1" => r1 @ "near_2", ReceiptEnum::Action(ActionReceipt{actions, ..}), { }, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_2); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_2); + assert_eq!(function_call_action.deposit, 0); } => [r2, cb2, ref1]); assert_receipts!(group, "near_2" => r2 @ "near_3", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_3); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_3); + assert_eq!(function_call_action.deposit, 0); } => [ref2]); assert_receipts!(group, "near_2" => cb2 @ "near_4", ReceiptEnum::Action(ActionReceipt{actions, ..}), { }, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_3); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_3); + assert_eq!(function_call_action.deposit, 0); } => [ref3]); assert_receipts!(group, "near_1" => cb1 @ "near_5", ReceiptEnum::Action(ActionReceipt{actions, ..}), { }, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_2); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_2); + assert_eq!(function_call_action.deposit, 0); } => [ref4]); @@ -411,12 +411,12 @@ fn test_single_promise_no_callback_batch() { signer_sender.account_id.clone(), signer_receiver.account_id, &signer_sender, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "call_promise".to_string(), args: serde_json::to_vec(&data).unwrap(), gas: GAS_1, deposit: 0, - })], + }))], CryptoHash::default(), ); @@ -430,17 +430,17 @@ fn test_single_promise_no_callback_batch() { assert_receipts!(group, "near_0" => r0 @ "near_1", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_1); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_1); + assert_eq!(function_call_action.deposit, 0); } => [r1, ref0] ); assert_receipts!(group, "near_1" => r1 @ "near_2", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_2); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_2); + assert_eq!(function_call_action.deposit, 0); } => [ref1]); assert_refund!(group, ref0 @ "near_0"); @@ -483,12 +483,12 @@ fn test_single_promise_with_callback_batch() { signer_sender.account_id.clone(), signer_receiver.account_id, &signer_sender, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "call_promise".to_string(), args: serde_json::to_vec(&data).unwrap(), gas: GAS_1, deposit: 0, - })], + }))], CryptoHash::default(), ); @@ -502,9 +502,9 @@ fn test_single_promise_with_callback_batch() { assert_receipts!(group, "near_0" => r0 @ "near_1", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_1); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_1); + assert_eq!(function_call_action.deposit, 0); } => [r1, r2, ref0] ); let data_id; @@ -514,9 +514,9 @@ fn test_single_promise_with_callback_batch() { data_id = output_data_receivers[0].data_id; }, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_2); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_2); + assert_eq!(function_call_action.deposit, 0); } => [ref1]); assert_receipts!(group, "near_1" => r2 @ "near_3", @@ -525,9 +525,9 @@ fn test_single_promise_with_callback_batch() { assert_eq!(data_id, input_data_ids[0].clone()); }, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_2); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_2); + assert_eq!(function_call_action.deposit, 0); } => [ref2]); @@ -557,12 +557,12 @@ fn test_simple_transfer() { signer_sender.account_id.clone(), signer_receiver.account_id, &signer_sender, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "call_promise".to_string(), args: serde_json::to_vec(&data).unwrap(), gas: GAS_1, deposit: 0, - })], + }))], CryptoHash::default(), ); @@ -576,9 +576,9 @@ fn test_simple_transfer() { assert_receipts!(group, "near_0" => r0 @ "near_1", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_1); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_1); + assert_eq!(function_call_action.deposit, 0); } => [r1, ref0] ); assert_receipts!(group, "near_1" => r1 @ "near_2", @@ -624,12 +624,12 @@ fn test_create_account_with_transfer_and_full_key() { signer_sender.account_id.clone(), signer_receiver.account_id, &signer_sender, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "call_promise".to_string(), args: serde_json::to_vec(&data).unwrap(), gas: GAS_1, deposit: 0, - })], + }))], CryptoHash::default(), ); @@ -643,9 +643,9 @@ fn test_create_account_with_transfer_and_full_key() { assert_receipts!(group, "near_0" => r0 @ "near_1", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_1); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_1); + assert_eq!(function_call_action.deposit, 0); } => [r1, ref0] ); assert_receipts!(group, "near_1" => r1 @ "near_2", @@ -655,10 +655,10 @@ fn test_create_account_with_transfer_and_full_key() { a1, Action::Transfer(TransferAction{deposit}), { assert_eq!(*deposit, 10000000000000000000000000); }, - a2, Action::AddKey(AddKeyAction{public_key, access_key}), { - assert_eq!(public_key, &signer_new_account.public_key); - assert_eq!(access_key.nonce, 0); - assert_eq!(access_key.permission, AccessKeyPermission::FullAccess); + a2, Action::AddKey(add_key_action), { + assert_eq!(add_key_action.public_key, signer_new_account.public_key); + assert_eq!(add_key_action.access_key.nonce, 0); + assert_eq!(add_key_action.access_key.permission, AccessKeyPermission::FullAccess); } => [ref1] ); @@ -736,12 +736,12 @@ fn test_account_factory() { signer_sender.account_id.clone(), signer_receiver.account_id, &signer_sender, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "call_promise".to_string(), args: serde_json::to_vec(&data).unwrap(), gas: GAS_1, deposit: 0, - })], + }))], CryptoHash::default(), ); @@ -755,9 +755,9 @@ fn test_account_factory() { assert_receipts!(group, "near_0" => r0 @ "near_1", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_1); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_1); + assert_eq!(function_call_action.deposit, 0); } => [r1, r2, ref0] ); @@ -773,10 +773,10 @@ fn test_account_factory() { a1, Action::Transfer(TransferAction{deposit}), { assert_eq!(*deposit, TESTING_INIT_BALANCE / 2); }, - a2, Action::AddKey(AddKeyAction{public_key, access_key}), { - assert_eq!(public_key, &signer_new_account.public_key); - assert_eq!(access_key.nonce, 0); - assert_eq!(access_key.permission, AccessKeyPermission::FunctionCall(FunctionCallPermission { + a2, Action::AddKey(add_key_action), { + assert_eq!(add_key_action.public_key, signer_new_account.public_key); + assert_eq!(add_key_action.access_key.nonce, 0); + assert_eq!(add_key_action.access_key.permission, AccessKeyPermission::FunctionCall(FunctionCallPermission { allowance: Some(TESTING_INIT_BALANCE / 2), receiver_id: "near_1".parse().unwrap(), method_names: vec!["call_promise".to_string(), "hello".to_string()], @@ -785,9 +785,9 @@ fn test_account_factory() { a3, Action::DeployContract(DeployContractAction{code}), { assert_eq!(code, near_test_contracts::rs_contract()); }, - a4, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_2); - assert_eq!(*deposit, 0); + a4, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_2); + assert_eq!(function_call_action.deposit, 0); } => [r3, ref1] ); assert_receipts!(group, "near_1" => r2 @ "near_2", @@ -795,25 +795,25 @@ fn test_account_factory() { assert_eq!(input_data_ids, &[data_id]); }, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_2); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_2); + assert_eq!(function_call_action.deposit, 0); } => [r4, ref2] ); assert_receipts!(group, "near_2" => r3 @ "near_0", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_3); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_3); + assert_eq!(function_call_action.deposit, 0); } => [ref3] ); assert_receipts!(group, "near_2" => r4 @ "near_1", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_3); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_3); + assert_eq!(function_call_action.deposit, 0); } => [ref4] ); @@ -882,12 +882,12 @@ fn test_create_account_add_key_call_delete_key_delete_account() { signer_sender.account_id.clone(), signer_receiver.account_id, &signer_sender, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "call_promise".to_string(), args: serde_json::to_vec(&data).unwrap(), gas: GAS_1, deposit: 0, - })], + }))], CryptoHash::default(), ); @@ -901,9 +901,9 @@ fn test_create_account_add_key_call_delete_key_delete_account() { assert_receipts!(group, "near_0" => r0 @ "near_1", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_1); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_1); + assert_eq!(function_call_action.deposit, 0); } => [r1, ref0] ); assert_receipts!(group, "near_1" => r1 @ "near_3", @@ -913,20 +913,20 @@ fn test_create_account_add_key_call_delete_key_delete_account() { a1, Action::Transfer(TransferAction{deposit}), { assert_eq!(*deposit, TESTING_INIT_BALANCE / 2); }, - a2, Action::AddKey(AddKeyAction{public_key, access_key}), { - assert_eq!(public_key, &signer_new_account.public_key); - assert_eq!(access_key.nonce, 1); - assert_eq!(access_key.permission, AccessKeyPermission::FullAccess); + a2, Action::AddKey(add_key_action), { + assert_eq!(add_key_action.public_key, signer_new_account.public_key); + assert_eq!(add_key_action.access_key.nonce, 1); + assert_eq!(add_key_action.access_key.permission, AccessKeyPermission::FullAccess); }, a3, Action::DeployContract(DeployContractAction{code}), { assert_eq!(code, near_test_contracts::rs_contract()); }, - a4, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_2); - assert_eq!(*deposit, 0); + a4, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_2); + assert_eq!(function_call_action.deposit, 0); }, - a5, Action::DeleteKey(DeleteKeyAction{public_key}), { - assert_eq!(public_key, &signer_new_account.public_key); + a5, Action::DeleteKey(delete_key_action), { + assert_eq!(delete_key_action.public_key, signer_new_account.public_key); }, a6, Action::DeleteAccount(DeleteAccountAction{beneficiary_id}), { assert_eq!(beneficiary_id.as_ref(), "near_2"); @@ -936,9 +936,9 @@ fn test_create_account_add_key_call_delete_key_delete_account() { assert_receipts!(group, "near_3" => r2 @ "near_0", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_3); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_3); + assert_eq!(function_call_action.deposit, 0); } => [ref2] ); assert_refund!(group, r3 @ "near_2"); @@ -976,12 +976,12 @@ fn test_transfer_64len_hex() { signer_sender.account_id.clone(), signer_receiver.account_id, &signer_sender, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "call_promise".to_string(), args: serde_json::to_vec(&data).unwrap(), gas: GAS_1, deposit: 0, - })], + }))], CryptoHash::default(), ); @@ -995,9 +995,9 @@ fn test_transfer_64len_hex() { assert_receipts!(group, "near_0" => r0 @ "near_1", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_1); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_1); + assert_eq!(function_call_action.deposit, 0); } => [r1, ref0] ); assert_receipts!(group, "near_1" => r1 @ account_id.as_ref(), @@ -1042,12 +1042,12 @@ fn test_create_transfer_64len_hex_fail() { signer_sender.account_id.clone(), signer_receiver.account_id, &signer_sender, - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "call_promise".to_string(), args: serde_json::to_vec(&data).unwrap(), gas: GAS_1, deposit: 0, - })], + }))], CryptoHash::default(), ); @@ -1061,9 +1061,9 @@ fn test_create_transfer_64len_hex_fail() { assert_receipts!(group, "near_0" => r0 @ "near_1", ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, actions, - a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { - assert_eq!(*gas, GAS_1); - assert_eq!(*deposit, 0); + a0, Action::FunctionCall(function_call_action), { + assert_eq!(function_call_action.gas, GAS_1); + assert_eq!(function_call_action.deposit, 0); } => [r1, ref0] ); assert_receipts!(group, "near_1" => r1 @ account_id.as_ref(), diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 70a43b798fa..cf7e1e0ee2e 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -2,6 +2,6 @@ # This specifies the version of Rust we use to build. # Individual crates in the workspace may support a lower version, as indicated by `rust-version` field in each crate's `Cargo.toml`. # The version specified below, should be at least as high as the maximum `rust-version` within the workspace. -channel = "1.71.0" +channel = "1.72.0" components = [ "rustfmt" ] targets = [ "wasm32-unknown-unknown" ] diff --git a/test-utils/runtime-tester/src/fuzzing.rs b/test-utils/runtime-tester/src/fuzzing.rs index 02b4a723fcb..a9970ef1583 100644 --- a/test-utils/runtime-tester/src/fuzzing.rs +++ b/test-utils/runtime-tester/src/fuzzing.rs @@ -155,13 +155,13 @@ impl TransactionConfig { signer, actions: vec![ Action::CreateAccount(CreateAccountAction {}), - Action::AddKey(AddKeyAction { + Action::AddKey(Box::new(AddKeyAction { public_key: new_public_key, access_key: AccessKey { nonce: 0, permission: AccessKeyPermission::FullAccess, }, - }), + })), Action::Transfer(TransferAction { deposit: NEAR_BASE }), ], }) @@ -266,7 +266,7 @@ impl TransactionConfig { while actions.len() < actions_num && u.len() > Function::size_hint(0).1.unwrap() { let function = u.choose(&receiver_functions)?; - actions.push(Action::FunctionCall(function.arbitrary(u)?)); + actions.push(Action::FunctionCall(Box::new(function.arbitrary(u)?))); } Ok(TransactionConfig { @@ -291,11 +291,11 @@ impl TransactionConfig { signer_id: signer_account.id.clone(), receiver_id: signer_account.id.clone(), signer, - actions: vec![Action::AddKey(scope.add_new_key( + actions: vec![Action::AddKey(Box::new(scope.add_new_key( u, scope.usize_id(&signer_account), nonce, - )?)], + )?))], }) }); @@ -323,7 +323,7 @@ impl TransactionConfig { signer_id: signer_account.id.clone(), receiver_id: signer_account.id.clone(), signer, - actions: vec![Action::DeleteKey(DeleteKeyAction { public_key })], + actions: vec![Action::DeleteKey(Box::new(DeleteKeyAction { public_key }))], }) }); diff --git a/test-utils/testlib/src/fees_utils.rs b/test-utils/testlib/src/fees_utils.rs index 18d6f9a91ff..ddd964946a5 100644 --- a/test-utils/testlib/src/fees_utils.rs +++ b/test-utils/testlib/src/fees_utils.rs @@ -1,25 +1,29 @@ //! Helper functions to compute the costs of certain actions assuming they succeed and the only //! actions in the transaction batch. use near_primitives::config::ActionCosts; +use near_primitives::runtime::config::RuntimeConfig; use near_primitives::runtime::fees::RuntimeFeesConfig; use near_primitives::transaction::Action; use near_primitives::types::{AccountId, Balance, Gas}; -use near_primitives::version::PROTOCOL_VERSION; pub struct FeeHelper { - pub cfg: RuntimeFeesConfig, + pub rt_cfg: RuntimeConfig, pub gas_price: Balance, } impl FeeHelper { - pub fn new(cfg: RuntimeFeesConfig, gas_price: Balance) -> Self { - Self { cfg, gas_price } + pub fn new(rt_cfg: RuntimeConfig, gas_price: Balance) -> Self { + Self { rt_cfg, gas_price } + } + + pub fn cfg(&self) -> &RuntimeFeesConfig { + &self.rt_cfg.fees } pub fn gas_to_balance_inflated(&self, gas: Gas) -> Balance { gas as Balance - * (self.gas_price * (*self.cfg.pessimistic_gas_price_inflation_ratio.numer() as u128) - / (*self.cfg.pessimistic_gas_price_inflation_ratio.denom() as u128)) + * (self.gas_price * (*self.cfg().pessimistic_gas_price_inflation_ratio.numer() as u128) + / (*self.cfg().pessimistic_gas_price_inflation_ratio.denom() as u128)) } pub fn gas_to_balance(&self, gas: Gas) -> Balance { @@ -27,28 +31,28 @@ impl FeeHelper { } pub fn gas_burnt_to_reward(&self, gas_burnt: Gas) -> Balance { - let gas_reward = gas_burnt * *self.cfg.burnt_gas_reward.numer() as u64 - / *self.cfg.burnt_gas_reward.denom() as u64; + let gas_reward = gas_burnt * *self.cfg().burnt_gas_reward.numer() as u64 + / *self.cfg().burnt_gas_reward.denom() as u64; self.gas_to_balance(gas_reward) } pub fn create_account_cost(&self) -> Balance { - let exec_gas = self.cfg.fee(ActionCosts::new_action_receipt).exec_fee() - + self.cfg.fee(ActionCosts::create_account).exec_fee(); - let send_gas = self.cfg.fee(ActionCosts::new_action_receipt).send_fee(false) - + self.cfg.fee(ActionCosts::create_account).send_fee(false); + let exec_gas = self.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + + self.cfg().fee(ActionCosts::create_account).exec_fee(); + let send_gas = self.cfg().fee(ActionCosts::new_action_receipt).send_fee(false) + + self.cfg().fee(ActionCosts::create_account).send_fee(false); self.gas_to_balance(exec_gas + send_gas) } pub fn create_account_transfer_full_key_fee(&self) -> Gas { - let exec_gas = self.cfg.fee(ActionCosts::new_action_receipt).exec_fee() - + self.cfg.fee(ActionCosts::create_account).exec_fee() - + self.cfg.fee(ActionCosts::transfer).exec_fee() - + self.cfg.fee(ActionCosts::add_full_access_key).exec_fee(); - let send_gas = self.cfg.fee(ActionCosts::new_action_receipt).send_fee(false) - + self.cfg.fee(ActionCosts::create_account).send_fee(false) - + self.cfg.fee(ActionCosts::transfer).send_fee(false) - + self.cfg.fee(ActionCosts::add_full_access_key).send_fee(false); + let exec_gas = self.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + + self.cfg().fee(ActionCosts::create_account).exec_fee() + + self.cfg().fee(ActionCosts::transfer).exec_fee() + + self.cfg().fee(ActionCosts::add_full_access_key).exec_fee(); + let send_gas = self.cfg().fee(ActionCosts::new_action_receipt).send_fee(false) + + self.cfg().fee(ActionCosts::create_account).send_fee(false) + + self.cfg().fee(ActionCosts::transfer).send_fee(false) + + self.cfg().fee(ActionCosts::add_full_access_key).send_fee(false); exec_gas + send_gas } @@ -57,56 +61,56 @@ impl FeeHelper { } pub fn create_account_transfer_full_key_cost_no_reward(&self) -> Balance { - let exec_gas = self.cfg.fee(ActionCosts::new_action_receipt).exec_fee() - + self.cfg.fee(ActionCosts::create_account).exec_fee() - + self.cfg.fee(ActionCosts::transfer).exec_fee() - + self.cfg.fee(ActionCosts::add_full_access_key).exec_fee(); - let send_gas = self.cfg.fee(ActionCosts::new_action_receipt).send_fee(false) - + self.cfg.fee(ActionCosts::create_account).send_fee(false) - + self.cfg.fee(ActionCosts::transfer).send_fee(false) - + self.cfg.fee(ActionCosts::add_full_access_key).send_fee(false); + let exec_gas = self.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + + self.cfg().fee(ActionCosts::create_account).exec_fee() + + self.cfg().fee(ActionCosts::transfer).exec_fee() + + self.cfg().fee(ActionCosts::add_full_access_key).exec_fee(); + let send_gas = self.cfg().fee(ActionCosts::new_action_receipt).send_fee(false) + + self.cfg().fee(ActionCosts::create_account).send_fee(false) + + self.cfg().fee(ActionCosts::transfer).send_fee(false) + + self.cfg().fee(ActionCosts::add_full_access_key).send_fee(false); self.gas_to_balance(send_gas) + self.gas_to_balance_inflated(exec_gas) } pub fn create_account_transfer_full_key_cost_fail_on_create_account(&self) -> Balance { - let exec_gas = self.cfg.fee(ActionCosts::new_action_receipt).exec_fee() - + self.cfg.fee(ActionCosts::create_account).exec_fee(); - let send_gas = self.cfg.fee(ActionCosts::new_action_receipt).send_fee(false) - + self.cfg.fee(ActionCosts::create_account).send_fee(false) - + self.cfg.fee(ActionCosts::transfer).send_fee(false) - + self.cfg.fee(ActionCosts::add_full_access_key).send_fee(false); + let exec_gas = self.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + + self.cfg().fee(ActionCosts::create_account).exec_fee(); + let send_gas = self.cfg().fee(ActionCosts::new_action_receipt).send_fee(false) + + self.cfg().fee(ActionCosts::create_account).send_fee(false) + + self.cfg().fee(ActionCosts::transfer).send_fee(false) + + self.cfg().fee(ActionCosts::add_full_access_key).send_fee(false); self.gas_to_balance(exec_gas + send_gas) } pub fn deploy_contract_cost(&self, num_bytes: u64) -> Balance { - let exec_gas = self.cfg.fee(ActionCosts::new_action_receipt).exec_fee() - + self.cfg.fee(ActionCosts::deploy_contract_base).exec_fee() - + num_bytes * self.cfg.fee(ActionCosts::deploy_contract_byte).exec_fee(); - let send_gas = self.cfg.fee(ActionCosts::new_action_receipt).send_fee(true) - + self.cfg.fee(ActionCosts::deploy_contract_base).send_fee(true) - + num_bytes * self.cfg.fee(ActionCosts::deploy_contract_byte).send_fee(true); + let exec_gas = self.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + + self.cfg().fee(ActionCosts::deploy_contract_base).exec_fee() + + num_bytes * self.cfg().fee(ActionCosts::deploy_contract_byte).exec_fee(); + let send_gas = self.cfg().fee(ActionCosts::new_action_receipt).send_fee(true) + + self.cfg().fee(ActionCosts::deploy_contract_base).send_fee(true) + + num_bytes * self.cfg().fee(ActionCosts::deploy_contract_byte).send_fee(true); self.gas_to_balance(exec_gas + send_gas) } pub fn function_call_exec_gas(&self, num_bytes: u64) -> Gas { - self.cfg.fee(ActionCosts::new_action_receipt).exec_fee() - + self.cfg.fee(ActionCosts::function_call_base).exec_fee() - + num_bytes * self.cfg.fee(ActionCosts::function_call_byte).exec_fee() + self.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + + self.cfg().fee(ActionCosts::function_call_base).exec_fee() + + num_bytes * self.cfg().fee(ActionCosts::function_call_byte).exec_fee() } pub fn function_call_cost(&self, num_bytes: u64, prepaid_gas: u64) -> Balance { let exec_gas = self.function_call_exec_gas(num_bytes); - let send_gas = self.cfg.fee(ActionCosts::new_action_receipt).send_fee(false) - + self.cfg.fee(ActionCosts::function_call_base).send_fee(false) - + num_bytes * self.cfg.fee(ActionCosts::function_call_byte).send_fee(false); + let send_gas = self.cfg().fee(ActionCosts::new_action_receipt).send_fee(false) + + self.cfg().fee(ActionCosts::function_call_base).send_fee(false) + + num_bytes * self.cfg().fee(ActionCosts::function_call_byte).send_fee(false); self.gas_to_balance(exec_gas + send_gas + prepaid_gas) } pub fn transfer_fee(&self) -> Gas { - let exec_gas = self.cfg.fee(ActionCosts::new_action_receipt).exec_fee() - + self.cfg.fee(ActionCosts::transfer).exec_fee(); - let send_gas = self.cfg.fee(ActionCosts::new_action_receipt).send_fee(false) - + self.cfg.fee(ActionCosts::transfer).send_fee(false); + let exec_gas = self.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + + self.cfg().fee(ActionCosts::transfer).exec_fee(); + let send_gas = self.cfg().fee(ActionCosts::new_action_receipt).send_fee(false) + + self.cfg().fee(ActionCosts::transfer).send_fee(false); exec_gas + send_gas } @@ -119,44 +123,44 @@ impl FeeHelper { } pub fn stake_cost(&self) -> Balance { - let exec_gas = self.cfg.fee(ActionCosts::new_action_receipt).exec_fee() - + self.cfg.fee(ActionCosts::stake).exec_fee(); - let send_gas = self.cfg.fee(ActionCosts::new_action_receipt).send_fee(true) - + self.cfg.fee(ActionCosts::stake).send_fee(true); + let exec_gas = self.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + + self.cfg().fee(ActionCosts::stake).exec_fee(); + let send_gas = self.cfg().fee(ActionCosts::new_action_receipt).send_fee(true) + + self.cfg().fee(ActionCosts::stake).send_fee(true); self.gas_to_balance(exec_gas + send_gas) } pub fn add_key_cost(&self, num_bytes: u64) -> Balance { - let exec_gas = self.cfg.fee(ActionCosts::new_action_receipt).exec_fee() - + self.cfg.fee(ActionCosts::add_function_call_key_base).exec_fee() - + num_bytes * self.cfg.fee(ActionCosts::add_function_call_key_byte).exec_fee(); - let send_gas = self.cfg.fee(ActionCosts::new_action_receipt).send_fee(true) - + self.cfg.fee(ActionCosts::add_function_call_key_base).send_fee(true) - + num_bytes * self.cfg.fee(ActionCosts::add_function_call_key_byte).send_fee(true); + let exec_gas = self.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + + self.cfg().fee(ActionCosts::add_function_call_key_base).exec_fee() + + num_bytes * self.cfg().fee(ActionCosts::add_function_call_key_byte).exec_fee(); + let send_gas = self.cfg().fee(ActionCosts::new_action_receipt).send_fee(true) + + self.cfg().fee(ActionCosts::add_function_call_key_base).send_fee(true) + + num_bytes * self.cfg().fee(ActionCosts::add_function_call_key_byte).send_fee(true); self.gas_to_balance(exec_gas + send_gas) } pub fn add_key_full_cost(&self) -> Balance { - let exec_gas = self.cfg.fee(ActionCosts::new_action_receipt).exec_fee() - + self.cfg.fee(ActionCosts::add_full_access_key).exec_fee(); - let send_gas = self.cfg.fee(ActionCosts::new_action_receipt).send_fee(true) - + self.cfg.fee(ActionCosts::add_full_access_key).send_fee(true); + let exec_gas = self.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + + self.cfg().fee(ActionCosts::add_full_access_key).exec_fee(); + let send_gas = self.cfg().fee(ActionCosts::new_action_receipt).send_fee(true) + + self.cfg().fee(ActionCosts::add_full_access_key).send_fee(true); self.gas_to_balance(exec_gas + send_gas) } pub fn delete_key_cost(&self) -> Balance { - let exec_gas = self.cfg.fee(ActionCosts::new_action_receipt).exec_fee() - + self.cfg.fee(ActionCosts::delete_key).exec_fee(); - let send_gas = self.cfg.fee(ActionCosts::new_action_receipt).send_fee(true) - + self.cfg.fee(ActionCosts::delete_key).send_fee(true); + let exec_gas = self.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + + self.cfg().fee(ActionCosts::delete_key).exec_fee(); + let send_gas = self.cfg().fee(ActionCosts::new_action_receipt).send_fee(true) + + self.cfg().fee(ActionCosts::delete_key).send_fee(true); self.gas_to_balance(exec_gas + send_gas) } pub fn prepaid_delete_account_cost(&self) -> Balance { - let exec_gas = self.cfg.fee(ActionCosts::new_action_receipt).exec_fee() - + self.cfg.fee(ActionCosts::delete_account).exec_fee(); - let send_gas = self.cfg.fee(ActionCosts::new_action_receipt).send_fee(false) - + self.cfg.fee(ActionCosts::delete_account).send_fee(false); + let exec_gas = self.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + + self.cfg().fee(ActionCosts::delete_account).exec_fee(); + let send_gas = self.cfg().fee(ActionCosts::new_action_receipt).send_fee(false) + + self.cfg().fee(ActionCosts::delete_account).send_fee(false); let total_fee = exec_gas + send_gas; @@ -172,19 +176,12 @@ impl FeeHelper { pub fn meta_tx_overhead_cost(&self, actions: &[Action], receiver: &AccountId) -> Balance { // for tests, we assume sender != receiver let sir = false; - let base = self.cfg.fee(ActionCosts::delegate); - let receipt = self.cfg.fee(ActionCosts::new_action_receipt); + let base = self.cfg().fee(ActionCosts::delegate); + let receipt = self.cfg().fee(ActionCosts::new_action_receipt); let total_gas = base.exec_fee() + base.send_fee(sir) + receipt.send_fee(sir) - + node_runtime::config::total_send_fees( - &self.cfg, - sir, - actions, - receiver, - PROTOCOL_VERSION, - ) - .unwrap(); + + node_runtime::config::total_send_fees(&self.rt_cfg, sir, actions, receiver).unwrap(); self.gas_to_balance(total_gas) } } diff --git a/tools/database/README.md b/tools/database/README.md index 9c4e3ec6dd6..5b21831ea80 100644 --- a/tools/database/README.md +++ b/tools/database/README.md @@ -62,6 +62,18 @@ Then you can call `neard database change-db-kind --new-kind Cold change-hot`. Notice that even though in your mind this db is cold, in your config this db hot, so you have to pass `change-hot`. +## Compact database + +Run compaction on the SST files. Running this command might increase database read performance. +This is good use case when changing `block_size` and wishing to perform test on how the RocksDB performance has +changed. + +Example usage: +```bash +cargo run --bin neard -- database compact-database +``` + + ## Make a DB Snapshot Makes a copy of a DB (hot store only) at a specified location. If the @@ -70,7 +82,7 @@ take no additional disk space due to hardlinking all the files. Example usage: ```bash -cargo run --bin neard -- --home /home/ubuntu/.near database make_snapshot --destination /home/ubuntu/.near/data/snapshot +cargo run --bin neard -- --home /home/ubuntu/.near database make-snapshot --destination /home/ubuntu/.near/data/snapshot ``` In this example all `.sst` files from `/home/ubuntu/.near/data` will be also diff --git a/tools/database/src/analyse_data_size_distribution.rs b/tools/database/src/analyse_data_size_distribution.rs index 4acdbeb6ff2..b6142394b6c 100644 --- a/tools/database/src/analyse_data_size_distribution.rs +++ b/tools/database/src/analyse_data_size_distribution.rs @@ -8,7 +8,7 @@ use std::sync::{Arc, Mutex}; use std::{panic, println}; use strum::IntoEnumIterator; -use crate::utils::open_rocksdb; +use crate::utils::{open_rocksdb, resolve_column}; #[derive(Parser)] pub(crate) struct AnalyseDataSizeDistributionCommand { @@ -21,10 +21,6 @@ pub(crate) struct AnalyseDataSizeDistributionCommand { top_k: usize, } -fn resolve_db_col(col: &str) -> Option { - DBCol::iter().filter(|db_col| <&str>::from(db_col) == col).next() -} - #[derive(Clone)] struct ColumnFamilyCountAndSize { number_of_pairs: usize, @@ -186,19 +182,17 @@ fn read_all_pairs(db: &RocksDB, col_families: &Vec) -> DataSizeDistributi DataSizeDistribution::new(key_sizes, value_sizes, column_families) } -fn get_column_families(input_col: &Option) -> Vec { +fn get_column_families(input_col: &Option) -> anyhow::Result> { match input_col { - Some(column_name) => { - vec![resolve_db_col(&column_name).unwrap()] - } - None => DBCol::iter().collect(), + Some(column_name) => Ok(vec![resolve_column(column_name)?]), + None => Ok(DBCol::iter().collect()), } } impl AnalyseDataSizeDistributionCommand { pub(crate) fn run(&self, home: &PathBuf) -> anyhow::Result<()> { - let db = open_rocksdb(home)?; - let column_families = get_column_families(&self.column); + let db = open_rocksdb(home, near_store::Mode::ReadOnly)?; + let column_families = get_column_families(&self.column)?; let results = read_all_pairs(&db, &column_families); results.print_results(self.top_k); diff --git a/tools/database/src/commands.rs b/tools/database/src/commands.rs index eab6fb58e4d..d3f337fce33 100644 --- a/tools/database/src/commands.rs +++ b/tools/database/src/commands.rs @@ -1,5 +1,6 @@ use crate::adjust_database::ChangeDbKindCommand; use crate::analyse_data_size_distribution::AnalyseDataSizeDistributionCommand; +use crate::compact::RunCompactionCommand; use crate::make_snapshot::MakeSnapshotCommand; use crate::run_migrations::RunMigrationsCommand; use crate::state_perf::StatePerfCommand; @@ -21,6 +22,9 @@ enum SubCommand { /// Change DbKind of hot or cold db. ChangeDbKind(ChangeDbKindCommand), + /// Run SST file compaction on database + CompactDatabase(RunCompactionCommand), + /// Make snapshot of the database MakeSnapshot(MakeSnapshotCommand), @@ -37,6 +41,7 @@ impl DatabaseCommand { match &self.subcmd { SubCommand::AnalyseDataSizeDistribution(cmd) => cmd.run(home), SubCommand::ChangeDbKind(cmd) => cmd.run(home), + SubCommand::CompactDatabase(cmd) => cmd.run(home), SubCommand::MakeSnapshot(cmd) => { let near_config = nearcore::config::load_config( &home, diff --git a/tools/database/src/compact.rs b/tools/database/src/compact.rs new file mode 100644 index 00000000000..d70f29bf574 --- /dev/null +++ b/tools/database/src/compact.rs @@ -0,0 +1,24 @@ +use crate::utils::{open_rocksdb, resolve_column}; +use clap::Parser; +use near_store::db::Database; +use std::path::PathBuf; + +#[derive(Parser)] +pub(crate) struct RunCompactionCommand { + /// If specified only this column will compacted + #[arg(short, long)] + column: Option, +} + +impl RunCompactionCommand { + pub(crate) fn run(&self, home: &PathBuf) -> anyhow::Result<()> { + let db = open_rocksdb(home, near_store::Mode::ReadWrite)?; + if let Some(col_name) = &self.column { + db.compact_column(resolve_column(col_name)?)?; + } else { + db.compact()?; + } + eprintln!("Compaction is finished!"); + Ok(()) + } +} diff --git a/tools/database/src/lib.rs b/tools/database/src/lib.rs index f4bb1914908..a540332bc00 100644 --- a/tools/database/src/lib.rs +++ b/tools/database/src/lib.rs @@ -1,6 +1,7 @@ mod adjust_database; mod analyse_data_size_distribution; pub mod commands; +mod compact; mod make_snapshot; mod run_migrations; mod state_perf; diff --git a/tools/database/src/state_perf.rs b/tools/database/src/state_perf.rs index 3997752208b..650dc7157fa 100644 --- a/tools/database/src/state_perf.rs +++ b/tools/database/src/state_perf.rs @@ -32,7 +32,7 @@ pub(crate) struct StatePerfCommand { impl StatePerfCommand { pub(crate) fn run(&self, home: &Path) -> anyhow::Result<()> { - let rocksdb = Arc::new(open_rocksdb(home)?); + let rocksdb = Arc::new(open_rocksdb(home, near_store::Mode::ReadOnly)?); let store = near_store::NodeStorage::new(rocksdb).get_hot_store(); eprintln!("Start State perf test"); let mut perf_context = PerfContext::new(); diff --git a/tools/database/src/utils.rs b/tools/database/src/utils.rs index 0de9852ec88..85eb6de28bf 100644 --- a/tools/database/src/utils.rs +++ b/tools/database/src/utils.rs @@ -1,16 +1,26 @@ use std::path::Path; -pub(crate) fn open_rocksdb(home: &Path) -> anyhow::Result { +use anyhow::anyhow; +use near_store::DBCol; +use strum::IntoEnumIterator; + +pub(crate) fn open_rocksdb( + home: &Path, + mode: near_store::Mode, +) -> anyhow::Result { let config = nearcore::config::Config::from_file_skip_validation( &home.join(nearcore::config::CONFIG_FILENAME), )?; let store_config = &config.store; let db_path = store_config.path.as_ref().cloned().unwrap_or_else(|| home.join("data")); - let rocksdb = near_store::db::RocksDB::open( - &db_path, - store_config, - near_store::Mode::ReadOnly, - near_store::Temperature::Hot, - )?; + let rocksdb = + near_store::db::RocksDB::open(&db_path, store_config, mode, near_store::Temperature::Hot)?; Ok(rocksdb) } + +pub(crate) fn resolve_column(col_name: &str) -> anyhow::Result { + DBCol::iter() + .filter(|db_col| <&str>::from(db_col) == col_name) + .next() + .ok_or_else(|| anyhow!("column {col_name} does not exist")) +} diff --git a/tools/debug-ui/src/RoutingTableView.tsx b/tools/debug-ui/src/RoutingTableView.tsx index b4c18ccb073..b6702201055 100644 --- a/tools/debug-ui/src/RoutingTableView.tsx +++ b/tools/debug-ui/src/RoutingTableView.tsx @@ -63,11 +63,16 @@ export const RoutingTableView = ({ addr }: RoutingTableViewProps) => { {direct_peers.map((peer_id) => { const peer_label = peerLabels[peer_id]; + + const peer_distances = routingInfo.peer_distances[peer_id]; + const formatted_distances = peer_distances == null ? "null" : + peer_distances.distance.map((x) => x ?? '_').join(', '); + return ( {peer_id.substring(8, 14)}... {peer_label} - {routingInfo.peer_distances[peer_id].distance.join(', ')} + {formatted_distances} ); })} diff --git a/tools/flat-storage/Cargo.toml b/tools/flat-storage/Cargo.toml index b2d49aba6d6..35e6080057e 100644 --- a/tools/flat-storage/Cargo.toml +++ b/tools/flat-storage/Cargo.toml @@ -13,8 +13,8 @@ anyhow.workspace = true borsh.workspace = true clap.workspace = true rayon.workspace = true - tqdm.workspace = true +tracing.workspace = true near-chain.workspace = true near-chain-configs.workspace = true diff --git a/tools/flat-storage/src/commands.rs b/tools/flat-storage/src/commands.rs index d667115fff8..f0633389e0f 100644 --- a/tools/flat-storage/src/commands.rs +++ b/tools/flat-storage/src/commands.rs @@ -4,6 +4,7 @@ use clap::Parser; use near_chain::flat_storage_creator::FlatStorageShardCreator; use near_chain::types::RuntimeAdapter; use near_chain::{ChainStore, ChainStoreAccess}; +use near_chain_configs::GenesisValidationMode; use near_epoch_manager::{EpochManager, EpochManagerAdapter, EpochManagerHandle}; use near_store::flat::{ inline_flat_state_values, store_helper, FlatStateDelta, FlatStateDeltaMetadata, @@ -142,9 +143,18 @@ impl FlatStorageCommand { (node_storage, epoch_manager, hot_runtime, chain_store, hot_store) } - pub fn run(&self, home_dir: &PathBuf) -> anyhow::Result<()> { - let near_config = load_config(home_dir, near_chain_configs::GenesisValidationMode::Full)?; - let opener = NodeStorage::opener(home_dir, false, &near_config.config.store, None); + pub fn run( + &self, + home_dir: &PathBuf, + genesis_validation: GenesisValidationMode, + ) -> anyhow::Result<()> { + let near_config = load_config(home_dir, genesis_validation)?; + let opener = NodeStorage::opener( + home_dir, + near_config.config.archive, + &near_config.config.store, + None, + ); match &self.subcmd { SubCommand::View => { diff --git a/tools/mirror/src/lib.rs b/tools/mirror/src/lib.rs index cb4c567a0d1..b3c4dad8be5 100644 --- a/tools/mirror/src/lib.rs +++ b/tools/mirror/src/lib.rs @@ -969,17 +969,17 @@ impl TxMirror { crate::key_mapping::map_account(tx.receiver_id(), self.secret.as_ref()); nonce_updates.insert((receiver_id, public_key.clone())); - actions.push(Action::AddKey(AddKeyAction { + actions.push(Action::AddKey(Box::new(AddKeyAction { public_key, access_key: add_key.access_key.clone(), - })); + }))); } Action::DeleteKey(delete_key) => { let replacement = crate::key_mapping::map_key(&delete_key.public_key, self.secret.as_ref()); let public_key = replacement.public_key(); - actions.push(Action::DeleteKey(DeleteKeyAction { public_key })); + actions.push(Action::DeleteKey(Box::new(DeleteKeyAction { public_key }))); } Action::Transfer(_) => { if tx.receiver_id().is_implicit() && source_actions.len() == 1 { @@ -1018,10 +1018,10 @@ impl TxMirror { }; } if account_created && !full_key_added { - actions.push(Action::AddKey(AddKeyAction { + actions.push(Action::AddKey(Box::new(AddKeyAction { public_key: crate::key_mapping::EXTRA_KEY.public_key(), access_key: AccessKey::full_access(), - })); + }))); } Ok((actions, nonce_updates)) } @@ -1176,21 +1176,27 @@ impl TxMirror { let mut nonce_updates = HashSet::new(); let mut target_actions = Vec::new(); + let mut full_key_added = false; + let mut account_created = false; for a in actions { match a { Action::AddKey(a) => { + if a.access_key.permission == AccessKeyPermission::FullAccess { + full_key_added = true; + } let target_public_key = crate::key_mapping::map_key(&a.public_key, self.secret.as_ref()) .public_key(); nonce_updates.insert((target_receiver_id.clone(), target_public_key.clone())); - target_actions.push(Action::AddKey(AddKeyAction { + target_actions.push(Action::AddKey(Box::new(AddKeyAction { public_key: target_public_key, access_key: a.access_key.clone(), - })); + }))); } Action::CreateAccount(_) => { + account_created = true; target_actions.push(Action::CreateAccount(CreateAccountAction {})) } Action::Transfer(_) => { @@ -1206,6 +1212,12 @@ impl TxMirror { _ => {} }; } + if account_created && !full_key_added { + target_actions.push(Action::AddKey(Box::new(AddKeyAction { + public_key: crate::key_mapping::EXTRA_KEY.public_key(), + access_key: AccessKey::full_access(), + }))); + } tracing::debug!( target: "mirror", "preparing {} for ({}, {}) with actions: {:?}", @@ -1283,19 +1295,20 @@ impl TxMirror { _ => {} }; } - if !key_added { - continue; - } - if provenance.is_create_account() && !account_created { - tracing::warn!( - target: "mirror", "for receipt {} predecessor and receiver are different but no create account in the actions: {:?}", - &receipt.receipt_id, &r.actions, - ); - } else if !provenance.is_create_account() && account_created { - tracing::warn!( - target: "mirror", "for receipt {} predecessor and receiver are the same but there's a create account in the actions: {:?}", - &receipt.receipt_id, &r.actions, - ); + if provenance.is_create_account() { + if !account_created { + continue; + } + } else { + if !key_added { + continue; + } + if account_created { + tracing::warn!( + target: "mirror", "for receipt {} predecessor and receiver are the same but there's a create account in the actions: {:?}", + &receipt.receipt_id, &r.actions, + ); + } } let outcome = self .source_chain_access @@ -1618,7 +1631,7 @@ impl TxMirror { &mut txs, predecessor_id, receiver_id.clone(), - &[Action::Stake(StakeAction { public_key, stake: 0 })], + &[Action::Stake(Box::new(StakeAction { public_key, stake: 0 }))], target_hash, MappedTxProvenance::Unstake(*target_hash), None, diff --git a/tools/state-parts/Cargo.toml b/tools/state-parts/Cargo.toml index 9b0d23dec9b..92a94007fd2 100644 --- a/tools/state-parts/Cargo.toml +++ b/tools/state-parts/Cargo.toml @@ -13,6 +13,7 @@ anyhow.workspace = true chrono.workspace = true clap.workspace = true once_cell.workspace = true +sha2 = "0.10.6" tokio.workspace = true tracing.workspace = true diff --git a/tools/state-parts/src/lib.rs b/tools/state-parts/src/lib.rs index d54764cdb4d..56f7c03ef41 100644 --- a/tools/state-parts/src/lib.rs +++ b/tools/state-parts/src/lib.rs @@ -1,14 +1,15 @@ use anyhow::Context; use near_async::time; -use near_network::raw::{ConnectError, Connection, Message, RoutedMessage}; +use near_network::raw::{ConnectError, Connection, DirectMessage, Message}; use near_network::types::HandshakeFailureReason; use near_primitives::hash::CryptoHash; use near_primitives::network::PeerId; use near_primitives::types::{AccountId, BlockHeight, ShardId}; use near_primitives::version::ProtocolVersion; +use sha2::Digest; +use sha2::Sha256; use std::collections::HashMap; use std::net::SocketAddr; - pub mod cli; struct AppInfo { @@ -27,7 +28,7 @@ fn handle_message( received_at: time::Instant, ) -> anyhow::Result<()> { match &msg { - Message::Routed(RoutedMessage::VersionedStateResponse(response)) => { + Message::Direct(DirectMessage::VersionedStateResponse(response)) => { let shard_id = response.shard_id(); let sync_hash = response.sync_hash(); let state_response = response.clone().take_state_response(); @@ -42,11 +43,21 @@ fn handle_message( } else { None }; + let part_hash = if let Some(part) = state_response.part() { + Sha256::digest(&part.1) + .iter() + .map(|byte| format!("{:02x}", byte)) + .collect::() + } else { + "No part".to_string() + }; + tracing::info!( shard_id, ?sync_hash, ?part_id, ?duration, + ?part_hash, "Received VersionedStateResponse" ); } @@ -129,9 +140,9 @@ async fn state_parts_from_node( tokio::select! { _ = &mut next_request => { let target = &peer_id; - let msg = RoutedMessage::StateRequestPart(shard_id, block_hash, part_id); + let msg = DirectMessage::StateRequestPart(shard_id, block_hash, part_id); tracing::info!(target: "state-parts", ?target, shard_id, ?block_hash, part_id, ttl, "Sending a request"); - result = peer.send_routed_message(msg, peer_id.clone(), ttl).await.with_context(|| format!("Failed sending State Part Request to {:?}", target)); + result = peer.send_message(msg).await.with_context(|| format!("Failed sending State Part Request to {:?}", target)); app_info.requests_sent.insert(part_id, time::Instant::now()); tracing::info!(target: "state-parts", ?result); if result.is_err() { diff --git a/tools/state-viewer/src/commands.rs b/tools/state-viewer/src/commands.rs index 5a7b766b534..00862e4fc9b 100644 --- a/tools/state-viewer/src/commands.rs +++ b/tools/state-viewer/src/commands.rs @@ -890,7 +890,6 @@ pub(crate) fn view_trie_leaves( Ok(()) } -#[allow(unused)] enum LoadTrieMode { /// Load latest state Latest, diff --git a/tools/state-viewer/src/contract_accounts.rs b/tools/state-viewer/src/contract_accounts.rs index 2fb38582b23..6d5cfadec86 100644 --- a/tools/state-viewer/src/contract_accounts.rs +++ b/tools/state-viewer/src/contract_accounts.rs @@ -174,8 +174,7 @@ impl ContractAccount { ) -> Result { let code = if filter.code_size { Some( - trie.storage - .retrieve_raw_bytes(&value_hash) + trie.retrieve_value(&value_hash) .map_err(|err| ContractAccountError::NoCode(err, account_id.clone()))?, ) } else { @@ -564,12 +563,12 @@ mod tests { let fn_call_receipt = create_receipt_with_actions( "alice.near", "bob.near", - vec![Action::FunctionCall(FunctionCallAction { + vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "foo".to_owned(), args: vec![], gas: 1000, deposit: 0, - })], + }))], ); // This is the receipt spawned, with the actions triggered by the diff --git a/tools/state-viewer/src/state_parts.rs b/tools/state-viewer/src/state_parts.rs index cd019cc397c..1f3da815048 100644 --- a/tools/state-viewer/src/state_parts.rs +++ b/tools/state-viewer/src/state_parts.rs @@ -394,7 +394,8 @@ async fn load_state_parts( fn print_state_part(state_root: &StateRoot, _part_id: PartId, data: &[u8]) { let trie_nodes: PartialState = BorshDeserialize::try_from_slice(data).unwrap(); - let trie = Trie::from_recorded_storage(PartialStorage { nodes: trie_nodes }, *state_root); + let trie = + Trie::from_recorded_storage(PartialStorage { nodes: trie_nodes }, *state_root, false); trie.print_recursive(&mut std::io::stdout().lock(), &state_root, u32::MAX); } @@ -473,7 +474,8 @@ async fn dump_state_parts( /// Returns the first `StateRecord` encountered while iterating over a sub-trie in the state part. fn get_first_state_record(state_root: &StateRoot, data: &[u8]) -> Option { let trie_nodes = BorshDeserialize::try_from_slice(data).unwrap(); - let trie = Trie::from_recorded_storage(PartialStorage { nodes: trie_nodes }, *state_root); + let trie = + Trie::from_recorded_storage(PartialStorage { nodes: trie_nodes }, *state_root, false); for (key, value) in trie.iter().unwrap().flatten() { if let Some(sr) = StateRecord::from_raw_key_value(key, value) {