From 9c2e9cbd7c127fcb881d406fbf152bbe87ed10fe Mon Sep 17 00:00:00 2001 From: pompon0 Date: Thu, 17 Nov 2022 17:07:12 +0100 Subject: [PATCH 001/188] deflaked fix_local_edges test (#8079) Removed a flake of fix_local_edges test that I managed to reproduce locally. The event ConnectionClosed was being emitted too early: it should be sent only once the asynchronous call to unregister completes. --- chain/network/src/peer/peer_actor.rs | 47 ++++++++++++------- .../src/peer_manager/network_state/mod.rs | 14 ++++-- chain/network/src/peer_manager/tests/nonce.rs | 2 +- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index 0a32a294a2d..2a03727a612 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -92,6 +92,8 @@ pub(crate) enum ClosingReason { PeerManager, #[error("Received DisconnectMessage from peer")] DisconnectMessage, + #[error("PeerActor stopped NOT via PeerActor::stop()")] + Unknown, } pub(crate) struct PeerActor { @@ -1143,36 +1145,47 @@ impl actix::Actor for PeerActor { } fn stopping(&mut self, _: &mut Self::Context) -> Running { + Running::Stop + } + + fn stopped(&mut self, _ctx: &mut Self::Context) { + // closing_reason may be None in case the whole actix system is stopped. + // It happens a lot in tests. metrics::PEER_CONNECTIONS_TOTAL.dec(); debug!(target: "network", "{:?}: [status = {:?}] Peer {} disconnected.", self.my_node_info.id, self.peer_status, self.peer_info); + if self.closing_reason.is_none() { + // Due to Actix semantics, sometimes closing reason may be not set. + // But it is only expected to happen in tests. + tracing::error!(target:"network", "closing reason not set. This should happen only in tests."); + } match &self.peer_status { // If PeerActor is in Connecting state, then // it was not registered in the NewtorkState, // so there is nothing to be done. - PeerStatus::Connecting(..) => {} + PeerStatus::Connecting(..) => { + // TODO(gprusak): reporting ConnectionClosed event is quite scattered right now and + // it is very ugly: it may happen here, in spawn_inner, or in NetworkState::unregister(). + // Centralize it, once we get rid of actix. + self.network_state.config.event_sink.push(Event::ConnectionClosed( + ConnectionClosedEvent { + stream_id: self.stream_id, + reason: self.closing_reason.clone().unwrap_or(ClosingReason::Unknown), + }, + )); + } // Clean up the Connection from the NetworkState. PeerStatus::Ready(conn) => { let network_state = self.network_state.clone(); let clock = self.clock.clone(); let conn = conn.clone(); - let ban_reason = match self.closing_reason { - Some(ClosingReason::Ban(reason)) => Some(reason), - _ => None, - }; - network_state.unregister(&clock, &conn, ban_reason); + network_state.unregister( + &clock, + &conn, + self.stream_id, + self.closing_reason.clone().unwrap_or(ClosingReason::Unknown), + ); } } - Running::Stop - } - - fn stopped(&mut self, _ctx: &mut Self::Context) { - // closing_reason may be None in case the whole actix system is stopped. - // It happens a lot in tests. - if let Some(reason) = self.closing_reason.take() { - self.network_state.config.event_sink.push(Event::ConnectionClosed( - ConnectionClosedEvent { stream_id: self.stream_id, reason }, - )); - } actix::Arbiter::current().stop(); } } diff --git a/chain/network/src/peer_manager/network_state/mod.rs b/chain/network/src/peer_manager/network_state/mod.rs index c7d814b88cb..1de33f4a32c 100644 --- a/chain/network/src/peer_manager/network_state/mod.rs +++ b/chain/network/src/peer_manager/network_state/mod.rs @@ -6,6 +6,7 @@ use crate::network_protocol::{ Edge, EdgeState, PartialEdgeInfo, PeerIdOrHash, PeerInfo, PeerMessage, Ping, Pong, RawRoutedMessage, RoutedMessageBody, RoutedMessageV2, RoutingTableUpdate, SignedAccountData, }; +use crate::peer::peer_actor::{ClosingReason, ConnectionClosedEvent}; use crate::peer_manager::connection; use crate::peer_manager::peer_manager_actor::Event; use crate::peer_manager::peer_store; @@ -14,6 +15,7 @@ use crate::routing; use crate::routing::routing_table_view::RoutingTableView; use crate::stats::metrics; use crate::store; +use crate::tcp; use crate::time; use crate::types::{ChainInfo, PeerType, ReasonForBan}; use arc_swap::ArcSwap; @@ -306,7 +308,8 @@ impl NetworkState { self: &Arc, clock: &time::Clock, conn: &Arc, - ban_reason: Option, + stream_id: tcp::StreamId, + reason: ClosingReason, ) { let this = self.clone(); let clock = clock.clone(); @@ -326,15 +329,18 @@ impl NetworkState { } // Save the fact that we are disconnecting to the PeerStore. - let res = match ban_reason { - Some(ban_reason) => { + let res = match reason { + ClosingReason::Ban(ban_reason) => { this.peer_store.peer_ban(&clock, &conn.peer_info.id, ban_reason) } - None => this.peer_store.peer_disconnected(&clock, &conn.peer_info.id), + _ => this.peer_store.peer_disconnected(&clock, &conn.peer_info.id), }; if let Err(err) = res { tracing::error!(target: "network", ?err, "Failed to save peer data"); } + this.config + .event_sink + .push(Event::ConnectionClosed(ConnectionClosedEvent { stream_id, reason })); }); } diff --git a/chain/network/src/peer_manager/tests/nonce.rs b/chain/network/src/peer_manager/tests/nonce.rs index 59e6ecc3385..1e6b18a4a32 100644 --- a/chain/network/src/peer_manager/tests/nonce.rs +++ b/chain/network/src/peer_manager/tests/nonce.rs @@ -57,7 +57,7 @@ async fn test_nonces() { ]; for test in test_cases { - println!("Running test {:?}", test.2); + tracing::info!(target: "test", "Running test {:?}", test.2); let cfg = peer::testonly::PeerConfig { network: chain.make_config(rng), chain: chain.clone(), From be239bb72c9d61614565ce843e1a42ca78ed128d Mon Sep 17 00:00:00 2001 From: Alex Kladov Date: Thu, 17 Nov 2022 16:26:08 +0000 Subject: [PATCH 002/188] Fix typo (#8080) --- docs/practices/workflows/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/practices/workflows/README.md b/docs/practices/workflows/README.md index ab348153b0a..ec59d47388c 100644 --- a/docs/practices/workflows/README.md +++ b/docs/practices/workflows/README.md @@ -1,3 +1,3 @@ -= Workflows +# Workflows This chapter documents various way you can run `neard` during development: running a local net, joining a test net, doing benchmarking and load testing. From fdd55774348cc1e42b487db130318fddb295e329 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Thu, 17 Nov 2022 17:54:07 +0100 Subject: [PATCH 003/188] docs: document the process for tracking security sensitive issues (#8078) https://docs.google.com/document/d/14w1Sdk1wXBTJIuzEksl1U1X_tMtvjn3fwdaeP_mIdPE/edit is the internal document being exported here. --- docs/SUMMARY.md | 1 + docs/practices/security_vulnerabilities.md | 46 ++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 docs/practices/security_vulnerabilities.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index c2588ff8ee0..a6c0d0a890a 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -31,6 +31,7 @@ - [Code Style](./practices/style.md) - [Documentation](./practices/docs.md) - [Tracking Issues](./practices/tracking_issues.md) +- [Security Vulnerabilities](./practices/security_vulnerabilities.md) - [Fast Builds](./practices/fast_builds.md) - [Testing](./practices/testing/README.md) - [Python Tests](./practices/testing/python_tests.md) diff --git a/docs/practices/security_vulnerabilities.md b/docs/practices/security_vulnerabilities.md new file mode 100644 index 00000000000..4697bab3ef5 --- /dev/null +++ b/docs/practices/security_vulnerabilities.md @@ -0,0 +1,46 @@ +## Security Vulnerabilities + +
+The intended audience of the information presented here are developers working +on the implementation of NEAR. + +Are you a security researcher? Please report security vulnerabilities to +security@near.org. +
+ +As nearcore is open source, all of its issues and pull requests are also +publicly tracked on github. However, from time to time, if a security sensitive +issue is discovered, those cannot be tracked publicly on github. However, we +should promote as similar a development process to work on such issues as +possible. To enable this, below is the high level process for working on +security sensitive issues. + +1. There is a [private fork of + nearcore](https://github.com/near/nearcore-private) on github. Access to + this repository is restricted to the set of people who are trusted to work on + and have knowledge about security sensitive issues pertaining to nearcore. + + This repository can be manually synced with the public nearcore repository + using the following commands: + + ```console + $ git remote add nearcore-public git@github.com:near/nearcore + $ git remote add nearcore-private git@github.com:near/nearcore-private + $ git fetch nearcore-public + $ git push nearcore-private nearcore-public/master:master + ``` +2. All security sensitive issues must be created on the private nearcore + repository. You must also assign one of the `[P-S0, P-S1]` labels to the + issue to indicate the severity of the issue. The two criteria to use to help + you judge the severity are ease of carrying out the attack and the impact of + the attack. An attack that is easy to do or can have a huge impact should + have the `P-S0` label and `P-S1` otherwise. + +3. All security sensitive pull requests should also be created on the private + nearcore repository. Note that once a PR has been approved, it should not be + merged into the private repository. Instead it should be first merged into + the public repository and then the private fork should be updated using the + steps above. + +4. Once work on a security issue is finished, it needs to be deployed to all the + impacted networks. Please contact the node team for help with this. From 099aa532bddc89a6f1b4f83c532d6fe6d2c756eb Mon Sep 17 00:00:00 2001 From: robin-near <111538878+robin-near@users.noreply.github.com> Date: Thu, 17 Nov 2022 09:34:56 -0800 Subject: [PATCH 004/188] Improve consensus performance when there are invalid chunks. (#8060) --- chain/chain/src/block_processing_utils.rs | 3 +- chain/chain/src/chain.rs | 64 +++- chain/chain/src/tests/challenges.rs | 1 + chain/chunks/src/client.rs | 19 +- chain/chunks/src/lib.rs | 10 +- chain/chunks/src/test_utils.rs | 2 +- chain/client-primitives/src/debug.rs | 3 + chain/client/src/client.rs | 147 +++++++- chain/client/src/client_actor.rs | 12 +- chain/client/src/debug.rs | 22 +- chain/client/src/metrics.rs | 18 + chain/client/src/test_utils.rs | 55 ++- chain/jsonrpc/res/validator.html | 14 +- .../src/tests/client/features.rs | 1 + .../client/features/adversarial_behaviors.rs | 353 ++++++++++++++++++ .../src/tests/client/process_blocks.rs | 3 +- .../src/tests/client/shards_manager.rs | 2 +- 17 files changed, 652 insertions(+), 77 deletions(-) create mode 100644 integration-tests/src/tests/client/features/adversarial_behaviors.rs diff --git a/chain/chain/src/block_processing_utils.rs b/chain/chain/src/block_processing_utils.rs index a5211537e8e..ec2f6859f2a 100644 --- a/chain/chain/src/block_processing_utils.rs +++ b/chain/chain/src/block_processing_utils.rs @@ -4,7 +4,7 @@ use crate::Provenance; use near_primitives::block::Block; use near_primitives::challenge::{ChallengeBody, ChallengesResult}; use near_primitives::hash::CryptoHash; -use near_primitives::sharding::{ReceiptProof, StateSyncInfo}; +use near_primitives::sharding::{ReceiptProof, ShardChunkHeader, StateSyncInfo}; use near_primitives::types::ShardId; use once_cell::sync::OnceCell; use std::collections::HashMap; @@ -64,6 +64,7 @@ pub struct BlockProcessingArtifact { pub orphans_missing_chunks: Vec, pub blocks_missing_chunks: Vec, pub challenges: Vec, + pub invalid_chunks: Vec, } /// This struct defines the callback function that will be called after apply chunks are finished diff --git a/chain/chain/src/chain.rs b/chain/chain/src/chain.rs index 467e1a76692..6929a992c59 100644 --- a/chain/chain/src/chain.rs +++ b/chain/chain/src/chain.rs @@ -130,7 +130,7 @@ const NEAR_BASE: Balance = 1_000_000_000_000_000_000_000_000; /// has not been caught up yet, thus the two modes IsCaughtUp and NotCaughtUp. /// CatchingUp is for when apply_chunks is called through catchup_blocks, this is to catch up the /// shard states for the next epoch -#[derive(Eq, PartialEq)] +#[derive(Eq, PartialEq, Copy, Clone, Debug)] enum ApplyChunksMode { IsCaughtUp, CatchingUp, @@ -1937,6 +1937,7 @@ impl Chain { &block, &provenance, &mut block_processing_artifact.challenges, + &mut block_processing_artifact.invalid_chunks, block_received_time, state_patch, ); @@ -2113,6 +2114,14 @@ impl Chain { let prev_head = self.store.head()?; let provenance = block_preprocess_info.provenance.clone(); let block_start_processing_time = block_preprocess_info.block_start_processing_time.clone(); + // TODO(#8055): this zip relies on the ordering of the apply_results. + for (apply_result, chunk) in apply_results.iter().zip(block.chunks().iter()) { + if let Err(err) = apply_result { + if err.is_bad_data() { + block_processing_artifacts.invalid_chunks.push(chunk.clone()); + } + } + } let new_head = match self.postprocess_block_only(me, &block, block_preprocess_info, apply_results) { Err(err) => { @@ -2235,6 +2244,7 @@ impl Chain { block: &MaybeValidated, provenance: &Provenance, challenges: &mut Vec, + invalid_chunks: &mut Vec, block_received_time: Instant, state_patch: SandboxStatePatch, ) -> Result< @@ -2399,6 +2409,7 @@ impl Chain { // otherwise put the block into the permanent storage, waiting for be caught up if is_caught_up { ApplyChunksMode::IsCaughtUp } else { ApplyChunksMode::NotCaughtUp }, state_patch, + invalid_chunks, )?; Ok(( @@ -3241,6 +3252,7 @@ impl Chain { &receipts_by_shard, ApplyChunksMode::CatchingUp, Default::default(), + &mut Vec::new(), )?; blocks_catch_up_state.scheduled_blocks.insert(pending_block); block_catch_up_scheduler(BlockCatchUpRequest { @@ -3571,14 +3583,12 @@ impl Chain { incoming_receipts: &HashMap>, mode: ApplyChunksMode, mut state_patch: SandboxStatePatch, + invalid_chunks: &mut Vec, ) -> Result< Vec Result + Send + 'static>>, Error, > { let _span = tracing::debug_span!(target: "chain", "apply_chunks_preprocessing").entered(); - let mut result: Vec< - Box Result + Send + 'static>, - > = Vec::new(); #[cfg(not(feature = "mock_node"))] let protocol_version = self.runtime_adapter.get_epoch_protocol_version(block.header().epoch_id())?; @@ -3587,9 +3597,13 @@ impl Chain { let will_shard_layout_change = self.runtime_adapter.will_shard_layout_change_next_epoch(prev_hash)?; let prev_chunk_headers = Chain::get_prev_chunk_headers(&*self.runtime_adapter, prev_block)?; - for (shard_id, (chunk_header, prev_chunk_header)) in - (block.chunks().iter().zip(prev_chunk_headers.iter())).enumerate() - { + 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(); @@ -3747,7 +3761,7 @@ impl Chain { let height = chunk_header.height_included(); let prev_block_hash = chunk_header.prev_block_hash().clone(); - result.push(Box::new(move |parent_span| -> Result { + Ok(Some(Box::new(move |parent_span| -> Result { let _span = tracing::debug_span!( target: "chain", parent: parent_span, @@ -3796,7 +3810,7 @@ impl Chain { } Err(err) => Err(err), } - })); + }))) } else { let new_extra = self.get_chunk_extra(prev_block.hash(), &shard_uid)?.clone(); @@ -3809,7 +3823,7 @@ impl Chain { let height = block.header().height(); let prev_block_hash = *prev_block.hash(); - result.push(Box::new(move |parent_span| -> Result { + Ok(Some(Box::new(move |parent_span| -> Result { let _span = tracing::debug_span!( target: "chain", parent: parent_span, @@ -3856,7 +3870,7 @@ impl Chain { } Err(err) => Err(err), } - })); + }))) } } else if let Some(split_state_roots) = split_state_roots { // case 3) @@ -3869,7 +3883,7 @@ impl Chain { self.store().get_state_changes_for_split_states(block.hash(), shard_id)?; let runtime_adapter = self.runtime_adapter.clone(); let block_hash = *block.hash(); - result.push(Box::new(move |parent_span| -> Result { + Ok(Some(Box::new(move |parent_span| -> Result { let _span = tracing::debug_span!( target: "chain", parent: parent_span, @@ -3886,11 +3900,29 @@ impl Chain { state_changes, )?, })) - })); + }))) + } else { + Ok(None) } - } - - Ok(result) + }; + 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() } } diff --git a/chain/chain/src/tests/challenges.rs b/chain/chain/src/tests/challenges.rs index 84b47cd7ea9..885eac89c76 100644 --- a/chain/chain/src/tests/challenges.rs +++ b/chain/chain/src/tests/challenges.rs @@ -52,6 +52,7 @@ fn challenges_new_head_prev() { &MaybeValidated::from(last_block), &Provenance::NONE, &mut vec![], + &mut vec![], Clock::instant(), Default::default(), ) { diff --git a/chain/chunks/src/client.rs b/chain/chunks/src/client.rs index 0fa27d7d22f..72824875d29 100644 --- a/chain/chunks/src/client.rs +++ b/chain/chunks/src/client.rs @@ -8,7 +8,7 @@ use near_primitives::{ epoch_manager::RngSeed, sharding::{EncodedShardChunk, PartialEncodedChunk, ShardChunk, ShardChunkHeader}, transaction::SignedTransaction, - types::ShardId, + types::{AccountId, ShardId}, }; pub trait ClientAdapterForShardsManager { @@ -18,7 +18,11 @@ pub trait ClientAdapterForShardsManager { shard_chunk: Option, ); fn saw_invalid_chunk(&self, chunk: EncodedShardChunk); - fn chunk_header_ready_for_inclusion(&self, chunk_header: ShardChunkHeader); + fn chunk_header_ready_for_inclusion( + &self, + chunk_header: ShardChunkHeader, + chunk_producer: AccountId, + ); } #[derive(Message)] @@ -26,7 +30,7 @@ pub trait ClientAdapterForShardsManager { pub enum ShardsManagerResponse { ChunkCompleted { partial_chunk: PartialEncodedChunk, shard_chunk: Option }, InvalidChunk(EncodedShardChunk), - ChunkHeaderReadyForInclusion(ShardChunkHeader), + ChunkHeaderReadyForInclusion { chunk_header: ShardChunkHeader, chunk_producer: AccountId }, } impl>> ClientAdapterForShardsManager for A { @@ -43,9 +47,14 @@ impl>> ClientAdapterForSh fn saw_invalid_chunk(&self, chunk: EncodedShardChunk) { self.do_send(ShardsManagerResponse::InvalidChunk(chunk).with_span_context()); } - fn chunk_header_ready_for_inclusion(&self, chunk_header: ShardChunkHeader) { + fn chunk_header_ready_for_inclusion( + &self, + chunk_header: ShardChunkHeader, + chunk_producer: AccountId, + ) { self.do_send( - ShardsManagerResponse::ChunkHeaderReadyForInclusion(chunk_header).with_span_context(), + ShardsManagerResponse::ChunkHeaderReadyForInclusion { chunk_header, chunk_producer } + .with_span_context(), ); } } diff --git a/chain/chunks/src/lib.rs b/chain/chunks/src/lib.rs index ae9067416e4..02534b18bcf 100644 --- a/chain/chunks/src/lib.rs +++ b/chain/chunks/src/lib.rs @@ -975,11 +975,12 @@ impl ShardsManager { .collect() } + /// Returns true if we were able to answer the request. This is for testing. pub fn process_partial_encoded_chunk_request( &mut self, request: PartialEncodedChunkRequestMsg, route_back: CryptoHash, - ) { + ) -> bool { let _span = tracing::debug_span!( target: "chunks", "process_partial_encoded_chunk_request", @@ -1005,8 +1006,10 @@ impl ShardsManager { NetworkRequests::PartialEncodedChunkResponse { route_back, response }, ) .with_span_context(), - ) + ); + return true; } + false } fn prepare_partial_encoded_chunk_response( @@ -1799,7 +1802,8 @@ impl ShardsManager { if have_all_parts && self.seals_mgr.should_trust_chunk_producer(&chunk_producer) { if self.encoded_chunks.mark_chunk_for_inclusion(&chunk_hash) { - self.client_adapter.chunk_header_ready_for_inclusion(header.clone()); + self.client_adapter + .chunk_header_ready_for_inclusion(header.clone(), chunk_producer); } } // we can safely unwrap here because we already checked that chunk_hash exist in encoded_chunks diff --git a/chain/chunks/src/test_utils.rs b/chain/chunks/src/test_utils.rs index e2717b2f51d..157c422ee07 100644 --- a/chain/chunks/src/test_utils.rs +++ b/chain/chunks/src/test_utils.rs @@ -332,7 +332,7 @@ impl ChunkTestFixture { pub fn count_chunk_ready_for_inclusion_messages(&self) -> usize { let mut chunks_ready = 0; while let Some(message) = self.mock_client_adapter.pop() { - if let ShardsManagerResponse::ChunkHeaderReadyForInclusion(_) = message { + if let ShardsManagerResponse::ChunkHeaderReadyForInclusion { .. } = message { chunks_ready += 1; } } diff --git a/chain/client-primitives/src/debug.rs b/chain/client-primitives/src/debug.rs index d5d52bb5065..62139c02005 100644 --- a/chain/client-primitives/src/debug.rs +++ b/chain/client-primitives/src/debug.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use crate::types::StatusError; use actix::Message; use chrono::DateTime; +use near_primitives::types::EpochId; use near_primitives::views::{ CatchupStatusView, ChainProcessingInfo, EpochValidatorInfo, RequestedStatePartsView, SyncStatusView, @@ -165,6 +166,8 @@ pub struct ValidatorStatus { // Sorted by block height inversely (high to low) // The range of heights are controlled by constants in client_actor.rs pub production: Vec<(BlockHeight, ProductionAtHeight)>, + // Chunk producers that this node has banned. + pub banned_chunk_producers: Vec<(EpochId, Vec)>, } // Different debug requests that can be sent by HTML pages, via GET. diff --git a/chain/client/src/client.rs b/chain/client/src/client.rs index 3ec8fb736d7..e929513e89a 100644 --- a/chain/client/src/client.rs +++ b/chain/client/src/client.rs @@ -62,6 +62,7 @@ use near_primitives::views::{CatchupStatusView, DroppedReason}; const NUM_REBROADCAST_BLOCKS: usize = 30; const CHUNK_HEADERS_FOR_INCLUSION_CACHE_SIZE: usize = 2048; +const NUM_EPOCH_CHUNK_PRODUCERS_TO_KEEP_IN_BLOCKLIST: usize = 1000; /// The time we wait for the response to a Epoch Sync request before retrying // TODO #3488 set 30_000 @@ -81,6 +82,10 @@ pub struct Client { pub adv_produce_blocks: bool, #[cfg(feature = "test_features")] pub adv_produce_blocks_only_valid: bool, + #[cfg(feature = "test_features")] + pub produce_invalid_chunks: bool, + #[cfg(feature = "test_features")] + pub produce_invalid_tx_in_chunks: bool, /// Fast Forward accrued delta height used to calculate fast forwarded timestamps for each block. #[cfg(feature = "sandbox")] @@ -94,8 +99,11 @@ pub struct Client { pub shards_mgr: ShardsManager, me: Option, pub sharded_tx_pool: ShardedTransactionPool, - prev_block_to_chunk_headers_ready_for_inclusion: - LruCache)>>, + prev_block_to_chunk_headers_ready_for_inclusion: LruCache< + CryptoHash, + HashMap, AccountId)>, + >, + pub do_not_include_chunks_from: LruCache<(EpochId, AccountId), ()>, /// Network adapter. network_adapter: Arc, /// Signer for block producer (if present). @@ -237,6 +245,10 @@ impl Client { adv_produce_blocks: false, #[cfg(feature = "test_features")] adv_produce_blocks_only_valid: false, + #[cfg(feature = "test_features")] + produce_invalid_chunks: false, + #[cfg(feature = "test_features")] + produce_invalid_tx_in_chunks: false, #[cfg(feature = "sandbox")] accrued_fastforward_delta: 0, config, @@ -250,6 +262,9 @@ impl Client { prev_block_to_chunk_headers_ready_for_inclusion: LruCache::new( CHUNK_HEADERS_FOR_INCLUSION_CACHE_SIZE, ), + do_not_include_chunks_from: LruCache::new( + NUM_EPOCH_CHUNK_PRODUCERS_TO_KEEP_IN_BLOCKLIST, + ), network_adapter, validator_signer, pending_approvals: lru::LruCache::new(num_block_producer_seats), @@ -408,19 +423,49 @@ impl Client { pub fn get_chunk_headers_ready_for_inclusion( &self, + epoch_id: &EpochId, prev_block_hash: &CryptoHash, - ) -> HashMap)> { + ) -> HashMap, AccountId)> { self.prev_block_to_chunk_headers_ready_for_inclusion .peek(prev_block_hash) .cloned() .unwrap_or_default() + .into_iter() + .filter(|(_, (chunk_header, _, chunk_producer))| { + let banned = self + .do_not_include_chunks_from + .contains(&(epoch_id.clone(), chunk_producer.clone())); + if banned { + warn!( + target: "client", + "Not including chunk {:?} from banned validator {}", + chunk_header.chunk_hash(), + chunk_producer); + metrics::CHUNK_DROPPED_BECAUSE_OF_BANNED_CHUNK_PRODUCER.inc(); + } + !banned + }) + .collect() } - pub fn get_num_chunks_ready_for_inclusion(&self, prev_block_hash: &CryptoHash) -> usize { - self.prev_block_to_chunk_headers_ready_for_inclusion - .peek(prev_block_hash) - .map(|x| x.len()) - .unwrap_or(0) + pub fn num_chunk_headers_ready_for_inclusion( + &self, + epoch_id: &EpochId, + prev_block_hash: &CryptoHash, + ) -> usize { + let entries = + match self.prev_block_to_chunk_headers_ready_for_inclusion.peek(prev_block_hash) { + Some(entries) => entries, + None => return 0, + }; + entries + .values() + .filter(|(_, _, chunk_producer)| { + !self + .do_not_include_chunks_from + .contains(&(epoch_id.clone(), chunk_producer.clone())) + }) + .count() } /// Produce block if we are block producer for given `next_height` block height. @@ -485,7 +530,7 @@ impl Client { } } - let new_chunks = self.get_chunk_headers_ready_for_inclusion(&prev_hash); + let new_chunks = self.get_chunk_headers_ready_for_inclusion(&epoch_id, &prev_hash); debug!(target: "client", "{:?} Producing block at height {}, parent {} @ {}, {} new chunks", validator_signer.validator_id(), next_height, prev.height(), format_hash(head.last_block_hash), new_chunks.len()); @@ -577,7 +622,7 @@ impl Client { ); // Collect new chunks. - for (shard_id, (mut chunk_header, _)) in new_chunks { + for (shard_id, (mut chunk_header, _, _)) in new_chunks { *chunk_header.height_included_mut() = next_height; chunks[shard_id as usize] = chunk_header; } @@ -700,6 +745,13 @@ impl Client { let prev_block_header = self.chain.get_block_header(&prev_block_hash)?; let transactions = self.prepare_transactions(shard_id, &chunk_extra, &prev_block_header)?; + let transactions = transactions; + #[cfg(feature = "test_features")] + let transactions = Self::maybe_insert_invalid_transaction( + transactions, + prev_block_hash, + self.produce_invalid_tx_in_chunks, + ); let num_filtered_transactions = transactions.len(); let (tx_root, _) = merklize(&transactions); let outgoing_receipts = self.chain.get_outgoing_receipts_for_shard( @@ -726,13 +778,16 @@ impl Client { let (outgoing_receipts_root, _) = merklize(&outgoing_receipts_hashes); let protocol_version = self.runtime_adapter.get_epoch_protocol_version(epoch_id)?; + let gas_used = chunk_extra.gas_used(); + #[cfg(feature = "test_features")] + let gas_used = if self.produce_invalid_chunks { gas_used + 1 } else { gas_used }; let (encoded_chunk, merkle_paths) = ShardsManager::create_encoded_shard_chunk( prev_block_hash, *chunk_extra.state_root(), *chunk_extra.outcome_root(), next_height, shard_id, - chunk_extra.gas_used(), + gas_used, chunk_extra.gas_limit(), chunk_extra.balance_burnt(), chunk_extra.validator_proposals().collect(), @@ -768,6 +823,27 @@ impl Client { Ok(Some((encoded_chunk, merkle_paths, outgoing_receipts))) } + #[cfg(feature = "test_features")] + fn maybe_insert_invalid_transaction( + mut txs: Vec, + prev_block_hash: CryptoHash, + insert: bool, + ) -> Vec { + if insert { + txs.push(SignedTransaction::new( + near_crypto::Signature::empty(near_crypto::KeyType::ED25519), + near_primitives::transaction::Transaction::new( + "test".parse().unwrap(), + near_crypto::PublicKey::empty(near_crypto::KeyType::SECP256K1), + "other".parse().unwrap(), + 3, + prev_block_hash, + ), + )); + } + txs + } + /// Prepares an ordered list of valid transactions from the pool up the limits. fn prepare_transactions( &mut self, @@ -1097,8 +1173,12 @@ impl Client { &mut self, block_processing_artifacts: BlockProcessingArtifact, ) { - let BlockProcessingArtifact { orphans_missing_chunks, blocks_missing_chunks, challenges } = - block_processing_artifacts; + let BlockProcessingArtifact { + orphans_missing_chunks, + blocks_missing_chunks, + challenges, + invalid_chunks, + } = block_processing_artifacts; // Send out challenges that accumulated via on_challenge. self.send_challenges(challenges); // For any missing chunk, call the ShardsManager with it so that it may apply forwarded parts. @@ -1117,6 +1197,34 @@ impl Client { } // Request any (still) missing chunks. self.request_missing_chunks(blocks_missing_chunks, orphans_missing_chunks); + + for chunk_header in invalid_chunks { + if let Err(err) = self.ban_chunk_producer_for_producing_invalid_chunk(chunk_header) { + error!(target: "client", "Failed to ban chunk producer for producing invalid chunk: {:?}", err); + } + } + } + + fn ban_chunk_producer_for_producing_invalid_chunk( + &mut self, + chunk_header: ShardChunkHeader, + ) -> Result<(), Error> { + let epoch_id = + self.runtime_adapter.get_epoch_id_from_prev_block(chunk_header.prev_block_hash())?; + let chunk_producer = self.runtime_adapter.get_chunk_producer( + &epoch_id, + chunk_header.height_created(), + chunk_header.shard_id(), + )?; + error!( + target: "client", + "Banning chunk producer {} for epoch {:?} for producing invalid chunk {:?}", + chunk_producer, + epoch_id, + chunk_header.chunk_hash()); + metrics::CHUNK_PRODUCER_BANNED_FOR_EPOCH.inc(); + self.do_not_include_chunks_from.put((epoch_id, chunk_producer), ()); + Ok(()) } fn rebroadcast_block(&mut self, block: &Block) { @@ -1158,14 +1266,18 @@ impl Client { } } - pub fn on_chunk_header_ready_for_inclusion(&mut self, chunk_header: ShardChunkHeader) { + pub fn on_chunk_header_ready_for_inclusion( + &mut self, + chunk_header: ShardChunkHeader, + chunk_producer: AccountId, + ) { let prev_block_hash = chunk_header.prev_block_hash(); self.prev_block_to_chunk_headers_ready_for_inclusion .get_or_insert(prev_block_hash.clone(), || HashMap::new()); self.prev_block_to_chunk_headers_ready_for_inclusion .get_mut(prev_block_hash) .unwrap() - .insert(chunk_header.shard_id(), (chunk_header, chrono::Utc::now())); + .insert(chunk_header.shard_id(), (chunk_header, chrono::Utc::now(), chunk_producer)); } pub fn sync_block_headers( @@ -1468,7 +1580,10 @@ impl Client { self.runtime_adapter.as_ref(), )?; persist_chunk(partial_chunk.clone(), Some(shard_chunk), self.chain.mut_store())?; - self.on_chunk_header_ready_for_inclusion(encoded_chunk.cloned_header()); + self.on_chunk_header_ready_for_inclusion( + encoded_chunk.cloned_header(), + self.me.clone().unwrap(), + ); self.shards_mgr.distribute_encoded_chunk( partial_chunk, encoded_chunk, diff --git a/chain/client/src/client_actor.rs b/chain/client/src/client_actor.rs index d9d87a8eb2f..e3c1aa55693 100644 --- a/chain/client/src/client_actor.rs +++ b/chain/client/src/client_actor.rs @@ -1152,8 +1152,9 @@ impl ClientActor { self.client.runtime_adapter.get_block_producer(&epoch_id, height)?; if me == next_block_producer_account { - let num_chunks = - self.client.get_num_chunks_ready_for_inclusion(&head.last_block_hash); + let num_chunks = self + .client + .num_chunk_headers_ready_for_inclusion(&epoch_id, &head.last_block_hash); let have_all_chunks = head.height == 0 || num_chunks as u64 == self.client.runtime_adapter.num_shards(&epoch_id).unwrap(); @@ -2042,8 +2043,11 @@ impl Handler> for ClientActor { ShardsManagerResponse::InvalidChunk(encoded_chunk) => { self.client.on_invalid_chunk(encoded_chunk); } - ShardsManagerResponse::ChunkHeaderReadyForInclusion(chunk_header) => { - self.client.on_chunk_header_ready_for_inclusion(chunk_header); + ShardsManagerResponse::ChunkHeaderReadyForInclusion { + chunk_header, + chunk_producer, + } => { + self.client.on_chunk_header_ready_for_inclusion(chunk_header, chunk_producer); } } } diff --git a/chain/client/src/debug.rs b/chain/client/src/debug.rs index f338d64db04..dbd06d25ad6 100644 --- a/chain/client/src/debug.rs +++ b/chain/client/src/debug.rs @@ -3,6 +3,7 @@ use crate::ClientActor; use actix::{Context, Handler}; use borsh::BorshSerialize; +use itertools::Itertools; use near_chain::crypto_hash_timer::CryptoHashTimer; use near_chain::{near_chain_primitives, ChainStoreAccess, RuntimeAdapter}; use near_client_primitives::debug::{ @@ -113,19 +114,14 @@ impl BlockProductionTracker { block_height: BlockHeight, epoch_id: &EpochId, num_shards: ShardId, - new_chunks: &HashMap)>, + new_chunks: &HashMap, AccountId)>, runtime_adapter: &dyn RuntimeAdapter, ) -> Result, Error> { let mut chunk_collection_info = vec![]; for shard_id in 0..num_shards { - if let Some((new_chunk, chunk_time)) = new_chunks.get(&shard_id) { - let chunk_producer = runtime_adapter.get_chunk_producer( - epoch_id, - new_chunk.height_created(), - shard_id, - )?; + if let Some((_, chunk_time, chunk_producer)) = new_chunks.get(&shard_id) { chunk_collection_info.push(ChunkCollection { - chunk_producer, + chunk_producer: chunk_producer.clone(), received_time: Some(chunk_time.clone()), chunk_included: true, }); @@ -618,6 +614,16 @@ impl ClientActor { shards: self.client.runtime_adapter.num_shards(&head.epoch_id).unwrap_or_default(), approval_history: self.client.doomslug.get_approval_history(), production: productions, + banned_chunk_producers: self + .client + .do_not_include_chunks_from + .iter() + .map(|(k, _)| k.clone()) + .sorted() + .group_by(|(k, _)| k.clone()) + .into_iter() + .map(|(k, vs)| (k, vs.map(|(_, v)| v).collect())) + .collect(), }) } } diff --git a/chain/client/src/metrics.rs b/chain/client/src/metrics.rs index 9949033e2c0..09231d21d60 100644 --- a/chain/client/src/metrics.rs +++ b/chain/client/src/metrics.rs @@ -151,6 +151,24 @@ pub(crate) static CHUNK_SKIPPED_TOTAL: Lazy = Lazy::new(|| { .unwrap() }); +pub(crate) static CHUNK_PRODUCER_BANNED_FOR_EPOCH: Lazy = Lazy::new(|| { + try_create_int_counter( + "near_chunk_producer_banned_for_epoch", + "Number of times we have banned a chunk producer for an epoch", + ) + .unwrap() +}); + +pub(crate) static CHUNK_DROPPED_BECAUSE_OF_BANNED_CHUNK_PRODUCER: Lazy = + Lazy::new(|| { + try_create_int_counter( + "near_chunk_dropped_because_of_banned_chunk_producer", + "Number of chunks we, as a block producer, + dropped, because the chunk is produced by a banned chunk producer", + ) + .unwrap() + }); + pub(crate) static PARTIAL_ENCODED_CHUNK_RESPONSE_DELAY: Lazy = Lazy::new(|| { try_create_histogram( "near_partial_encoded_chunk_response_delay", diff --git a/chain/client/src/test_utils.rs b/chain/client/src/test_utils.rs index ea2c56c7b29..814e97ef9c0 100644 --- a/chain/client/src/test_utils.rs +++ b/chain/client/src/test_utils.rs @@ -11,7 +11,7 @@ use futures::{future, FutureExt}; use num_rational::Ratio; use once_cell::sync::OnceCell; use rand::{thread_rng, Rng}; -use tracing::info; +use tracing::{info, warn}; use crate::{start_view_client, Client, ClientActor, SyncStatus, ViewClientActor}; use near_chain::chain::{do_apply_chunks, BlockCatchUpRequest, StateSplitRequest}; @@ -173,8 +173,7 @@ impl Client { /// has started. pub fn finish_block_in_processing(&mut self, hash: &CryptoHash) -> Vec { if let Ok(()) = wait_for_block_in_processing(&mut self.chain, hash) { - let (accepted_blocks, errors) = self.postprocess_ready_blocks(Arc::new(|_| {}), true); - assert!(errors.is_empty()); + let (accepted_blocks, _) = self.postprocess_ready_blocks(Arc::new(|_| {}), true); return accepted_blocks; } vec![] @@ -1429,7 +1428,12 @@ impl TestEnv { { let target_id = self.account_to_client_index[&target.account_id.unwrap()]; let response = self.get_partial_encoded_chunk_response(target_id, request); - self.clients[id].shards_mgr.process_partial_encoded_chunk_response(response).unwrap(); + if let Some(response) = response { + self.clients[id] + .shards_mgr + .process_partial_encoded_chunk_response(response) + .unwrap(); + } } else { panic!("The request is not a PartialEncodedChunk request {:?}", request); } @@ -1439,24 +1443,33 @@ impl TestEnv { &mut self, id: usize, request: PartialEncodedChunkRequestMsg, - ) -> PartialEncodedChunkResponseMsg { + ) -> Option { let client = &mut self.clients[id]; - client.shards_mgr.process_partial_encoded_chunk_request(request, CryptoHash::default()); - let response = self.network_adapters[id].pop_most_recent().unwrap(); - if let PeerManagerMessageRequest::NetworkRequests( - NetworkRequests::PartialEncodedChunkResponse { route_back: _, response }, - ) = response + if client + .shards_mgr + .process_partial_encoded_chunk_request(request.clone(), CryptoHash::default()) { - return response; + let response = self.network_adapters[id].pop_most_recent().unwrap(); + if let PeerManagerMessageRequest::NetworkRequests( + NetworkRequests::PartialEncodedChunkResponse { route_back: _, response }, + ) = response + { + Some(response) + } else { + panic!( + "did not find PartialEncodedChunkResponse from the network queue {:?}", + response + ); + } } else { - panic!( - "did not find PartialEncodedChunkResponse from the network queue {:?}", - response - ); + // TODO: Somehow this may fail at epoch boundaries. Figure out why. + warn!("Failed to process PartialEncodedChunkRequest from client {}: {:?}", id, request); + None } } - pub fn process_shards_manager_responses(&mut self, id: usize) { + pub fn process_shards_manager_responses(&mut self, id: usize) -> bool { + let mut any_processed = false; while let Some(msg) = self.client_adapters[id].pop() { match msg { ShardsManagerResponse::ChunkCompleted { partial_chunk, shard_chunk } => { @@ -1469,11 +1482,17 @@ impl TestEnv { ShardsManagerResponse::InvalidChunk(encoded_chunk) => { self.clients[id].on_invalid_chunk(encoded_chunk); } - ShardsManagerResponse::ChunkHeaderReadyForInclusion(header) => { - self.clients[id].on_chunk_header_ready_for_inclusion(header); + ShardsManagerResponse::ChunkHeaderReadyForInclusion { + chunk_header, + chunk_producer, + } => { + self.clients[id] + .on_chunk_header_ready_for_inclusion(chunk_header, chunk_producer); } } + any_processed = true; } + any_processed } pub fn process_shards_manager_responses_and_finish_processing_blocks(&mut self, idx: usize) { diff --git a/chain/jsonrpc/res/validator.html b/chain/jsonrpc/res/validator.html index 52bd654d406..770ab855565 100644 --- a/chain/jsonrpc/res/validator.html +++ b/chain/jsonrpc/res/validator.html @@ -346,7 +346,7 @@ chunk_cell.append("F + " + time_delta + "ms
") if (thresholdApprovalTime != null) { let time_since_threshold = Date.parse(chunk_collection_time) - thresholdApprovalTime; - let text = $("T + " + time_since_threshold+ "ms
"); + let text = $("T + " + time_since_threshold + "ms
"); if (time_since_threshold > 300) { text.addClass('chunk-delay-red') } else if (time_since_threshold > 150) { @@ -389,6 +389,12 @@ $('.js-tbody-production').append($("HEAD")); } + for (let [epoch_id, chunk_producers] of data.status_response.ValidatorStatus.banned_chunk_producers) { + $('#banned-chunk-producers').append( + $('

') + .text('Banned chunk producers for epoch ' + epoch_id + ': ' + chunk_producers.join(', ')) + ); + } }; $(document).ready(() => { @@ -426,7 +432,8 @@

Shards can either be produced by this validator (marked as 'ME') or received from other validators.
Shards have missing chunks are marked as grey.
- We also mark shards arrival time in color. Shards that are delayed by more than 150ms after T are marked as orange, + We also mark shards arrival time in color. Shards that are delayed by more than 150ms after T are marked as + orange, and ones delayed more than 300 marked as red.
Approvals
Green field means that validators endorses the PREVIOUS block.
@@ -434,6 +441,7 @@

Other colors means different amount of skips.

+
@@ -468,4 +476,4 @@

- + \ No newline at end of file diff --git a/integration-tests/src/tests/client/features.rs b/integration-tests/src/tests/client/features.rs index 8d6304f53bb..d65abc878d1 100644 --- a/integration-tests/src/tests/client/features.rs +++ b/integration-tests/src/tests/client/features.rs @@ -2,6 +2,7 @@ mod access_key_nonce_for_implicit_accounts; mod account_id_in_function_call_permission; +mod adversarial_behaviors; mod cap_max_gas_price; mod chunk_nodes_cache; #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] diff --git a/integration-tests/src/tests/client/features/adversarial_behaviors.rs b/integration-tests/src/tests/client/features/adversarial_behaviors.rs new file mode 100644 index 00000000000..9b9cf58656e --- /dev/null +++ b/integration-tests/src/tests/client/features/adversarial_behaviors.rs @@ -0,0 +1,353 @@ +use std::{collections::HashSet, path::Path, sync::Arc}; + +use near_chain::{ChainGenesis, Provenance, RuntimeAdapter}; +use near_chain_configs::Genesis; +use near_client::test_utils::TestEnv; +use near_network::types::{NetworkRequests, PeerManagerMessageRequest}; +use near_o11y::testonly::init_test_logger; +use near_primitives::{ + runtime::config_store::RuntimeConfigStore, + shard_layout::ShardLayout, + sharding::PartialEncodedChunk, + types::{AccountId, EpochId, ShardId}, +}; +use near_store::test_utils::create_test_store; +use nearcore::{config::GenesisExt, TrackedConfig}; +use tracing::log::debug; + +struct AdversarialBehaviorTestData { + num_validators: usize, + env: TestEnv, +} + +const EPOCH_LENGTH: u64 = 20; + +impl AdversarialBehaviorTestData { + fn new() -> AdversarialBehaviorTestData { + let num_clients = 8; + let num_validators = 8 as usize; + let num_block_producers = 4; + let epoch_length = EPOCH_LENGTH; + + let accounts: Vec = + (0..num_clients).map(|i| format!("test{}", i).parse().unwrap()).collect(); + let mut genesis = Genesis::test(accounts, num_validators as u64); + { + let config = &mut genesis.config; + config.epoch_length = epoch_length; + config.shard_layout = ShardLayout::v1_test(); + config.num_block_producer_seats_per_shard = vec![ + num_block_producers as u64, + num_block_producers as u64, + num_block_producers as u64, + num_block_producers as u64, + ]; + config.num_block_producer_seats = num_block_producers as u64; + // Configure kickout threshold at 50%. + config.block_producer_kickout_threshold = 50; + config.chunk_producer_kickout_threshold = 50; + } + let chain_genesis = ChainGenesis::new(&genesis); + let runtimes: Vec> = (0..num_clients) + .map(|_| { + Arc::new(nearcore::NightshadeRuntime::test_with_runtime_config_store( + Path::new("."), + create_test_store(), + &genesis, + TrackedConfig::AllShards, + RuntimeConfigStore::test(), + )) as Arc + }) + .collect(); + let env = TestEnv::builder(chain_genesis) + .clients_count(num_clients) + .validator_seats(num_validators as usize) + .runtime_adapters(runtimes) + .build(); + + AdversarialBehaviorTestData { num_validators, env } + } + + fn process_one_peer_message(&mut self, client_id: usize, requests: NetworkRequests) { + match requests { + NetworkRequests::PartialEncodedChunkRequest { .. } => { + self.env.process_partial_encoded_chunk_request( + client_id, + PeerManagerMessageRequest::NetworkRequests(requests), + ); + } + NetworkRequests::PartialEncodedChunkMessage { account_id, partial_encoded_chunk } => { + self.env + .client(&account_id) + .shards_mgr + .process_partial_encoded_chunk( + PartialEncodedChunk::from(partial_encoded_chunk).into(), + ) + .unwrap(); + } + NetworkRequests::PartialEncodedChunkForward { account_id, forward } => { + match self + .env + .client(&account_id) + .shards_mgr + .process_partial_encoded_chunk_forward(forward) + { + Ok(_) | Err(near_chunks::Error::UnknownChunk) => {} + Err(e) => { + panic!("Unexpected error from chunk forward: {:?}", e) + } + } + } + NetworkRequests::Challenge(_) => { + // challenges not enabled. + } + _ => { + panic!("Unexpected network request: {:?}", requests); + } + } + } + + fn process_all_actor_messages(&mut self) { + loop { + let mut any_message_processed = false; + for i in 0..self.num_validators { + if let Some(msg) = self.env.network_adapters[i].pop() { + any_message_processed = true; + match msg { + PeerManagerMessageRequest::NetworkRequests(requests) => { + self.process_one_peer_message(i, requests); + } + _ => { + panic!("Unexpected message: {:?}", msg); + } + } + } + } + for i in 0..self.env.clients.len() { + any_message_processed |= self.env.process_shards_manager_responses(i); + } + if !any_message_processed { + break; + } + } + } +} + +#[test] +fn test_non_adversarial_case() { + init_test_logger(); + let mut test = AdversarialBehaviorTestData::new(); + let runtime_adapter = test.env.clients[0].runtime_adapter.clone(); + for height in 1..=EPOCH_LENGTH * 4 + 5 { + debug!(target: "test", "======= Height {} ======", height); + test.process_all_actor_messages(); + let epoch_id = runtime_adapter + .get_epoch_id_from_prev_block( + &test.env.clients[0].chain.head().unwrap().last_block_hash, + ) + .unwrap(); + let block_producer = runtime_adapter.get_block_producer(&epoch_id, height).unwrap(); + + let block = test.env.client(&block_producer).produce_block(height).unwrap().unwrap(); + assert_eq!(block.header().height(), height); + + if height > 1 { + assert_eq!(block.header().prev_height().unwrap(), height - 1); + let prev_block = + test.env.clients[0].chain.get_block(&block.header().prev_hash()).unwrap(); + for i in 0..4 { + // TODO: mysteriously we might miss a chunk around epoch boundaries. + // Figure out why... + assert!( + block.chunks()[i].height_created() == prev_block.header().height() + 1 + || (height % EPOCH_LENGTH == 1 + && block.chunks()[i].chunk_hash() + == prev_block.chunks()[i].chunk_hash()) + ); + } + } + + for i in 0..test.num_validators { + debug!(target: "test", "Processing block {} as validator #{}", height, i); + let _ = test.env.clients[i].start_process_block( + block.clone().into(), + if i == 0 { Provenance::PRODUCED } else { Provenance::NONE }, + Arc::new(|_| {}), + ); + let mut accepted_blocks = + test.env.clients[i].finish_block_in_processing(block.header().hash()); + // Process any chunk part requests that this client sent. Note that this would also + // process other network messages (such as production of the next chunk) which is OK. + test.process_all_actor_messages(); + accepted_blocks.extend(test.env.clients[i].finish_blocks_in_processing()); + + assert_eq!( + accepted_blocks.len(), + 1, + "Processing of block {} failed at validator #{}", + height, + i + ); + assert_eq!(&accepted_blocks[0], block.header().hash()); + assert_eq!(test.env.clients[i].chain.head().unwrap().height, height); + } + } + + // Sanity check that the final chain head is what we expect + assert_eq!(test.env.clients[0].chain.head().unwrap().height, EPOCH_LENGTH * 4 + 5); + let final_prev_block_hash = test.env.clients[0].chain.head().unwrap().prev_block_hash; + let final_epoch_id = + runtime_adapter.get_epoch_id_from_prev_block(&final_prev_block_hash).unwrap(); + let final_block_producers = runtime_adapter + .get_epoch_block_producers_ordered(&final_epoch_id, &final_prev_block_hash) + .unwrap(); + // No producers should be kicked out. + assert_eq!(final_block_producers.len(), 4); + let final_chunk_producers = runtime_adapter.get_epoch_chunk_producers(&final_epoch_id).unwrap(); + assert_eq!(final_chunk_producers.len(), 8); +} + +// Not marking this with test_features, because it's good to ensure this compiles, and also +// if we mark this with features we'd also have to mark a bunch of imports as features. +#[allow(dead_code)] +fn test_banning_chunk_producer_when_seeing_invalid_chunk_base( + mut test: AdversarialBehaviorTestData, +) { + let runtime_adapter = test.env.clients[0].runtime_adapter.clone(); + let bad_chunk_producer = + test.env.clients[7].validator_signer.as_ref().unwrap().validator_id().clone(); + let mut epochs_seen_invalid_chunk: HashSet = HashSet::new(); + let mut last_block_skipped = false; + for height in 1..=EPOCH_LENGTH * 4 + 5 { + debug!(target: "test", "======= Height {} ======", height); + test.process_all_actor_messages(); + let epoch_id = runtime_adapter + .get_epoch_id_from_prev_block( + &test.env.clients[0].chain.head().unwrap().last_block_hash, + ) + .unwrap(); + let block_producer = runtime_adapter.get_block_producer(&epoch_id, height).unwrap(); + + let block = test.env.client(&block_producer).produce_block(height).unwrap().unwrap(); + assert_eq!(block.header().height(), height); + + let mut invalid_chunks_in_this_block: HashSet = HashSet::new(); + let mut this_block_should_be_skipped = false; + if height > 1 { + if last_block_skipped { + assert_eq!(block.header().prev_height().unwrap(), height - 2); + } else { + assert_eq!(block.header().prev_height().unwrap(), height - 1); + } + for shard_id in 0..4 { + let chunk_producer = runtime_adapter + .get_chunk_producer( + &epoch_id, + block.header().prev_height().unwrap() + 1, + shard_id, + ) + .unwrap(); + if &chunk_producer == &bad_chunk_producer { + invalid_chunks_in_this_block.insert(shard_id); + if !epochs_seen_invalid_chunk.contains(&epoch_id) { + this_block_should_be_skipped = true; + epochs_seen_invalid_chunk.insert(epoch_id.clone()); + } + } + } + } + debug!(target: "test", "Epoch id of new block: {:?}", epoch_id); + debug!(target: "test", "Block should be skipped: {}; previous block skipped: {}", + this_block_should_be_skipped, last_block_skipped); + + if height > 1 { + let prev_block = + test.env.clients[0].chain.get_block(&block.header().prev_hash()).unwrap(); + for i in 0..4 { + if invalid_chunks_in_this_block.contains(&(i as ShardId)) + && !this_block_should_be_skipped + { + assert_eq!(block.chunks()[i].chunk_hash(), prev_block.chunks()[i].chunk_hash()); + } else { + // TODO: mysteriously we might miss a chunk around epoch boundaries. + // Figure out why... + assert!( + block.chunks()[i].height_created() == prev_block.header().height() + 1 + || (height % EPOCH_LENGTH == 1 + && block.chunks()[i].chunk_hash() + == prev_block.chunks()[i].chunk_hash()) + ); + } + } + } + + // The block producer of course has the complete block so we can process that. + for i in 0..test.num_validators { + debug!(target: "test", "Processing block {} as validator #{}", height, i); + let _ = test.env.clients[i].start_process_block( + block.clone().into(), + if i == 0 { Provenance::PRODUCED } else { Provenance::NONE }, + Arc::new(|_| {}), + ); + let mut accepted_blocks = + test.env.clients[i].finish_block_in_processing(block.header().hash()); + // Process any chunk part requests that this client sent. Note that this would also + // process other network messages (such as production of the next chunk) which is OK. + test.process_all_actor_messages(); + accepted_blocks.extend(test.env.clients[i].finish_blocks_in_processing()); + + if this_block_should_be_skipped { + assert_eq!( + accepted_blocks.len(), + 0, + "Processing of block {} should have failed due to invalid chunk", + height + ); + } else { + assert_eq!( + accepted_blocks.len(), + 1, + "Processing of block {} failed at validator #{}", + height, + i + ); + assert_eq!(&accepted_blocks[0], block.header().hash()); + assert_eq!(test.env.clients[i].chain.head().unwrap().height, height); + } + } + last_block_skipped = this_block_should_be_skipped; + } + + // Sanity check that the final chain head is what we expect + assert_eq!(test.env.clients[0].chain.head().unwrap().height, EPOCH_LENGTH * 4 + 5); + // Bad validator should've been kicked out in the third epoch, so it only had two chances + // to produce bad chunks. Other validators should not be kicked out. + assert_eq!(epochs_seen_invalid_chunk.len(), 2); + let final_prev_block_hash = test.env.clients[0].chain.head().unwrap().prev_block_hash; + let final_epoch_id = + runtime_adapter.get_epoch_id_from_prev_block(&final_prev_block_hash).unwrap(); + let final_block_producers = runtime_adapter + .get_epoch_block_producers_ordered(&final_epoch_id, &final_prev_block_hash) + .unwrap(); + assert!(final_block_producers.len() >= 3); // 3 validators if the bad validator was a block producer + let final_chunk_producers = runtime_adapter.get_epoch_chunk_producers(&final_epoch_id).unwrap(); + assert_eq!(final_chunk_producers.len(), 7); +} + +#[test] +#[cfg(feature = "test_features")] +fn test_banning_chunk_producer_when_seeing_invalid_chunk() { + init_test_logger(); + let mut test = AdversarialBehaviorTestData::new(); + test.env.clients[7].produce_invalid_chunks = true; + test_banning_chunk_producer_when_seeing_invalid_chunk_base(test); +} + +#[test] +#[cfg(feature = "test_features")] +fn test_banning_chunk_producer_when_seeing_invalid_tx_in_chunk() { + init_test_logger(); + let mut test = AdversarialBehaviorTestData::new(); + test.env.clients[7].produce_invalid_tx_in_chunks = true; + test_banning_chunk_producer_when_seeing_invalid_chunk_base(test); +} diff --git a/integration-tests/src/tests/client/process_blocks.rs b/integration-tests/src/tests/client/process_blocks.rs index dabeb8ba9f9..235ad273ded 100644 --- a/integration-tests/src/tests/client/process_blocks.rs +++ b/integration-tests/src/tests/client/process_blocks.rs @@ -2265,7 +2265,8 @@ fn test_validate_chunk_extra() { assert_eq!(accepted_blocks.len(), 1); // About to produce a block on top of block1. Validate that this chunk is legit. - let chunks = env.clients[0].get_chunk_headers_ready_for_inclusion(&block1.hash()); + let chunks = env.clients[0] + .get_chunk_headers_ready_for_inclusion(block1.header().epoch_id(), &block1.hash()); let chunk_extra = env.clients[0].chain.get_chunk_extra(block1.hash(), &ShardUId::single_shard()).unwrap(); assert!(validate_chunk_with_chunk_extra( diff --git a/integration-tests/src/tests/client/shards_manager.rs b/integration-tests/src/tests/client/shards_manager.rs index c991dd4c6a9..a125544a3d7 100644 --- a/integration-tests/src/tests/client/shards_manager.rs +++ b/integration-tests/src/tests/client/shards_manager.rs @@ -38,7 +38,7 @@ fn test_prepare_partial_encoded_chunk_response() { part_ords: (0..env.clients[0].runtime_adapter.num_total_parts() as u64).collect(), tracking_shards: Some(0u64).into_iter().collect(), }; - let res = Some(env.get_partial_encoded_chunk_response(0, request.clone())); + let res = env.get_partial_encoded_chunk_response(0, request.clone()); // Make the same request but this time call directly to ShardsManager and // get the request from a PartialEncodedChunk object. From cb7ec19c681c8a503f7f05a20beefcecd47dd9a0 Mon Sep 17 00:00:00 2001 From: mzhangmzz <34969888+mzhangmzz@users.noreply.github.com> Date: Thu, 17 Nov 2022 16:00:15 -0500 Subject: [PATCH 005/188] do not enter sync mode if peer is at an invalid hash (#8071) Unfortunately, it is very hard to add test for this because the syncing logic is in ClientActor. I already tested this in localnet and Robin tested it on mocknet and we verified that it behaves as expected. My plan is to merge this PR as it is, but I'll have a follow up PR where I refactor the ClientActor code to move the logic into client and also add tests. --- chain/chain-primitives/src/error.rs | 2 ++ chain/chain/src/chain.rs | 20 ++++++++++++++++++++ chain/chain/src/metrics.rs | 3 +++ chain/client/src/client_actor.rs | 13 ++++++++++--- chain/client/src/metrics.rs | 5 +++++ 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/chain/chain-primitives/src/error.rs b/chain/chain-primitives/src/error.rs index d33c55e29f7..34fd00dccd0 100644 --- a/chain/chain-primitives/src/error.rs +++ b/chain/chain-primitives/src/error.rs @@ -348,4 +348,6 @@ pub enum BlockKnownError { KnownInStore, #[error("already known in blocks in processing")] KnownInProcessing, + #[error("already known in invalid blocks")] + KnownAsInvalid, } diff --git a/chain/chain/src/chain.rs b/chain/chain/src/chain.rs index 6929a992c59..415d1f8e0a6 100644 --- a/chain/chain/src/chain.rs +++ b/chain/chain/src/chain.rs @@ -79,6 +79,7 @@ use crate::{metrics, DoomslugThresholdMode}; use actix::Message; use crossbeam_channel::{unbounded, Receiver, Sender}; use delay_detector::DelayDetector; +use lru::LruCache; use near_client_primitives::types::StateSplitApplyingStatus; use near_primitives::shard_layout::{ account_id_to_shard_id, account_id_to_shard_uid, ShardLayout, ShardUId, @@ -101,6 +102,9 @@ const MAX_ORPHAN_AGE_SECS: u64 = 300; // its NUM_ORPHAN_ANCESTORS_CHECK'th ancestor has been accepted pub const NUM_ORPHAN_ANCESTORS_CHECK: u64 = 3; +/// The size of the invalid_blocks in-memory pool +pub const INVALID_CHUNKS_POOL_SIZE: usize = 5000; + // Maximum number of orphans that we can request missing chunks // Note that if there are no forks, the maximum number of orphans we would // request missing chunks will not exceed NUM_ORPHAN_ANCESTORS_CHECK, @@ -414,6 +418,9 @@ pub fn check_known( if chain.blocks_with_missing_chunks.contains(block_hash) { return Ok(Err(BlockKnownError::KnownInMissingChunks)); } + if chain.is_block_invalid(block_hash) { + return Ok(Err(BlockKnownError::KnownAsInvalid)); + } check_known_store(chain, block_hash) } @@ -447,6 +454,8 @@ pub struct Chain { /// Used when it is needed to create flat storage in background for some shards. flat_storage_creator: Option, + invalid_blocks: LruCache, + /// Support for sandbox's patch_state requests. /// /// Sandbox needs ability to arbitrary modify the state. Blockchains @@ -526,6 +535,7 @@ impl Chain { apply_chunks_receiver: rc, last_time_head_updated: Clock::instant(), flat_storage_creator: None, + invalid_blocks: LruCache::new(INVALID_CHUNKS_POOL_SIZE), pending_state_patch: Default::default(), requested_state_parts: StateRequestTracker::new(), }) @@ -667,6 +677,7 @@ impl Chain { orphans: OrphanBlockPool::new(), blocks_with_missing_chunks: MissingChunksPool::new(), blocks_in_processing: BlocksInProcessing::new(), + invalid_blocks: LruCache::new(INVALID_CHUNKS_POOL_SIZE), genesis: genesis.clone(), transaction_validity_period: chain_genesis.transaction_validity_period, epoch_length: chain_genesis.epoch_length, @@ -2125,6 +2136,10 @@ impl Chain { let new_head = match self.postprocess_block_only(me, &block, block_preprocess_info, apply_results) { Err(err) => { + if err.is_bad_data() { + self.invalid_blocks.put(*block.hash(), ()); + metrics::NUM_INVALID_BLOCKS.inc(); + } self.blocks_delay_tracker.mark_block_errored(&block_hash, err.to_string()); return Err(err); } @@ -4367,6 +4382,11 @@ impl Chain { self.store.is_height_processed(height) } + #[inline] + pub fn is_block_invalid(&self, hash: &CryptoHash) -> bool { + self.invalid_blocks.contains(hash) + } + /// Check if can sync with sync_hash pub fn check_sync_hash_validity(&self, sync_hash: &CryptoHash) -> Result { let head = self.head()?; diff --git a/chain/chain/src/metrics.rs b/chain/chain/src/metrics.rs index c56262252f5..e38c70a68c1 100644 --- a/chain/chain/src/metrics.rs +++ b/chain/chain/src/metrics.rs @@ -107,3 +107,6 @@ pub static STATE_PART_ELAPSED: Lazy = Lazy::new(|| { ) .unwrap() }); +pub static NUM_INVALID_BLOCKS: Lazy = Lazy::new(|| { + try_create_int_gauge("near_num_invalid_blocks", "Number of invalid blocks").unwrap() +}); diff --git a/chain/client/src/client_actor.rs b/chain/client/src/client_actor.rs index e3c1aa55693..dbf27add8f1 100644 --- a/chain/client/src/client_actor.rs +++ b/chain/client/src/client_actor.rs @@ -1482,9 +1482,16 @@ impl ClientActor { let head = self.client.chain.head()?; let mut is_syncing = self.client.sync_status.is_syncing(); - let peer_info = if let Some(peer_info) = - self.network_info.highest_height_peers.choose(&mut thread_rng()) - { + // Only consider peers whose latest block is not invalid blocks + let eligible_peers: Vec<_> = self + .network_info + .highest_height_peers + .iter() + .filter(|p| !self.client.chain.is_block_invalid(&p.highest_block_hash)) + .collect(); + metrics::PEERS_WITH_INVALID_HASH + .set(self.network_info.highest_height_peers.len() as i64 - eligible_peers.len() as i64); + let peer_info = if let Some(peer_info) = eligible_peers.choose(&mut thread_rng()) { peer_info } else { if !self.client.config.skip_sync_wait { diff --git a/chain/client/src/metrics.rs b/chain/client/src/metrics.rs index 09231d21d60..3feea635bb5 100644 --- a/chain/client/src/metrics.rs +++ b/chain/client/src/metrics.rs @@ -142,6 +142,11 @@ pub(crate) static PROTOCOL_UPGRADE_BLOCK_HEIGHT: Lazy = Lazy::new(|| { .unwrap() }); +pub(crate) static PEERS_WITH_INVALID_HASH: Lazy = Lazy::new(|| { + try_create_int_gauge("near_peers_with_invalid_hash", "Number of peers that are on invalid hash") + .unwrap() +}); + pub(crate) static CHUNK_SKIPPED_TOTAL: Lazy = Lazy::new(|| { try_create_int_counter_vec( "near_chunk_skipped_total", From 667b7feadaa2c32a4b2342a14d4b54cbf88bb250 Mon Sep 17 00:00:00 2001 From: Alex Kladov Date: Fri, 18 Nov 2022 11:32:58 +0000 Subject: [PATCH 006/188] feat: honor NO_COLOR in tests (#8063) --- core/o11y/src/lib.rs | 6 +++++- core/o11y/src/testonly.rs | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/core/o11y/src/lib.rs b/core/o11y/src/lib.rs index 1cf182c5dc1..da868093547 100644 --- a/core/o11y/src/lib.rs +++ b/core/o11y/src/lib.rs @@ -319,10 +319,14 @@ fn use_color_output(options: &Options) -> bool { match options.color { ColorOutput::Always => true, ColorOutput::Never => false, - ColorOutput::Auto => std::env::var_os("NO_COLOR").is_none() && is_terminal(), + ColorOutput::Auto => use_color_auto(), } } +fn use_color_auto() -> bool { + std::env::var_os("NO_COLOR").is_none() && is_terminal() +} + /// Constructs a subscriber set to the option appropriate for the NEAR code. /// /// Subscriber enables only logging. diff --git a/core/o11y/src/testonly.rs b/core/o11y/src/testonly.rs index 1560e236b52..17e484f533d 100644 --- a/core/o11y/src/testonly.rs +++ b/core/o11y/src/testonly.rs @@ -1,9 +1,10 @@ mod tracing_capture; -pub use tracing_capture::TracingCapture; - +use crate::use_color_auto; use tracing_subscriber::{fmt as subscriber_fmt, EnvFilter}; +pub use tracing_capture::TracingCapture; + fn setup_subscriber_from_filter(mut env_filter: EnvFilter) { if let Ok(rust_log) = std::env::var("RUST_LOG") { for directive in rust_log.split(',').filter_map(|s| match s.parse() { @@ -18,6 +19,7 @@ fn setup_subscriber_from_filter(mut env_filter: EnvFilter) { } let _ = subscriber_fmt::Subscriber::builder() + .with_ansi(use_color_auto()) .with_span_events(subscriber_fmt::format::FmtSpan::CLOSE) .with_env_filter(env_filter) .with_writer(subscriber_fmt::TestWriter::new()) From 6440d9180a2249787356be1ec4c716e08e88fa51 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Fri, 18 Nov 2022 13:50:30 +0100 Subject: [PATCH 007/188] set file descriptor limit in neard to accomodate up to 1000 connections (#8049) Since the limit is a piece of global state which is not atomically mutable, we set it only directly in main(). --- Cargo.lock | 2 ++ neard/Cargo.toml | 2 ++ neard/src/main.rs | 14 ++++++++++++++ 3 files changed, 18 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 169114951f3..f49d1313d09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3747,6 +3747,7 @@ dependencies = [ "near-dyn-configs", "near-jsonrpc-primitives", "near-mirror", + "near-network", "near-o11y", "near-performance-metrics", "near-ping", @@ -3758,6 +3759,7 @@ dependencies = [ "openssl-probe", "opentelemetry", "rayon", + "rlimit", "rustc_version 0.4.0", "serde", "serde_json", diff --git a/neard/Cargo.toml b/neard/Cargo.toml index 47a9aeecb57..39c9bba9508 100644 --- a/neard/Cargo.toml +++ b/neard/Cargo.toml @@ -21,6 +21,7 @@ once_cell.workspace = true openssl-probe.workspace = true opentelemetry.workspace = true rayon.workspace = true +rlimit.workspace = true serde.workspace = true serde_json.workspace = true shell-escape.workspace = true @@ -34,6 +35,7 @@ near-amend-genesis = { path = "../tools/amend-genesis" } near-chain-configs = { path = "../core/chain-configs" } near-dyn-configs = { path = "../core/dyn-configs" } near-jsonrpc-primitives = { path = "../chain/jsonrpc-primitives" } +near-network = { path = "../chain/network" } near-mirror = { path = "../tools/mirror" } near-primitives = { path = "../core/primitives" } near-performance-metrics = { path = "../utils/near-performance-metrics" } diff --git a/neard/src/main.rs b/neard/src/main.rs index c4fb185bcd7..34578b01e3d 100644 --- a/neard/src/main.rs +++ b/neard/src/main.rs @@ -59,5 +59,19 @@ fn main() -> anyhow::Result<()> { openssl_probe::init_ssl_cert_env_vars(); near_performance_metrics::process::schedule_printing_performance_stats(Duration::from_secs(60)); + // The default FD soft limit in linux is 1024. + // We use more than that, for example we support up to 1000 TCP + // connections, using 5 FDs per each connection. + // We consider 65535 to be a reasonable limit for this binary, + // and we enforce it here. We also set the hard limit to the same value + // to prevent the inner logic from trying to bump it further: + // FD limit is a global variable, so it shouldn't be modified in an + // uncoordinated way. + const FD_LIMIT: u64 = 65535; + let (_, hard) = rlimit::Resource::NOFILE.get().context("rlimit::Resource::NOFILE::get()")?; + rlimit::Resource::NOFILE.set(FD_LIMIT, FD_LIMIT).context(format!( + "couldn't set the file descriptor limit to {FD_LIMIT}, hard limit = {hard}" + ))?; + NeardCmd::parse_and_run() } From f211ccb8d0656527e622722800d5cd45a477ddf7 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Fri, 18 Nov 2022 13:21:30 +0000 Subject: [PATCH 008/188] feat: rename `value_return` in gas profile (#8067) This cost never made sense to me. It's cost is defined by `new_data_receipt_byte` but for some reason it was tracked as `value_return`. In #8044 I introduced `new_data_receipt_byte` as an action cost but it is actually never used. This change replaces the old `value_return` cost with `new_data_receipt_byte`. This is an observable change on the RPC, as the profile now returns a different key for this cost. But is is not a breaking change because the profile keys are untyped. --- core/primitives-core/src/config.rs | 1 - core/primitives-core/src/profile.rs | 3 +-- core/primitives/src/views.rs | 3 +-- runtime/near-vm-logic/src/logic.rs | 6 +++++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/core/primitives-core/src/config.rs b/core/primitives-core/src/config.rs index addcb7609db..bf23b15c2c9 100644 --- a/core/primitives-core/src/config.rs +++ b/core/primitives-core/src/config.rs @@ -666,7 +666,6 @@ pub enum ActionCosts { add_function_call_key_base, add_function_call_key_byte, delete_key, - value_return, new_action_receipt, new_data_receipt_base, new_data_receipt_byte, diff --git a/core/primitives-core/src/profile.rs b/core/primitives-core/src/profile.rs index 848144d1887..2cf232ad80c 100644 --- a/core/primitives-core/src/profile.rs +++ b/core/primitives-core/src/profile.rs @@ -193,10 +193,9 @@ impl Cost { Cost::ActionCost { action_cost_kind: ActionCosts::add_function_call_key_base } => 6, Cost::ActionCost { action_cost_kind: ActionCosts::add_function_call_key_byte } => 6, Cost::ActionCost { action_cost_kind: ActionCosts::delete_key } => 7, - Cost::ActionCost { action_cost_kind: ActionCosts::value_return } => 8, + Cost::ActionCost { action_cost_kind: ActionCosts::new_data_receipt_byte } => 8, Cost::ActionCost { action_cost_kind: ActionCosts::new_action_receipt } => 9, Cost::ActionCost { action_cost_kind: ActionCosts::new_data_receipt_base } => 9, - Cost::ActionCost { action_cost_kind: ActionCosts::new_data_receipt_byte } => 9, Cost::ExtCost { ext_cost_kind: ExtCosts::base } => 10, Cost::ExtCost { ext_cost_kind: ExtCosts::contract_loading_base } => 11, Cost::ExtCost { ext_cost_kind: ExtCosts::contract_loading_bytes } => 12, diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index a2a2fc4924b..444f2d43029 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -1278,8 +1278,7 @@ impl From for ExecutionMetadataView { Cost::ActionCost { action_cost_kind: ActionCosts::new_action_receipt - | ActionCosts::new_data_receipt_base - | ActionCosts::new_data_receipt_byte, + | ActionCosts::new_data_receipt_base, } => "NEW_RECEIPT".to_owned(), // other costs have always been mapped one-to-one Cost::ActionCost { action_cost_kind: action_cost } => { diff --git a/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs index 2d8f8974d0f..c7427e2c4da 100644 --- a/runtime/near-vm-logic/src/logic.rs +++ b/runtime/near-vm-logic/src/logic.rs @@ -2217,7 +2217,11 @@ impl<'a> VMLogic<'a> { ) .ok_or(HostError::IntegerOverflow)?; } - self.gas_counter.pay_action_accumulated(burn_gas, burn_gas, ActionCosts::value_return)?; + self.gas_counter.pay_action_accumulated( + burn_gas, + burn_gas, + ActionCosts::new_data_receipt_byte, + )?; self.return_data = ReturnData::Value(return_val); Ok(()) } From 93703dd8234410362cd324bb83b5d5ceb1a906d2 Mon Sep 17 00:00:00 2001 From: Simonas Kazlauskas Date: Fri, 18 Nov 2022 15:27:30 +0000 Subject: [PATCH 009/188] docs: Paragraphize the aside callout (#8086) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the padding was weird, because the first line wasn’t wrapped up in `

`s like it should have been. Also add a link to the mail address. --- docs/practices/security_vulnerabilities.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/practices/security_vulnerabilities.md b/docs/practices/security_vulnerabilities.md index 4697bab3ef5..4e105aa0e49 100644 --- a/docs/practices/security_vulnerabilities.md +++ b/docs/practices/security_vulnerabilities.md @@ -1,11 +1,13 @@ ## Security Vulnerabilities

+ The intended audience of the information presented here are developers working on the implementation of NEAR. Are you a security researcher? Please report security vulnerabilities to -security@near.org. +[security@near.org](mailto:security@near.org). +
As nearcore is open source, all of its issues and pull requests are also From 46c5b57ee9f5abcb2422fb697ca2b2fa3d47075f Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Mon, 21 Nov 2022 11:28:16 +0100 Subject: [PATCH 010/188] store: test RawTrieNode encoding produces expected result (#8089) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In addition to testing whether RawTrieNode encode+decode round trip works, hard-code the expected encoded representation to make sure that refactoring or any other modifications to the code don’t change it. --- core/store/src/trie/mod.rs | 43 ++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/core/store/src/trie/mod.rs b/core/store/src/trie/mod.rs index ccd512a2381..7461ee4433e 100644 --- a/core/store/src/trie/mod.rs +++ b/core/store/src/trie/mod.rs @@ -399,13 +399,6 @@ impl RawTrieNode { } } - #[cfg(test)] - fn encode(&self) -> Vec { - let mut out = Vec::new(); - self.encode_into(&mut out); - out - } - fn decode(bytes: &[u8]) -> Result { let mut cursor = Cursor::new(bytes); match cursor.read_u8()? { @@ -1064,25 +1057,39 @@ mod tests { #[test] fn test_encode_decode() { - let value = vec![123, 245, 255]; + fn test(node: RawTrieNode, encoded: &[u8]) { + let mut buf = Vec::new(); + node.encode_into(&mut buf); + assert_eq!(encoded, buf.as_slice()); + assert_eq!(node, RawTrieNode::decode(&buf).unwrap()); + } + let value_length = 3; - let value_hash = hash(&value); + let value_hash = CryptoHash::hash_bytes(&[123, 245, 255]); let node = RawTrieNode::Leaf(vec![1, 2, 3], value_length, value_hash); - let buf = node.encode(); - let new_node = RawTrieNode::decode(&buf).expect("Failed to deserialize"); - assert_eq!(node, new_node); + let encoded = [ + 0, 3, 0, 0, 0, 1, 2, 3, 3, 0, 0, 0, 194, 40, 8, 24, 64, 219, 69, 132, 86, 52, 110, 175, + 57, 198, 165, 200, 83, 237, 211, 11, 194, 83, 251, 33, 145, 138, 234, 226, 7, 242, 186, + 73, + ]; + test(node, &encoded); let mut children: [Option; 16] = Default::default(); children[3] = Some(Trie::EMPTY_ROOT); let node = RawTrieNode::Branch(children, Some((value_length, value_hash))); - let buf = node.encode(); - let new_node = RawTrieNode::decode(&buf).expect("Failed to deserialize"); - assert_eq!(node, new_node); + let encoded = [ + 2, 3, 0, 0, 0, 194, 40, 8, 24, 64, 219, 69, 132, 86, 52, 110, 175, 57, 198, 165, 200, + 83, 237, 211, 11, 194, 83, 251, 33, 145, 138, 234, 226, 7, 242, 186, 73, 8, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + test(node, &encoded); let node = RawTrieNode::Extension(vec![123, 245, 255], Trie::EMPTY_ROOT); - let buf = node.encode(); - let new_node = RawTrieNode::decode(&buf).expect("Failed to deserialize"); - assert_eq!(node, new_node); + let encoded = [ + 3, 3, 0, 0, 0, 123, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + test(node, &encoded); } #[test] From 74d697089dbb40bf2b13822f06be1a1a30af6d34 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Mon, 21 Nov 2022 10:53:12 +0000 Subject: [PATCH 011/188] feat: increase column block cache for flat state (#7986) Also increase it for block info and block height because we see increased load on those. --- core/store/src/config.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/store/src/config.rs b/core/store/src/config.rs index fef5cc45640..3e33206f43c 100644 --- a/core/store/src/config.rs +++ b/core/store/src/config.rs @@ -144,6 +144,12 @@ impl StoreConfig { pub const fn col_cache_size(&self, col: crate::DBCol) -> bytesize::ByteSize { match col { crate::DBCol::State => self.col_state_cache_size, + #[cfg(feature = "protocol_feature_flat_state")] + crate::DBCol::FlatState => self.col_state_cache_size, + #[cfg(feature = "protocol_feature_flat_state")] + crate::DBCol::BlockInfo => bytesize::ByteSize::mib(64), + #[cfg(feature = "protocol_feature_flat_state")] + crate::DBCol::BlockHeight => bytesize::ByteSize::mib(64), _ => bytesize::ByteSize::mib(32), } } From dc65db13fdb3a6a620806ef1ff6746e8048150eb Mon Sep 17 00:00:00 2001 From: nikurt <86772482+nikurt@users.noreply.github.com> Date: Mon, 21 Nov 2022 15:36:16 +0100 Subject: [PATCH 012/188] Measure the number of hops of routed messages (#8050) * Measure the number of hops for routed messages. * Test output * Clarify * max_num_hops --- .../src/network_protocol/borsh_conv.rs | 8 +++-- chain/network/src/network_protocol/mod.rs | 4 +++ .../src/network_protocol/network.proto | 2 ++ .../proto_conv/peer_message.rs | 2 ++ chain/network/src/peer/peer_actor.rs | 2 +- chain/network/src/stats/metrics.rs | 33 +++++++++++++++++-- 6 files changed, 45 insertions(+), 6 deletions(-) diff --git a/chain/network/src/network_protocol/borsh_conv.rs b/chain/network/src/network_protocol/borsh_conv.rs index ebe3fc58a9a..1e09ecd8bc9 100644 --- a/chain/network/src/network_protocol/borsh_conv.rs +++ b/chain/network/src/network_protocol/borsh_conv.rs @@ -127,9 +127,11 @@ impl TryFrom<&net::PeerMessage> for mem::PeerMessage { net::PeerMessage::BlockRequest(bh) => mem::PeerMessage::BlockRequest(bh), net::PeerMessage::Block(b) => mem::PeerMessage::Block(b), net::PeerMessage::Transaction(t) => mem::PeerMessage::Transaction(t), - net::PeerMessage::Routed(r) => { - mem::PeerMessage::Routed(Box::new(RoutedMessageV2 { msg: *r, created_at: None })) - } + net::PeerMessage::Routed(r) => mem::PeerMessage::Routed(Box::new(RoutedMessageV2 { + msg: *r, + created_at: None, + num_hops: Some(0), + })), net::PeerMessage::Disconnect => mem::PeerMessage::Disconnect, net::PeerMessage::Challenge(c) => mem::PeerMessage::Challenge(c), net::PeerMessage::_HandshakeV2 => return Err(Self::Error::DeprecatedHandshakeV2), diff --git a/chain/network/src/network_protocol/mod.rs b/chain/network/src/network_protocol/mod.rs index 79d0a8147ff..9b2f5442919 100644 --- a/chain/network/src/network_protocol/mod.rs +++ b/chain/network/src/network_protocol/mod.rs @@ -476,6 +476,9 @@ pub struct RoutedMessageV2 { pub msg: RoutedMessage, /// The time the Routed message was created by `author`. pub created_at: Option, + /// Number of peers this routed message travelled through. + /// Doesn't include the peers that are the source and the destination of the message. + pub num_hops: Option, } impl std::ops::Deref for RoutedMessageV2 { @@ -696,6 +699,7 @@ impl RawRoutedMessage { body: self.body, }, created_at: now, + num_hops: Some(0), } } } diff --git a/chain/network/src/network_protocol/network.proto b/chain/network/src/network_protocol/network.proto index 2b49162e246..6a077bf644b 100644 --- a/chain/network/src/network_protocol/network.proto +++ b/chain/network/src/network_protocol/network.proto @@ -299,6 +299,8 @@ message RoutedMessage { bytes borsh = 1; // Timestamp of creating the Routed message by its original author. google.protobuf.Timestamp created_at = 2; + // Number of peers this routed message travelled through. Doesn't include the peer that created the message. + optional int32 num_hops = 3; } // Disconnect is send by a node before closing a TCP connection. 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 0825e546f1c..1ab7c76fc4d 100644 --- a/chain/network/src/network_protocol/proto_conv/peer_message.rs +++ b/chain/network/src/network_protocol/proto_conv/peer_message.rs @@ -140,6 +140,7 @@ impl From<&PeerMessage> for proto::PeerMessage { PeerMessage::Routed(r) => ProtoMT::Routed(proto::RoutedMessage { borsh: r.msg.try_to_vec().unwrap(), created_at: MF::from_option(r.created_at.as_ref().map(utc_to_proto)), + num_hops: r.num_hops, ..Default::default() }), PeerMessage::Disconnect => ProtoMT::Disconnect(proto::Disconnect::new()), @@ -252,6 +253,7 @@ impl TryFrom<&proto::PeerMessage> for PeerMessage { .map(utc_from_proto) .transpose() .map_err(Self::Error::RoutedCreatedAtTimestamp)?, + num_hops: r.num_hops, })), ProtoMT::Disconnect(_) => PeerMessage::Disconnect, ProtoMT::Challenge(c) => PeerMessage::Challenge( diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index 2a03727a612..ca44c8fea19 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -1041,7 +1041,7 @@ impl PeerActor { ); } if self.network_state.message_for_me(&msg.target) { - metrics::record_routed_msg_latency(&self.clock, &msg); + metrics::record_routed_msg_metrics(&self.clock, &msg); // Handle Ping and Pong message if they are for us without sending to client. // i.e. Return false in case of Ping and Pong match &msg.body { diff --git a/chain/network/src/stats/metrics.rs b/chain/network/src/stats/metrics.rs index 931d4d0efea..d6e9c1790f9 100644 --- a/chain/network/src/stats/metrics.rs +++ b/chain/network/src/stats/metrics.rs @@ -332,6 +332,14 @@ static NETWORK_ROUTED_MSG_LATENCY: Lazy = Lazy::new(|| { ) .unwrap() }); +static NETWORK_ROUTED_MSG_NUM_HOPS: Lazy = Lazy::new(|| { + try_create_int_counter_vec( + "near_network_routed_msg_hops", + "Number of peers the routed message travelled through", + &["routed", "hops"], + ) + .unwrap() +}); pub(crate) static CONNECTED_TO_MYSELF: Lazy = Lazy::new(|| { try_create_int_counter( @@ -341,9 +349,14 @@ pub(crate) static CONNECTED_TO_MYSELF: Lazy = Lazy::new(|| { .unwrap() }); -// The routed message received its destination. If the timestamp of creation of this message is +pub(crate) fn record_routed_msg_metrics(clock: &time::Clock, msg: &RoutedMessageV2) { + record_routed_msg_latency(clock, msg); + record_routed_msg_hops(msg); +} + +// The routed message reached its destination. If the timestamp of creation of this message is // known, then update the corresponding latency metric histogram. -pub(crate) fn record_routed_msg_latency(clock: &time::Clock, msg: &RoutedMessageV2) { +fn record_routed_msg_latency(clock: &time::Clock, msg: &RoutedMessageV2) { if let Some(created_at) = msg.created_at { let now = clock.now_utc(); let duration = now - created_at; @@ -353,6 +366,22 @@ pub(crate) fn record_routed_msg_latency(clock: &time::Clock, msg: &RoutedMessage } } +// The routed message reached its destination. If the number of hops is known, then update the +// corresponding metric. +fn record_routed_msg_hops(msg: &RoutedMessageV2) { + const MAX_NUM_HOPS: i32 = 20; + // We assume that the number of hops is small. + // As long as the number of hops is below 10, this metric will not consume too much memory. + if let Some(num_hops) = msg.num_hops { + if num_hops >= 0 { + let num_hops = if num_hops > MAX_NUM_HOPS { MAX_NUM_HOPS } else { num_hops }; + NETWORK_ROUTED_MSG_NUM_HOPS + .with_label_values(&[msg.body_variant(), &num_hops.to_string()]) + .inc(); + } + } +} + #[derive(Clone, Copy, strum::AsRefStr)] pub(crate) enum MessageDropped { NoRouteFound, From fe390ae2969c82f18b070b5b5fa89323fb12846d Mon Sep 17 00:00:00 2001 From: mzhangmzz <34969888+mzhangmzz@users.noreply.github.com> Date: Mon, 21 Nov 2022 11:08:34 -0500 Subject: [PATCH 013/188] mark block as invalid in preprocess_block (#8090) Co-authored-by: near-bulldozer[bot] <73298989+near-bulldozer[bot]@users.noreply.github.com> --- chain/chain/src/chain.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/chain/chain/src/chain.rs b/chain/chain/src/chain.rs index 415d1f8e0a6..62f2027d203 100644 --- a/chain/chain/src/chain.rs +++ b/chain/chain/src/chain.rs @@ -1958,6 +1958,10 @@ impl Chain { preprocess_res } Err(e) => { + if e.is_bad_data() { + metrics::NUM_INVALID_BLOCKS.inc(); + self.invalid_blocks.put(*block.hash(), ()); + } preprocess_timer.stop_and_discard(); match &e { Error::Orphan => { From 33b22490467bbc08fb82999f8bb7eafb5a64eb4f Mon Sep 17 00:00:00 2001 From: mzhangmzz <34969888+mzhangmzz@users.noreply.github.com> Date: Mon, 21 Nov 2022 11:25:43 -0500 Subject: [PATCH 014/188] make debug page show whether the peer is at an invalid or valid block (#8091) Screen Shot 2022-11-18 at 3 16 29 PM --- chain/client/src/client_actor.rs | 3 +- chain/client/src/debug.rs | 63 ++++++++++++++++++++++++++++- chain/jsonrpc/res/network_info.html | 10 ++++- chain/network/src/types.rs | 60 --------------------------- core/primitives/src/views.rs | 1 + 5 files changed, 74 insertions(+), 63 deletions(-) diff --git a/chain/client/src/client_actor.rs b/chain/client/src/client_actor.rs index dbf27add8f1..2f0d14239f3 100644 --- a/chain/client/src/client_actor.rs +++ b/chain/client/src/client_actor.rs @@ -11,6 +11,7 @@ use crate::adapter::{ RecvPartialEncodedChunkRequest, RecvPartialEncodedChunkResponse, SetNetworkInfo, StateResponse, }; use crate::client::{Client, EPOCH_START_INFO_BLOCKS}; +use crate::debug::new_network_info_view; use crate::info::{ display_sync_status, get_validator_epoch_stats, InfoHelper, ValidatorInfoHelper, }; @@ -828,7 +829,7 @@ impl Handler> for ClientActor { // For now - provide info about last 50 blocks. let detailed_debug_status = if msg.detailed { Some(DetailedDebugStatus { - network_info: self.network_info.clone().into(), + network_info: new_network_info_view(&self.client.chain, &self.network_info), sync_status: format!( "{} ({})", self.client.sync_status.as_variant_name().to_string(), diff --git a/chain/client/src/debug.rs b/chain/client/src/debug.rs index dbd06d25ad6..3281ba5cf35 100644 --- a/chain/client/src/debug.rs +++ b/chain/client/src/debug.rs @@ -5,7 +5,7 @@ use actix::{Context, Handler}; use borsh::BorshSerialize; use itertools::Itertools; use near_chain::crypto_hash_timer::CryptoHashTimer; -use near_chain::{near_chain_primitives, ChainStoreAccess, RuntimeAdapter}; +use near_chain::{near_chain_primitives, Chain, ChainStoreAccess, RuntimeAdapter}; use near_client_primitives::debug::{ ApprovalAtHeightStatus, BlockProduction, ChunkCollection, DebugBlockStatusData, DebugStatus, DebugStatusResponse, MissedHeightInfo, ProductionAtHeight, ValidatorStatus, @@ -30,8 +30,10 @@ use std::cmp::{max, min}; use std::collections::{HashMap, HashSet}; use near_client_primitives::debug::{DebugBlockStatus, DebugChunkStatus}; +use near_network::types::{ConnectedPeerInfo, NetworkInfo, PeerType}; use near_primitives::sharding::ShardChunkHeader; use near_primitives::time::Clock; +use near_primitives::views::{KnownProducerView, NetworkInfoView, PeerInfoView}; // Constants for debug requests. const DEBUG_BLOCKS_TO_FETCH: u32 = 50; @@ -627,3 +629,62 @@ impl ClientActor { }) } } +fn new_peer_info_view(chain: &Chain, connected_peer_info: &ConnectedPeerInfo) -> PeerInfoView { + let full_peer_info = &connected_peer_info.full_peer_info; + PeerInfoView { + addr: match full_peer_info.peer_info.addr { + Some(socket_addr) => socket_addr.to_string(), + None => "N/A".to_string(), + }, + account_id: full_peer_info.peer_info.account_id.clone(), + height: full_peer_info.chain_info.last_block.map(|x| x.height), + block_hash: full_peer_info.chain_info.last_block.map(|x| x.hash), + is_highest_block_invalid: full_peer_info + .chain_info + .last_block + .map(|x| chain.is_block_invalid(&x.hash)) + .unwrap_or_default(), + tracked_shards: full_peer_info.chain_info.tracked_shards.clone(), + archival: full_peer_info.chain_info.archival, + peer_id: full_peer_info.peer_info.id.public_key().clone(), + received_bytes_per_sec: connected_peer_info.received_bytes_per_sec, + sent_bytes_per_sec: connected_peer_info.sent_bytes_per_sec, + last_time_peer_requested_millis: connected_peer_info + .last_time_peer_requested + .elapsed() + .whole_milliseconds() as u64, + last_time_received_message_millis: connected_peer_info + .last_time_received_message + .elapsed() + .whole_milliseconds() as u64, + connection_established_time_millis: connected_peer_info + .connection_established_time + .elapsed() + .whole_milliseconds() as u64, + is_outbound_peer: connected_peer_info.peer_type == PeerType::Outbound, + } +} + +pub(crate) fn new_network_info_view(chain: &Chain, network_info: &NetworkInfo) -> NetworkInfoView { + NetworkInfoView { + peer_max_count: network_info.peer_max_count, + num_connected_peers: network_info.num_connected_peers, + connected_peers: network_info + .connected_peers + .iter() + .map(|full_peer_info| new_peer_info_view(chain, full_peer_info)) + .collect::>(), + known_producers: network_info + .known_producers + .iter() + .map(|it| KnownProducerView { + account_id: it.account_id.clone(), + peer_id: it.peer_id.public_key().clone(), + next_hops: it + .next_hops + .as_ref() + .map(|it| it.iter().map(|peer_id| peer_id.public_key().clone()).collect()), + }) + .collect(), + } +} diff --git a/chain/jsonrpc/res/network_info.html b/chain/jsonrpc/res/network_info.html index 2ffac77c08d..80e5de40ef7 100644 --- a/chain/jsonrpc/res/network_info.html +++ b/chain/jsonrpc/res/network_info.html @@ -103,6 +103,14 @@ }); } + function displayHash(peer) { + if (peer.is_highest_block_invalid) { + return peer.block_hash + " (INVALID)" + } else { + return peer.block_hash + " (Valid)" + } + } + function peerClass(current_height, peer_height) { if (peer_height > current_height + 5) { return 'peer_ahead_alot'; @@ -255,7 +263,7 @@ .append($('

') .append($(' + diff --git a/chain/network/src/network_protocol/edge.rs b/chain/network/src/network_protocol/edge.rs index 720bf283557..2bd0b64dd1e 100644 --- a/chain/network/src/network_protocol/edge.rs +++ b/chain/network/src/network_protocol/edge.rs @@ -135,6 +135,16 @@ impl Edge { } } + /// Create a fresh nonce (based on the current time). + pub fn create_fresh_nonce(clock: &time::Clock) -> u64 { + let mut nonce = clock.now_utc().unix_timestamp() as u64; + // Even nonce means that the edge should be removed, so if the timestamp is even, add one to get the odd value. + if nonce % 2 == 0 { + nonce += 1; + } + nonce + } + /// Create the remove edge change from an added edge change. pub fn remove_edge(&self, my_peer_id: PeerId, sk: &SecretKey) -> Edge { assert_eq!(self.edge_type(), EdgeState::Active); diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index 53085b38a81..dbff2793440 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -8,7 +8,7 @@ use crate::network_protocol::{ use crate::peer::stream; use crate::peer::tracker::Tracker; use crate::peer_manager::connection; -use crate::peer_manager::network_state::NetworkState; +use crate::peer_manager::network_state::{NetworkState, PRUNE_EDGES_AFTER}; use crate::peer_manager::peer_manager_actor::Event; use crate::private_actix::{RegisterPeerError, SendMessage}; use crate::routing::edge::verify_nonce; @@ -156,9 +156,21 @@ impl PeerActor { stream: tcp::Stream, force_encoding: Option, network_state: Arc, + ) -> anyhow::Result> { + Self::spawn_with_nonce(clock, stream, force_encoding, network_state, None) + } + + /// Span peer actor, and make it establish a connection with a given nonce. + /// Used mostly for tests. + pub(crate) fn spawn_with_nonce( + clock: time::Clock, + stream: tcp::Stream, + force_encoding: Option, + network_state: Arc, + nonce: Option, ) -> anyhow::Result> { let stream_id = stream.id(); - match Self::spawn_inner(clock, stream, force_encoding, network_state.clone()) { + match Self::spawn_inner(clock, stream, force_encoding, network_state.clone(), nonce) { Ok(it) => Ok(it), Err(reason) => { network_state.config.event_sink.push(Event::ConnectionClosed( @@ -174,6 +186,7 @@ impl PeerActor { stream: tcp::Stream, force_encoding: Option, network_state: Arc, + nonce: Option, ) -> Result, ClosingReason> { let connecting_status = match &stream.type_ { tcp::StreamType::Inbound => ConnectingStatus::Inbound( @@ -189,7 +202,7 @@ impl PeerActor { .start_outbound(peer_id.clone()) .map_err(ClosingReason::OutboundNotAllowed)?, handshake_spec: HandshakeSpec { - partial_edge_info: network_state.propose_edge(peer_id, None), + partial_edge_info: network_state.propose_edge(&clock, peer_id, nonce), protocol_version: PROTOCOL_VERSION, peer_id: peer_id.clone(), }, @@ -431,6 +444,7 @@ impl PeerActor { )); return; } + // Verify if nonce is sane. if let Err(err) = verify_nonce(&self.clock, handshake.partial_edge_info.nonce) { tracing::debug!(target: "network", nonce=?handshake.partial_edge_info.nonce, my_node_id = ?self.my_node_id(), peer_id=?handshake.sender_peer_id, "bad nonce, disconnecting: {err}"); @@ -470,7 +484,7 @@ impl PeerActor { handshake_spec.partial_edge_info.clone() } ConnectingStatus::Inbound { .. } => { - self.network_state.propose_edge(&handshake.sender_peer_id, Some(nonce)) + self.network_state.propose_edge(&self.clock, &handshake.sender_peer_id, Some(nonce)) } }; let edge = Edge::new( @@ -499,7 +513,7 @@ impl PeerActor { let conn = Arc::new(connection::Connection { addr: ctx.address(), peer_info: peer_info.clone(), - edge, + edge: AtomicCell::new(edge), genesis_id: handshake.sender_chain_info.genesis_id.clone(), tracked_shards: handshake.sender_chain_info.tracked_shards.clone(), archival: handshake.sender_chain_info.archival, @@ -556,6 +570,11 @@ impl PeerActor { }) }); + // This time is used to figure out when the first run of the callbacks it run. + // It is important that it is set here (rather than calling clock.now() within the future) - as it makes testing a lot easier (and more deterministic). + + let start_time = self.clock.now(); + // Here we stop processing any PeerActor events until PeerManager // decides whether to accept the connection or not: ctx.wait makes // the actor event loop poll on the future until it completes before @@ -588,7 +607,7 @@ impl PeerActor { })); // Only broadcast the new edge from the outbound endpoint. act.network_state.tier2.broadcast_message(Arc::new(PeerMessage::SyncRoutingTable( - RoutingTableUpdate::from_edges(vec![conn.edge.clone()]), + RoutingTableUpdate::from_edges(vec![conn.edge.load()]), ))); } @@ -623,11 +642,38 @@ impl PeerActor { } } })); + + // Refresh connection nonces but only if we're outbound. For inbound connection, the other party should + // take care of nonce refresh. + if act.peer_type == PeerType::Outbound { + ctx.spawn(wrap_future({ + let conn = conn.clone(); + let network_state = act.network_state.clone(); + let clock = act.clock.clone(); + async move { + // How often should we refresh a nonce from a peer. + // It should be smaller than PRUNE_EDGES_AFTER. + let mut interval = time::Interval::new(start_time + PRUNE_EDGES_AFTER / 3, PRUNE_EDGES_AFTER / 3); + loop { + interval.tick(&clock).await; + conn.send_message(Arc::new( + PeerMessage::RequestUpdateNonce(PartialEdgeInfo::new( + &network_state.config.node_id(), + &conn.peer_info.id, + Edge::create_fresh_nonce(&clock), + &network_state.config.node_key, + ) + ))); + + } + } + })); + } // Sync the RoutingTable. act.sync_routing_table(); act.network_state.config.event_sink.push(Event::HandshakeCompleted(HandshakeCompletedEvent{ stream_id: act.stream_id, - edge: conn.edge.clone(), + edge: conn.edge.load(), })); }, Err(err) => { @@ -724,8 +770,11 @@ impl PeerActor { return; } // Recreate the edge with a newer nonce. - handshake_spec.partial_edge_info = - self.network_state.propose_edge(&handshake_spec.peer_id, Some(edge.next())); + handshake_spec.partial_edge_info = self.network_state.propose_edge( + &self.clock, + &handshake_spec.peer_id, + Some(std::cmp::max(edge.next(), Edge::create_fresh_nonce(&self.clock))), + ); let spec = handshake_spec.clone(); ctx.wait(actix::fut::ready(()).then(move |_, act: &mut Self, _| { act.send_handshake(spec); diff --git a/chain/network/src/peer_manager/connection/mod.rs b/chain/network/src/peer_manager/connection/mod.rs index 188cd842f92..d2ac6123412 100644 --- a/chain/network/src/peer_manager/connection/mod.rs +++ b/chain/network/src/peer_manager/connection/mod.rs @@ -46,7 +46,7 @@ pub(crate) struct Connection { pub addr: actix::Addr, pub peer_info: PeerInfo, - pub edge: Edge, + pub edge: AtomicCell, /// Chain Id and hash of genesis block. pub genesis_id: GenesisId, /// Shards that the peer is tracking. @@ -77,7 +77,7 @@ impl fmt::Debug for Connection { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Connection") .field("peer_info", &self.peer_info) - .field("edge", &self.edge) + .field("edge", &self.edge.load()) .field("peer_type", &self.peer_type) .field("connection_established_time", &self.connection_established_time) .finish() @@ -295,6 +295,20 @@ impl Pool { pool.ready.remove(peer_id); }); } + /// Update the edge in the pool (if it is newer). + pub fn update_edge(&self, new_edge: &Edge) { + self.0.update(|pool| { + let other = new_edge.other(&pool.me); + if let Some(other) = other { + if let Some(connection) = pool.ready.get_mut(other) { + let edge = connection.edge.load(); + if edge.nonce() < new_edge.nonce() { + connection.edge.store(new_edge.clone()); + } + } + } + }) + } /// Send message to peer that belongs to our active set /// Return whether the message is sent or not. diff --git a/chain/network/src/peer_manager/network_state/mod.rs b/chain/network/src/peer_manager/network_state/mod.rs index b21c7e28e61..78295454d99 100644 --- a/chain/network/src/peer_manager/network_state/mod.rs +++ b/chain/network/src/peer_manager/network_state/mod.rs @@ -42,7 +42,7 @@ const IMPORTANT_MESSAGE_RESENT_COUNT: usize = 3; const PRUNE_UNREACHABLE_PEERS_AFTER: time::Duration = time::Duration::hours(1); /// Remove the edges that were created more that this duration ago. -const PRUNE_EDGES_AFTER: time::Duration = time::Duration::minutes(30); +pub const PRUNE_EDGES_AFTER: time::Duration = time::Duration::minutes(30); impl WhitelistNode { pub fn from_peer_info(pi: &PeerInfo) -> anyhow::Result { @@ -258,7 +258,7 @@ impl NetworkState { // Verify and broadcast the edge of the connection. Only then insert the new // connection to TIER2 pool, so that nothing is broadcasted to conn. // TODO(gprusak): consider actually banning the peer for consistency. - this.add_edges(&clock, vec![conn.edge.clone()]) + this.add_edges(&clock, vec![conn.edge.load()]) .await .map_err(|_: ReasonForBan| RegisterPeerError::InvalidEdge)?; this.tier2.insert_ready(conn.clone()).map_err(RegisterPeerError::PoolError)?; @@ -474,7 +474,7 @@ impl NetworkState { PartialEdgeInfo::new( &node_id, &conn.peer_info.id, - edge.next(), + std::cmp::max(Edge::create_fresh_nonce(&clock), edge.next()), &this.config.node_key, ), ))); diff --git a/chain/network/src/peer_manager/network_state/routing.rs b/chain/network/src/peer_manager/network_state/routing.rs index 5f7acb8afeb..cea54f684fd 100644 --- a/chain/network/src/peer_manager/network_state/routing.rs +++ b/chain/network/src/peer_manager/network_state/routing.rs @@ -37,11 +37,24 @@ impl NetworkState { /// Constructs a partial edge to the given peer with the nonce specified. /// If nonce is None, nonce is selected automatically. - pub fn propose_edge(&self, peer1: &PeerId, with_nonce: Option) -> PartialEdgeInfo { + pub fn propose_edge( + &self, + clock: &time::Clock, + peer1: &PeerId, + with_nonce: Option, + ) -> PartialEdgeInfo { // When we create a new edge we increase the latest nonce by 2 in case we miss a removal // proposal from our partner. let nonce = with_nonce.unwrap_or_else(|| { - self.graph.load().local_edges.get(peer1).map_or(1, |edge| edge.next()) + let nonce = Edge::create_fresh_nonce(clock); + // If we already had a connection to this peer - check that edge's nonce. + // And use either that one or the one from the current timestamp. + // We would use existing edge's nonce, if we were trying to connect to a given peer multiple times per second. + self.graph + .load() + .local_edges + .get(peer1) + .map_or(nonce, |edge| std::cmp::max(edge.next(), nonce)) }); PartialEdgeInfo::new(&self.config.node_id(), peer1, nonce, &self.config.node_key) } @@ -85,6 +98,14 @@ impl NetworkState { if edges.len() == 0 { return result; } + + // Select local edges (where we are one of the peers) - and update the peer's Connection nonces. + for e in &edges { + if let Some(_) = e.other(&self.config.node_id()) { + self.tier2.update_edge(&e); + } + } + let this = self.clone(); let clock = clock.clone(); let _ = self diff --git a/chain/network/src/peer_manager/peer_manager_actor.rs b/chain/network/src/peer_manager/peer_manager_actor.rs index 93cf3bf255c..6806658f6d3 100644 --- a/chain/network/src/peer_manager/peer_manager_actor.rs +++ b/chain/network/src/peer_manager/peer_manager_actor.rs @@ -584,6 +584,7 @@ impl PeerManagerActor { last_time_received_message: cp.last_time_received_message.load(), connection_established_time: cp.connection_established_time, peer_type: cp.peer_type, + nonce: cp.edge.load().nonce(), }) .collect(), num_connected_peers: tier2.ready.len(), diff --git a/chain/network/src/peer_manager/tests/nonce.rs b/chain/network/src/peer_manager/tests/nonce.rs index e69e60acf68..f65bb6d2ac0 100644 --- a/chain/network/src/peer_manager/tests/nonce.rs +++ b/chain/network/src/peer_manager/tests/nonce.rs @@ -2,11 +2,13 @@ use crate::network_protocol::testonly as data; use crate::network_protocol::{ Encoding, Handshake, PartialEdgeInfo, PeerMessage, EDGE_MIN_TIMESTAMP_NONCE, }; -use crate::peer_manager; +use crate::peer_manager::testonly::{ActorHandler, Event}; +use crate::peer_manager::{self, peer_manager_actor}; use crate::tcp; use crate::testonly::make_rng; use crate::testonly::stream; use crate::time; +use crate::types::Edge; use near_o11y::testonly::init_test_logger; use near_primitives::network::PeerId; use near_primitives::version; @@ -87,3 +89,82 @@ async fn test_nonces() { } } } + +async fn wait_for_edge(actor_handler: &mut ActorHandler) -> Edge { + actor_handler + .events + .recv_until(|ev| match ev { + Event::PeerManager(peer_manager_actor::Event::EdgesAdded(ev)) => Some(ev[0].clone()), + _ => None, + }) + .await +} + +#[tokio::test] +/// Create 2 peer managers, that connect to each other. +/// Verify that the will refresh their nonce after some time. +async fn test_nonce_refresh() { + init_test_logger(); + let mut rng = make_rng(921853255); + let rng = &mut rng; + let mut clock = time::FakeClock::new(*EDGE_MIN_TIMESTAMP_NONCE + time::Duration::days(2)); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + // Start a PeerManager. + let pm = peer_manager::testonly::start( + clock.clock(), + near_store::db::TestDB::new(), + chain.make_config(rng), + chain.clone(), + ) + .await; + + // Start another peer manager. + let mut pm2 = peer_manager::testonly::start( + clock.clock(), + near_store::db::TestDB::new(), + chain.make_config(rng), + chain.clone(), + ) + .await; + + pm2.connect_to(&pm.peer_info()).await; + + let edge = wait_for_edge(&mut pm2).await; + let start_time = clock.now_utc(); + // First edge between them should have the nonce equal to the current time. + assert_eq!(Edge::nonce_to_utc(edge.nonce()).unwrap().unwrap(), start_time); + + // Advance a clock by 1 hour. + clock.advance(time::Duration::HOUR); + + let new_nonce_utc = clock.now_utc(); + + loop { + let edge = wait_for_edge(&mut pm2).await; + if Edge::nonce_to_utc(edge.nonce()).unwrap().unwrap() == start_time { + tracing::debug!("Still seeing old edge.."); + } else { + assert_eq!(Edge::nonce_to_utc(edge.nonce()).unwrap().unwrap(), new_nonce_utc); + break; + } + } + + // Check that the nonces were properly updates on both pm and pm2 states. + let pm_peer_info = pm.peer_info().id.clone(); + let pm2_nonce = pm2 + .with_state(|s| async move { + s.tier2.load().ready.get(&pm_peer_info).unwrap().edge.load().nonce() + }) + .await; + + assert_eq!(Edge::nonce_to_utc(pm2_nonce).unwrap().unwrap(), new_nonce_utc); + + let pm_nonce = pm + .with_state(|s| async move { + s.tier2.load().ready.get(&pm2.peer_info().id).unwrap().edge.load().nonce() + }) + .await; + + assert_eq!(Edge::nonce_to_utc(pm_nonce).unwrap().unwrap(), new_nonce_utc); +} diff --git a/chain/network/src/types.rs b/chain/network/src/types.rs index 2dc9ad74933..47f7fdf4ec4 100644 --- a/chain/network/src/types.rs +++ b/chain/network/src/types.rs @@ -333,20 +333,6 @@ pub struct PeerChainInfo { pub archival: bool, } -impl From<&FullPeerInfo> for ConnectedPeerInfo { - fn from(full_peer_info: &FullPeerInfo) -> Self { - ConnectedPeerInfo { - full_peer_info: full_peer_info.clone(), - received_bytes_per_sec: 0, - sent_bytes_per_sec: 0, - last_time_peer_requested: time::Instant::now(), - last_time_received_message: time::Instant::now(), - connection_established_time: time::Instant::now(), - peer_type: PeerType::Outbound, - } - } -} - // Information about the connected peer that is shared with the rest of the system. #[derive(Debug, Clone)] pub struct ConnectedPeerInfo { @@ -363,6 +349,8 @@ pub struct ConnectedPeerInfo { pub connection_established_time: time::Instant, /// Who started connection. Inbound (other) or Outbound (us). pub peer_type: PeerType, + /// Nonce used for the connection with the peer. + pub nonce: u64, } #[derive(Debug, Clone, actix::MessageResponse)] diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index 60376d0e8d6..4627ed71a4d 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -350,6 +350,8 @@ pub struct PeerInfoView { pub last_time_received_message_millis: u64, pub connection_established_time_millis: u64, pub is_outbound_peer: bool, + /// Connection nonce. + pub nonce: u64, } /// Information about a Producer: its account name, peer_id and a list of connected peers that diff --git a/integration-tests/src/tests/client/process_blocks.rs b/integration-tests/src/tests/client/process_blocks.rs index b9e5508e155..91415830ad0 100644 --- a/integration-tests/src/tests/client/process_blocks.rs +++ b/integration-tests/src/tests/client/process_blocks.rs @@ -33,7 +33,7 @@ use near_crypto::{InMemorySigner, KeyType, PublicKey, Signature, Signer}; use near_network::test_utils::{wait_or_panic, MockPeerManagerAdapter}; use near_network::types::{ BlockInfo, ConnectedPeerInfo, HighestHeightPeerInfo, NetworkInfo, PeerChainInfo, - PeerManagerMessageRequest, PeerManagerMessageResponse, + PeerManagerMessageRequest, PeerManagerMessageResponse, PeerType, }; use near_network::types::{FullPeerInfo, NetworkRequests, NetworkResponses}; use near_network::types::{PeerInfo, ReasonForBan}; @@ -1028,15 +1028,24 @@ fn client_sync_headers() { ); client.do_send( SetNetworkInfo(NetworkInfo { - connected_peers: vec![ConnectedPeerInfo::from(&FullPeerInfo { - peer_info: peer_info2.clone(), - chain_info: PeerChainInfo { - genesis_id: Default::default(), - last_block: Some(BlockInfo { height: 5, hash: hash(&[5]) }), - tracked_shards: vec![], - archival: false, + connected_peers: vec![ConnectedPeerInfo { + full_peer_info: FullPeerInfo { + peer_info: peer_info2.clone(), + chain_info: PeerChainInfo { + genesis_id: Default::default(), + last_block: Some(BlockInfo { height: 5, hash: hash(&[5]) }), + tracked_shards: vec![], + archival: false, + }, }, - })], + received_bytes_per_sec: 0, + sent_bytes_per_sec: 0, + last_time_peer_requested: near_network::time::Instant::now(), + last_time_received_message: near_network::time::Instant::now(), + connection_established_time: near_network::time::Instant::now(), + peer_type: PeerType::Outbound, + nonce: 1, + }], num_connected_peers: 1, peer_max_count: 1, highest_height_peers: vec![HighestHeightPeerInfo { diff --git a/tools/mock-node/src/lib.rs b/tools/mock-node/src/lib.rs index 3ba6e84814c..71f47451d45 100644 --- a/tools/mock-node/src/lib.rs +++ b/tools/mock-node/src/lib.rs @@ -6,9 +6,10 @@ use anyhow::{anyhow, Context as AnyhowContext}; use near_chain::{Block, BlockHeader, Chain, ChainStoreAccess, Error}; use near_chain_configs::GenesisConfig; use near_client::sync; +use near_network::time; use near_network::types::{ - BlockInfo, FullPeerInfo, NetworkInfo, NetworkRequests, NetworkResponses, - PeerManagerMessageRequest, PeerManagerMessageResponse, SetChainInfo, + BlockInfo, ConnectedPeerInfo, FullPeerInfo, NetworkInfo, NetworkRequests, NetworkResponses, + PeerManagerMessageRequest, PeerManagerMessageResponse, PeerType, SetChainInfo, }; use near_network::types::{ PartialEncodedChunkRequestMsg, PartialEncodedChunkResponseMsg, PeerInfo, @@ -238,7 +239,16 @@ impl MockPeerManagerActor { }, }; let network_info = NetworkInfo { - connected_peers: vec![(&peer).into()], + connected_peers: vec![ConnectedPeerInfo { + full_peer_info: peer.clone(), + received_bytes_per_sec: 0, + sent_bytes_per_sec: 0, + last_time_peer_requested: time::Instant::now(), + last_time_received_message: time::Instant::now(), + connection_established_time: time::Instant::now(), + peer_type: PeerType::Outbound, + nonce: 1, + }], num_connected_peers: 1, peer_max_count: 1, highest_height_peers: vec![>>::into(peer).unwrap()], From 946610290900b8216a42917a7eab9130a37b1176 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Fri, 25 Nov 2022 16:15:26 +0100 Subject: [PATCH 034/188] Separated Tier1Handshake message from Tier2Handshake message (#8118) Separated TIER2 Handshake from TIER1 Handshake message, to make it impossible for the peers to confuse which network the given connection belongs to. In particular the binary from the previous release won't be able to accept TIER1 connections. --- .../src/network_protocol/borsh_conv.rs | 9 ++++++-- chain/network/src/network_protocol/mod.rs | 3 ++- .../src/network_protocol/network.proto | 13 ++++++++++- .../proto_conv/peer_message.rs | 10 ++++++--- chain/network/src/network_protocol/tests.rs | 22 +++++++++++-------- chain/network/src/peer/peer_actor.rs | 6 ++--- chain/network/src/peer/tests/communication.rs | 10 ++++----- .../src/peer_manager/tests/connection_pool.rs | 2 +- chain/network/src/peer_manager/tests/nonce.rs | 4 ++-- chain/network/src/raw/connection.rs | 4 ++-- 10 files changed, 54 insertions(+), 29 deletions(-) diff --git a/chain/network/src/network_protocol/borsh_conv.rs b/chain/network/src/network_protocol/borsh_conv.rs index 1e09ecd8bc9..23f4ad441af 100644 --- a/chain/network/src/network_protocol/borsh_conv.rs +++ b/chain/network/src/network_protocol/borsh_conv.rs @@ -106,7 +106,7 @@ impl TryFrom<&net::PeerMessage> for mem::PeerMessage { type Error = ParsePeerMessageError; fn try_from(x: &net::PeerMessage) -> Result { Ok(match x.clone() { - net::PeerMessage::Handshake(h) => mem::PeerMessage::Handshake((&h).into()), + net::PeerMessage::Handshake(h) => mem::PeerMessage::Tier2Handshake((&h).into()), net::PeerMessage::HandshakeFailure(pi, hfr) => { mem::PeerMessage::HandshakeFailure(pi, (&hfr).into()) } @@ -150,10 +150,15 @@ impl TryFrom<&net::PeerMessage> for mem::PeerMessage { } } +// We are working on deprecating Borsh support for network messages altogether, +// so any new message variants are simply unsupported. impl From<&mem::PeerMessage> for net::PeerMessage { fn from(x: &mem::PeerMessage) -> Self { match x.clone() { - mem::PeerMessage::Handshake(h) => net::PeerMessage::Handshake((&h).into()), + mem::PeerMessage::Tier1Handshake(_) => { + panic!("Tier1Handshake is not supported in Borsh encoding") + } + mem::PeerMessage::Tier2Handshake(h) => net::PeerMessage::Handshake((&h).into()), mem::PeerMessage::HandshakeFailure(pi, hfr) => { net::PeerMessage::HandshakeFailure(pi, (&hfr).into()) } diff --git a/chain/network/src/network_protocol/mod.rs b/chain/network/src/network_protocol/mod.rs index ad1455863ae..7959f51588e 100644 --- a/chain/network/src/network_protocol/mod.rs +++ b/chain/network/src/network_protocol/mod.rs @@ -236,7 +236,8 @@ pub struct SyncAccountsData { #[derive(PartialEq, Eq, Clone, Debug, strum::IntoStaticStr, strum::EnumVariantNames)] #[allow(clippy::large_enum_variant)] pub enum PeerMessage { - Handshake(Handshake), + Tier1Handshake(Handshake), + Tier2Handshake(Handshake), HandshakeFailure(PeerInfo, HandshakeFailureReason), /// When a failed nonce is used by some peer, this message is sent back as evidence. LastEdge(Edge), diff --git a/chain/network/src/network_protocol/network.proto b/chain/network/src/network_protocol/network.proto index 488ca198f13..840da94faf0 100644 --- a/chain/network/src/network_protocol/network.proto +++ b/chain/network/src/network_protocol/network.proto @@ -364,7 +364,18 @@ message PeerMessage { TraceContext trace_context = 26; oneof message_type { - Handshake handshake = 4; + // Handshakes for TIER1 and TIER2 networks are considered separate, + // so that a node binary which doesn't support TIER1 connection won't + // be even able to PARSE the handshake. This way we avoid accidental + // connections, such that one end thinks it is a TIER2 connection and the + // other thinks it is a TIER1 connection. As currently both TIER1 and TIER2 + // connections are handled by the same PeerActor, both fields use the same + // underlying message type. If we ever decide to separate the handshake + // implementations, we can copy the Handshake message type defition and + // make it evolve differently for TIER1 and TIER2. + Handshake tier1_handshake = 27; + Handshake tier2_handshake = 4; + HandshakeFailure handshake_failure = 5; LastEdge last_edge = 6; RoutingTableUpdate sync_routing_table = 7; 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 1ab7c76fc4d..5c74b39c367 100644 --- a/chain/network/src/network_protocol/proto_conv/peer_message.rs +++ b/chain/network/src/network_protocol/proto_conv/peer_message.rs @@ -81,7 +81,8 @@ impl From<&PeerMessage> for proto::PeerMessage { fn from(x: &PeerMessage) -> Self { Self { message_type: Some(match x { - PeerMessage::Handshake(h) => ProtoMT::Handshake(h.into()), + PeerMessage::Tier1Handshake(h) => ProtoMT::Tier1Handshake(h.into()), + PeerMessage::Tier2Handshake(h) => ProtoMT::Tier2Handshake(h.into()), PeerMessage::HandshakeFailure(pi, hfr) => { ProtoMT::HandshakeFailure((pi, hfr).into()) } @@ -200,8 +201,11 @@ impl TryFrom<&proto::PeerMessage> for PeerMessage { type Error = ParsePeerMessageError; fn try_from(x: &proto::PeerMessage) -> Result { Ok(match x.message_type.as_ref().ok_or(Self::Error::Empty)? { - ProtoMT::Handshake(h) => { - PeerMessage::Handshake(h.try_into().map_err(Self::Error::Handshake)?) + ProtoMT::Tier1Handshake(h) => { + PeerMessage::Tier1Handshake(h.try_into().map_err(Self::Error::Handshake)?) + } + ProtoMT::Tier2Handshake(h) => { + PeerMessage::Tier2Handshake(h.try_into().map_err(Self::Error::Handshake)?) } ProtoMT::HandshakeFailure(hf) => { let (pi, hfr) = hf.try_into().map_err(Self::Error::HandshakeFailure)?; diff --git a/chain/network/src/network_protocol/tests.rs b/chain/network/src/network_protocol/tests.rs index d6c4d7a192e..c41037960b3 100644 --- a/chain/network/src/network_protocol/tests.rs +++ b/chain/network/src/network_protocol/tests.rs @@ -57,14 +57,18 @@ fn bad_account_data_size() { #[test] fn serialize_deserialize_protobuf_only() { let mut rng = make_rng(39521947542); - let clock = time::FakeClock::default(); - let msgs = [PeerMessage::SyncAccountsData(SyncAccountsData { - accounts_data: (0..4) - .map(|_| Arc::new(data::make_signed_account_data(&mut rng, &clock.clock()))) - .collect(), - incremental: true, - requesting_full_sync: true, - })]; + let mut clock = time::FakeClock::default(); + let chain = data::Chain::make(&mut clock, &mut rng, 12); + let msgs = [ + PeerMessage::Tier1Handshake(data::make_handshake(&mut rng, &chain)), + PeerMessage::SyncAccountsData(SyncAccountsData { + accounts_data: (0..4) + .map(|_| Arc::new(data::make_signed_account_data(&mut rng, &clock.clock()))) + .collect(), + incremental: true, + requesting_full_sync: true, + }), + ]; for m in msgs { let m2 = PeerMessage::deserialize(Encoding::Proto, &m.serialize(Encoding::Proto)) .with_context(|| m.to_string()) @@ -101,7 +105,7 @@ fn serialize_deserialize() -> anyhow::Result<()> { }), )); let msgs = [ - PeerMessage::Handshake(data::make_handshake(&mut rng, &chain)), + PeerMessage::Tier2Handshake(data::make_handshake(&mut rng, &chain)), PeerMessage::HandshakeFailure( data::make_peer_info(&mut rng), HandshakeFailureReason::InvalidTarget, diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index dbff2793440..bff81a300ea 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -347,7 +347,7 @@ impl PeerActor { }, partial_edge_info: spec.partial_edge_info, }; - let msg = PeerMessage::Handshake(handshake); + let msg = PeerMessage::Tier2Handshake(handshake); self.send_message_or_log(&msg); } @@ -781,7 +781,7 @@ impl PeerActor { actix::fut::ready(()) })); } - (PeerStatus::Connecting { .. }, PeerMessage::Handshake(msg)) => { + (PeerStatus::Connecting { .. }, PeerMessage::Tier2Handshake(msg)) => { self.process_handshake(ctx, msg) } (_, msg) => { @@ -962,7 +962,7 @@ impl PeerActor { tracing::debug!(target: "network", "Disconnect signal. Me: {:?} Peer: {:?}", self.my_node_info.id, self.other_peer_id()); self.stop(ctx, ClosingReason::DisconnectMessage); } - PeerMessage::Handshake(_) => { + PeerMessage::Tier2Handshake(_) => { // Received handshake after already have seen handshake from this peer. tracing::debug!(target: "network", "Duplicate handshake from {}", self.peer_info); } diff --git a/chain/network/src/peer/tests/communication.rs b/chain/network/src/peer/tests/communication.rs index 0e9e26080db..cd644cfda12 100644 --- a/chain/network/src/peer/tests/communication.rs +++ b/chain/network/src/peer/tests/communication.rs @@ -207,7 +207,7 @@ async fn test_handshake(outbound_encoding: Option, inbound_encoding: O }; // We will also introduce chain_id mismatch, but ProtocolVersionMismatch is expected to take priority. handshake.sender_chain_info.genesis_id.chain_id = "unknown_chain".to_string(); - outbound.write(&PeerMessage::Handshake(handshake.clone())).await; + outbound.write(&PeerMessage::Tier2Handshake(handshake.clone())).await; let resp = outbound.read().await.unwrap(); assert_matches!( resp, @@ -217,7 +217,7 @@ async fn test_handshake(outbound_encoding: Option, inbound_encoding: O // Send too new PROTOCOL_VERSION, expect ProtocolVersionMismatch handshake.protocol_version = PROTOCOL_VERSION + 1; handshake.oldest_supported_version = PROTOCOL_VERSION + 1; - outbound.write(&PeerMessage::Handshake(handshake.clone())).await; + outbound.write(&PeerMessage::Tier2Handshake(handshake.clone())).await; let resp = outbound.read().await.unwrap(); assert_matches!( resp, @@ -228,7 +228,7 @@ async fn test_handshake(outbound_encoding: Option, inbound_encoding: O // We fix protocol_version, but chain_id is still mismatching. handshake.protocol_version = PROTOCOL_VERSION; handshake.oldest_supported_version = PROTOCOL_VERSION; - outbound.write(&PeerMessage::Handshake(handshake.clone())).await; + outbound.write(&PeerMessage::Tier2Handshake(handshake.clone())).await; let resp = outbound.read().await.unwrap(); assert_matches!( resp, @@ -237,9 +237,9 @@ async fn test_handshake(outbound_encoding: Option, inbound_encoding: O // Send a correct Handshake, expect a matching Handshake response. handshake.sender_chain_info = chain.get_peer_chain_info(); - outbound.write(&PeerMessage::Handshake(handshake.clone())).await; + outbound.write(&PeerMessage::Tier2Handshake(handshake.clone())).await; let resp = outbound.read().await.unwrap(); - assert_matches!(resp, PeerMessage::Handshake(_)); + assert_matches!(resp, PeerMessage::Tier2Handshake(_)); } #[tokio::test] diff --git a/chain/network/src/peer_manager/tests/connection_pool.rs b/chain/network/src/peer_manager/tests/connection_pool.rs index 3177457c132..9fb72343ffb 100644 --- a/chain/network/src/peer_manager/tests/connection_pool.rs +++ b/chain/network/src/peer_manager/tests/connection_pool.rs @@ -86,7 +86,7 @@ async fn loop_connection() { let mut events = pm.events.from_now(); let mut stream = Stream::new(Some(Encoding::Proto), stream); stream - .write(&PeerMessage::Handshake(Handshake { + .write(&PeerMessage::Tier2Handshake(Handshake { protocol_version: PROTOCOL_VERSION, oldest_supported_version: PROTOCOL_VERSION, sender_peer_id: pm.cfg.node_id(), diff --git a/chain/network/src/peer_manager/tests/nonce.rs b/chain/network/src/peer_manager/tests/nonce.rs index f65bb6d2ac0..e310e9dea70 100644 --- a/chain/network/src/peer_manager/tests/nonce.rs +++ b/chain/network/src/peer_manager/tests/nonce.rs @@ -64,7 +64,7 @@ async fn test_nonces() { let mut stream = stream::Stream::new(Some(Encoding::Proto), stream); let peer_key = data::make_secret_key(rng); let peer_id = PeerId::new(peer_key.public_key()); - let handshake = PeerMessage::Handshake(Handshake { + let handshake = PeerMessage::Tier2Handshake(Handshake { protocol_version: version::PROTOCOL_VERSION, oldest_supported_version: version::PEER_MIN_ALLOWED_PROTOCOL_VERSION, sender_peer_id: peer_id.clone(), @@ -78,7 +78,7 @@ async fn test_nonces() { stream.write(&handshake).await; if test.1 { match stream.read().await { - Ok(PeerMessage::Handshake { .. }) => {} + Ok(PeerMessage::Tier2Handshake { .. }) => {} got => panic!("got = {got:?}, want Handshake"), } } else { diff --git a/chain/network/src/raw/connection.rs b/chain/network/src/raw/connection.rs index 1f4ba892bd3..d943c81efa8 100644 --- a/chain/network/src/raw/connection.rs +++ b/chain/network/src/raw/connection.rs @@ -129,7 +129,7 @@ impl Connection { genesis_hash: CryptoHash, head_height: BlockHeight, ) -> Result<(), ConnectError> { - let handshake = PeerMessage::Handshake(Handshake { + let handshake = PeerMessage::Tier2Handshake(Handshake { protocol_version, oldest_supported_version: protocol_version - 2, sender_peer_id: self.my_peer_id.clone(), @@ -159,7 +159,7 @@ impl Connection { match message { // TODO: maybe check the handshake for sanity - PeerMessage::Handshake(_) => { + PeerMessage::Tier2Handshake(_) => { tracing::info!(target: "network", "handshake latency: {}", timestamp - start); } PeerMessage::HandshakeFailure(_peer_info, reason) => { From 864d9b1185c740084fce404d85e82cfa33210420 Mon Sep 17 00:00:00 2001 From: nikurt <86772482+nikurt@users.noreply.github.com> Date: Fri, 25 Nov 2022 16:32:30 +0100 Subject: [PATCH 035/188] Easy epoch selection for dumping state parts and log the number of nodes visited (#8102) This lets us easily script a tool to dump the state of each epoch, without running extra commands to find the correct `--block-hash` value. --- core/store/src/trie/state_parts.rs | 6 +- nearcore/src/runtime/mod.rs | 3 +- tools/state-viewer/src/cli.rs | 10 +- tools/state-viewer/src/dump_state_parts.rs | 128 +++++++++++++++++++-- tools/state-viewer/src/epoch_info.rs | 5 +- 5 files changed, 133 insertions(+), 19 deletions(-) diff --git a/core/store/src/trie/state_parts.rs b/core/store/src/trie/state_parts.rs index a4a263ebaf3..414327a2236 100644 --- a/core/store/src/trie/state_parts.rs +++ b/core/store/src/trie/state_parts.rs @@ -44,8 +44,12 @@ impl Trie { fn visit_nodes_for_state_part(&self, part_id: PartId) -> Result<(), StorageError> { let path_begin = self.find_path_for_part_boundary(part_id.idx, part_id.total)?; let path_end = self.find_path_for_part_boundary(part_id.idx + 1, part_id.total)?; + let mut iterator = self.iter()?; - iterator.visit_nodes_interval(&path_begin, &path_end)?; + let nodes_list = iterator.visit_nodes_interval(&path_begin, &path_end)?; + tracing::debug!( + target: "state_parts", + num_nodes = nodes_list.len()); // Extra nodes for compatibility with the previous version of computing state parts if part_id.idx + 1 != part_id.total { diff --git a/nearcore/src/runtime/mod.rs b/nearcore/src/runtime/mod.rs index 4209f768664..70166c3737a 100644 --- a/nearcore/src/runtime/mod.rs +++ b/nearcore/src/runtime/mod.rs @@ -1206,9 +1206,10 @@ impl RuntimeAdapter for NightshadeRuntime { let _span = tracing::debug_span!( target: "runtime", "obtain_state_part", + part_id = part_id.idx, shard_id, %block_hash, - ?part_id) + num_parts = part_id.total) .entered(); let epoch_id = self.get_epoch_id(block_hash)?; let shard_uid = self.get_shard_uid_from_epoch_id(shard_id, &epoch_id)?; diff --git a/tools/state-viewer/src/cli.rs b/tools/state-viewer/src/cli.rs index 0a1715a8d1d..6a12f2278f4 100644 --- a/tools/state-viewer/src/cli.rs +++ b/tools/state-viewer/src/cli.rs @@ -1,7 +1,7 @@ use crate::commands::*; use crate::dump_state_parts::dump_state_parts; -use crate::epoch_info; use crate::rocksdb_stats::get_rocksdb_stats; +use crate::{dump_state_parts, epoch_info}; use clap::{Args, Parser, Subcommand}; use near_chain_configs::{GenesisChangeConfig, GenesisValidationMode}; use near_primitives::account::id::AccountId; @@ -477,9 +477,9 @@ impl ViewTrieCmd { #[derive(Parser)] pub struct DumpStatePartsCmd { - /// Last block of a previous epoch. - #[clap(long)] - block_hash: CryptoHash, + /// Selects an epoch. The dump will be of the state at the beginning of this epoch. + #[clap(subcommand)] + epoch_selection: dump_state_parts::EpochSelection, /// Shard id. #[clap(long)] shard_id: ShardId, @@ -494,7 +494,7 @@ pub struct DumpStatePartsCmd { impl DumpStatePartsCmd { pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { dump_state_parts( - self.block_hash, + self.epoch_selection, self.shard_id, self.part_id, home_dir, diff --git a/tools/state-viewer/src/dump_state_parts.rs b/tools/state-viewer/src/dump_state_parts.rs index 50d1eaaddec..8457e42f083 100644 --- a/tools/state-viewer/src/dump_state_parts.rs +++ b/tools/state-viewer/src/dump_state_parts.rs @@ -1,15 +1,108 @@ +use crate::epoch_info::iterate_and_filter; +use clap::Subcommand; use near_chain::{ChainStore, ChainStoreAccess, RuntimeAdapter}; +use near_epoch_manager::EpochManager; +use near_primitives::epoch_manager::epoch_info::EpochInfo; use near_primitives::state_part::PartId; use near_primitives::syncing::get_num_state_parts; +use near_primitives::types::EpochId; use near_primitives_core::hash::CryptoHash; -use near_primitives_core::types::ShardId; +use near_primitives_core::types::{BlockHeight, EpochHeight, ShardId}; use near_store::Store; use nearcore::{NearConfig, NightshadeRuntime}; use std::path::Path; +use std::str::FromStr; use std::sync::Arc; +use std::time::Instant; + +#[derive(Subcommand, Debug, Clone)] +pub(crate) enum EpochSelection { + /// Current epoch. + Current, + /// Fetch the given epoch. + EpochId { epoch_id: String }, + /// Fetch epochs at the given height. + EpochHeight { epoch_height: EpochHeight }, + /// Fetch an epoch containing the given block hash. + BlockHash { block_hash: String }, + /// Fetch an epoch containing the given block height. + BlockHeight { block_height: BlockHeight }, +} + +impl EpochSelection { + pub fn to_epoch_id( + &self, + store: Store, + chain_store: &ChainStore, + epoch_manager: &EpochManager, + ) -> EpochId { + match self { + EpochSelection::Current => { + epoch_manager.get_epoch_id(&chain_store.head().unwrap().last_block_hash).unwrap() + } + EpochSelection::EpochId { epoch_id } => { + EpochId(CryptoHash::from_str(&epoch_id).unwrap()) + } + EpochSelection::EpochHeight { epoch_height } => { + // Fetch epochs at the given height. + // There should only be one epoch at a given height. But this is a debug tool, let's check + // if there are multiple epochs at a given height. + let epoch_ids = iterate_and_filter(store, |epoch_info| { + epoch_info.epoch_height() == *epoch_height + }); + assert_eq!(epoch_ids.len(), 1, "{:#?}", epoch_ids); + epoch_ids[0].clone() + } + EpochSelection::BlockHash { block_hash } => { + let block_hash = CryptoHash::from_str(&block_hash).unwrap(); + epoch_manager.get_epoch_id(&block_hash).unwrap() + } + EpochSelection::BlockHeight { block_height } => { + // Fetch an epoch containing the given block height. + let block_hash = chain_store.get_block_hash_by_height(*block_height).unwrap(); + epoch_manager.get_epoch_id(&block_hash).unwrap() + } + } + } +} + +/// Returns block hash of the last block of an epoch preceding the given `epoch_info`. +fn get_prev_hash_of_epoch( + epoch_info: &EpochInfo, + chain_store: &ChainStore, + epoch_manager: &EpochManager, +) -> CryptoHash { + let head = chain_store.head().unwrap(); + let mut cur_block_info = epoch_manager.get_block_info(&head.last_block_hash).unwrap(); + // EpochManager doesn't have an API that maps EpochId to Blocks, and this function works + // around that limitation by iterating over the epochs. + // This workaround is acceptable because: + // 1) Extending EpochManager's API is a major change. + // 2) This use case is not critical at all. + loop { + let cur_epoch_info = epoch_manager.get_epoch_info(cur_block_info.epoch_id()).unwrap(); + let cur_epoch_height = cur_epoch_info.epoch_height(); + assert!( + cur_epoch_height >= epoch_info.epoch_height(), + "cur_block_info: {:#?}, epoch_info.epoch_height: {}", + cur_block_info, + epoch_info.epoch_height() + ); + let epoch_first_block_info = + epoch_manager.get_block_info(cur_block_info.epoch_first_block()).unwrap(); + let prev_epoch_last_block_info = + epoch_manager.get_block_info(epoch_first_block_info.prev_hash()).unwrap(); + + if cur_epoch_height == epoch_info.epoch_height() { + return *prev_epoch_last_block_info.hash(); + } + + cur_block_info = prev_epoch_last_block_info; + } +} pub(crate) fn dump_state_parts( - sync_prev_hash: CryptoHash, + epoch_selection: EpochSelection, shard_id: ShardId, part_id: Option, home_dir: &Path, @@ -19,17 +112,21 @@ pub(crate) fn dump_state_parts( ) { let runtime_adapter: Arc = Arc::new(NightshadeRuntime::from_config(home_dir, store.clone(), &near_config)); - - let chain_store = ChainStore::new( + let mut epoch_manager = + EpochManager::new_from_genesis_config(store.clone(), &near_config.genesis.config) + .expect("Failed to start Epoch Manager"); + let mut chain_store = ChainStore::new( store.clone(), near_config.genesis.config.genesis_height, !near_config.client_config.archive, ); + let epoch_id = epoch_selection.to_epoch_id(store, &mut chain_store, &mut epoch_manager); + let epoch = runtime_adapter.get_epoch_info(&epoch_id).unwrap(); + let sync_prev_hash = get_prev_hash_of_epoch(&epoch, &mut chain_store, &mut epoch_manager); let sync_prev_block = chain_store.get_block(&sync_prev_hash).unwrap(); assert!(runtime_adapter.is_next_block_epoch_start(&sync_prev_hash).unwrap()); - assert!( shard_id < sync_prev_block.chunks().len() as u64, "shard_id: {}, #shards: {}", @@ -41,10 +138,19 @@ pub(crate) fn dump_state_parts( runtime_adapter.get_state_root_node(shard_id, &sync_prev_hash, &state_root).unwrap(); let num_parts = get_num_state_parts(state_root_node.memory_usage); - tracing::debug!(num_parts); + tracing::info!( + target: "dump-state-parts", + epoch_height = epoch.epoch_height(), + epoch_id = ?epoch_id.0, + shard_id, + num_parts, + ?sync_prev_hash, + "Dumping state as seen at the beginning of the specified epoch.", + ); std::fs::create_dir_all(output_dir).unwrap(); for part_id in if let Some(part_id) = part_id { part_id..part_id + 1 } else { 0..num_parts } { + let now = Instant::now(); assert!(part_id < num_parts, "part_id: {}, num_parts: {}", part_id, num_parts); let state_part = runtime_adapter .obtain_state_part( @@ -57,11 +163,11 @@ pub(crate) fn dump_state_parts( let filename = output_dir.join(format!("state_part_{:06}", part_id)); let len = state_part.len(); std::fs::write(&filename, state_part).unwrap(); - tracing::debug!( - "part_id: {}, result length: {}, wrote {}", + tracing::info!( + target: "dump-state-parts", part_id, - len, - filename.display() - ); + part_length = len, + ?filename, + elapsed_sec = now.elapsed().as_secs_f64()); } } diff --git a/tools/state-viewer/src/epoch_info.rs b/tools/state-viewer/src/epoch_info.rs index 67eb2d694bf..0f8bb02a5b9 100644 --- a/tools/state-viewer/src/epoch_info.rs +++ b/tools/state-viewer/src/epoch_info.rs @@ -183,7 +183,10 @@ fn get_epoch_ids( // Iterates over the DBCol::EpochInfo column, ignores AGGREGATOR_KEY and returns deserialized EpochId // for EpochInfos that satisfy the given predicate. -fn iterate_and_filter(store: Store, predicate: impl Fn(EpochInfo) -> bool) -> Vec { +pub(crate) fn iterate_and_filter( + store: Store, + predicate: impl Fn(EpochInfo) -> bool, +) -> Vec { store .iter(DBCol::EpochInfo) .map(Result::unwrap) From b60fb4e72dfa32dc2d770da2393fb603856d5538 Mon Sep 17 00:00:00 2001 From: Saketh Are Date: Fri, 25 Nov 2022 11:31:59 -0500 Subject: [PATCH 036/188] Migrate RoutingTable tests to network crate (#8073) These tests create some small network topologies and verify that the routing tables are populated correctly. This PR migrates them from integration-tests into the chain/network crate. The goals for this migration are to: - utilize the internal APIs of the near-network crate - make the tests await the changes rather than poll - to decrease the public API surface of the near-network crate --- .../network/src/peer_manager/tests/routing.rs | 251 ++++++++++++++++++ .../src/tests/network/routing.rs | 76 ------ 2 files changed, 251 insertions(+), 76 deletions(-) diff --git a/chain/network/src/peer_manager/tests/routing.rs b/chain/network/src/peer_manager/tests/routing.rs index 0dac8a239a8..50ced9ef6f2 100644 --- a/chain/network/src/peer_manager/tests/routing.rs +++ b/chain/network/src/peer_manager/tests/routing.rs @@ -18,6 +18,257 @@ use rand::Rng as _; use std::collections::HashSet; use std::sync::Arc; +// test routing in a two-node network before and after connecting the nodes +#[tokio::test] +async fn simple() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + tracing::info!(target:"test", "start two nodes"); + let pm0 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm1 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + + let id0 = pm0.cfg.node_id(); + let id1 = pm1.cfg.node_id(); + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[]).await; + tracing::info!(target:"test", "wait for {id1} routing table"); + pm1.wait_for_routing_table(&[]).await; + + tracing::info!(target:"test", "connect the nodes"); + pm0.connect_to(&pm1.peer_info()).await; + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[(id1.clone(), vec![id1.clone()])]).await; + tracing::info!(target:"test", "wait for {id1} routing table"); + pm1.wait_for_routing_table(&[(id0.clone(), vec![id0.clone()])]).await; +} + +// test routing for three nodes in a line +#[tokio::test] +async fn three_nodes_path() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + tracing::info!(target:"test", "connect three nodes in a line"); + let pm0 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm1 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm2 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + + pm0.connect_to(&pm1.peer_info()).await; + pm1.connect_to(&pm2.peer_info()).await; + + let id0 = pm0.cfg.node_id(); + let id1 = pm1.cfg.node_id(); + let id2 = pm2.cfg.node_id(); + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[ + (id1.clone(), vec![id1.clone()]), + (id2.clone(), vec![id1.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id1} routing table"); + pm1.wait_for_routing_table(&[ + (id0.clone(), vec![id0.clone()]), + (id2.clone(), vec![id2.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id2} routing table"); + pm2.wait_for_routing_table(&[ + (id0.clone(), vec![id1.clone()]), + (id1.clone(), vec![id1.clone()]), + ]) + .await; +} + +// test routing for three nodes in a line, then test routing after completing the triangle +#[tokio::test] +async fn three_nodes_star() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + tracing::info!(target:"test", "connect three nodes in a line"); + let pm0 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm1 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm2 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + + pm0.connect_to(&pm1.peer_info()).await; + pm1.connect_to(&pm2.peer_info()).await; + + let id0 = pm0.cfg.node_id(); + let id1 = pm1.cfg.node_id(); + let id2 = pm2.cfg.node_id(); + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[ + (id1.clone(), vec![id1.clone()]), + (id2.clone(), vec![id1.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id1} routing table"); + pm1.wait_for_routing_table(&[ + (id0.clone(), vec![id0.clone()]), + (id2.clone(), vec![id2.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id2} routing table"); + pm2.wait_for_routing_table(&[ + (id0.clone(), vec![id1.clone()]), + (id1.clone(), vec![id1.clone()]), + ]) + .await; + + tracing::info!(target:"test", "connect {id0} and {id2}"); + pm0.connect_to(&pm2.peer_info()).await; + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[ + (id1.clone(), vec![id1.clone()]), + (id2.clone(), vec![id2.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id1} routing table"); + pm1.wait_for_routing_table(&[ + (id0.clone(), vec![id0.clone()]), + (id2.clone(), vec![id2.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id2} routing table"); + pm2.wait_for_routing_table(&[ + (id0.clone(), vec![id0.clone()]), + (id1.clone(), vec![id1.clone()]), + ]) + .await; +} + +// test routing in a network with two connected components having two nodes each, +// then test routing after joining them into a square +#[tokio::test] +async fn join_components() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + tracing::info!(target:"test", "create two connected components having two nodes each"); + let pm0 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm1 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm2 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm3 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + + let id0 = pm0.cfg.node_id(); + let id1 = pm1.cfg.node_id(); + let id2 = pm2.cfg.node_id(); + let id3 = pm3.cfg.node_id(); + + pm0.connect_to(&pm1.peer_info()).await; + pm2.connect_to(&pm3.peer_info()).await; + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[(id1.clone(), vec![id1.clone()])]).await; + tracing::info!(target:"test", "wait for {id1} routing table"); + pm1.wait_for_routing_table(&[(id0.clone(), vec![id0.clone()])]).await; + + tracing::info!(target:"test", "wait for {id2} routing table"); + pm2.wait_for_routing_table(&[(id3.clone(), vec![id3.clone()])]).await; + tracing::info!(target:"test", "wait for {id3} routing table"); + pm3.wait_for_routing_table(&[(id2.clone(), vec![id2.clone()])]).await; + + tracing::info!(target:"test", "join the two components into a square"); + pm0.connect_to(&pm2.peer_info()).await; + pm3.connect_to(&pm1.peer_info()).await; + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[ + (id1.clone(), vec![id1.clone()]), + (id2.clone(), vec![id2.clone()]), + (id3.clone(), vec![id1.clone(), id2.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id1} routing table"); + pm1.wait_for_routing_table(&[ + (id0.clone(), vec![id0.clone()]), + (id2.clone(), vec![id0.clone(), id3.clone()]), + (id3.clone(), vec![id3.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id2} routing table"); + pm2.wait_for_routing_table(&[ + (id0.clone(), vec![id0.clone()]), + (id1.clone(), vec![id0.clone(), id3.clone()]), + (id3.clone(), vec![id3.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id3} routing table"); + pm3.wait_for_routing_table(&[ + (id0.clone(), vec![id1.clone(), id2.clone()]), + (id1.clone(), vec![id1.clone()]), + (id2.clone(), vec![id2.clone()]), + ]) + .await; +} + +// test routing for three nodes in a line, then test dropping the middle node +#[tokio::test] +async fn simple_remove() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + tracing::info!(target:"test", "connect 3 nodes in a line"); + let pm0 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm1 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm2 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + + pm0.connect_to(&pm1.peer_info()).await; + pm1.connect_to(&pm2.peer_info()).await; + + let id0 = pm0.cfg.node_id(); + let id1 = pm1.cfg.node_id(); + let id2 = pm2.cfg.node_id(); + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[ + (id1.clone(), vec![id1.clone()]), + (id2.clone(), vec![id1.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id1} routing table"); + pm1.wait_for_routing_table(&[ + (id0.clone(), vec![id0.clone()]), + (id2.clone(), vec![id2.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id2} routing table"); + pm2.wait_for_routing_table(&[ + (id0.clone(), vec![id1.clone()]), + (id1.clone(), vec![id1.clone()]), + ]) + .await; + + tracing::info!(target:"test","stop {id1}"); + drop(pm1); + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[]).await; + tracing::info!(target:"test", "wait for {id2} routing table"); + pm2.wait_for_routing_table(&[]).await; +} + // test that TTL is handled property. #[tokio::test] async fn ttl() { diff --git a/integration-tests/src/tests/network/routing.rs b/integration-tests/src/tests/network/routing.rs index c4a7d4f20ff..95341731c4b 100644 --- a/integration-tests/src/tests/network/routing.rs +++ b/integration-tests/src/tests/network/routing.rs @@ -1,17 +1,6 @@ use crate::tests::network::runner::*; use near_network::time; -#[test] -fn simple() -> anyhow::Result<()> { - let mut runner = Runner::new(2, 1); - - runner.push(Action::AddEdge { from: 0, to: 1, force: true }); - runner.push(Action::CheckRoutingTable(0, vec![(1, vec![1])])); - runner.push(Action::CheckRoutingTable(1, vec![(0, vec![0])])); - - start_test(runner) -} - #[test] fn from_boot_nodes() -> anyhow::Result<()> { let mut runner = Runner::new(2, 1).use_boot_nodes(vec![0]).enable_outbound(); @@ -22,56 +11,6 @@ fn from_boot_nodes() -> anyhow::Result<()> { start_test(runner) } -#[test] -fn three_nodes_path() -> anyhow::Result<()> { - let mut runner = Runner::new(3, 2); - - runner.push(Action::AddEdge { from: 0, to: 1, force: true }); - runner.push(Action::AddEdge { from: 1, to: 2, force: true }); - runner.push(Action::CheckRoutingTable(1, vec![(0, vec![0]), (2, vec![2])])); - runner.push(Action::CheckRoutingTable(0, vec![(1, vec![1]), (2, vec![1])])); - runner.push(Action::CheckRoutingTable(2, vec![(1, vec![1]), (0, vec![1])])); - - start_test(runner) -} - -#[test] -fn three_nodes_star() -> anyhow::Result<()> { - let mut runner = Runner::new(3, 2); - - runner.push(Action::AddEdge { from: 0, to: 1, force: true }); - runner.push(Action::AddEdge { from: 1, to: 2, force: true }); - runner.push(Action::CheckRoutingTable(1, vec![(0, vec![0]), (2, vec![2])])); - runner.push(Action::CheckRoutingTable(0, vec![(1, vec![1]), (2, vec![1])])); - runner.push(Action::CheckRoutingTable(2, vec![(1, vec![1]), (0, vec![1])])); - runner.push(Action::AddEdge { from: 0, to: 2, force: true }); - runner.push(Action::CheckRoutingTable(1, vec![(0, vec![0]), (2, vec![2])])); - runner.push(Action::CheckRoutingTable(0, vec![(1, vec![1]), (2, vec![2])])); - runner.push(Action::CheckRoutingTable(2, vec![(1, vec![1]), (0, vec![0])])); - - start_test(runner) -} - -#[test] -fn join_components() -> anyhow::Result<()> { - let mut runner = Runner::new(4, 4); - - runner.push(Action::AddEdge { from: 0, to: 1, force: true }); - runner.push(Action::AddEdge { from: 2, to: 3, force: true }); - runner.push(Action::CheckRoutingTable(0, vec![(1, vec![1])])); - runner.push(Action::CheckRoutingTable(1, vec![(0, vec![0])])); - runner.push(Action::CheckRoutingTable(2, vec![(3, vec![3])])); - runner.push(Action::CheckRoutingTable(3, vec![(2, vec![2])])); - runner.push(Action::AddEdge { from: 0, to: 2, force: true }); - runner.push(Action::AddEdge { from: 3, to: 1, force: true }); - runner.push(Action::CheckRoutingTable(0, vec![(1, vec![1]), (2, vec![2]), (3, vec![1, 2])])); - runner.push(Action::CheckRoutingTable(3, vec![(1, vec![1]), (2, vec![2]), (0, vec![1, 2])])); - runner.push(Action::CheckRoutingTable(1, vec![(0, vec![0]), (3, vec![3]), (2, vec![0, 3])])); - runner.push(Action::CheckRoutingTable(2, vec![(0, vec![0]), (3, vec![3]), (1, vec![0, 3])])); - - start_test(runner) -} - #[test] fn account_propagation() -> anyhow::Result<()> { let mut runner = Runner::new(3, 2); @@ -166,21 +105,6 @@ fn test_drop_after_ttl() -> anyhow::Result<()> { start_test(runner) } -#[test] -fn simple_remove() -> anyhow::Result<()> { - let mut runner = Runner::new(3, 3); - - runner.push(Action::AddEdge { from: 0, to: 1, force: true }); - runner.push(Action::AddEdge { from: 1, to: 2, force: true }); - runner.push(Action::CheckRoutingTable(0, vec![(1, vec![1]), (2, vec![1])])); - runner.push(Action::CheckRoutingTable(2, vec![(1, vec![1]), (0, vec![1])])); - runner.push(Action::Stop(1)); - runner.push(Action::CheckRoutingTable(0, vec![])); - runner.push(Action::CheckRoutingTable(2, vec![])); - - start_test(runner) -} - #[test] fn blacklist_01() -> anyhow::Result<()> { let mut runner = Runner::new(2, 2).add_to_blacklist(0, Some(1)).use_boot_nodes(vec![0]); From 313693c442c3081537b51ebc9044800d590aea6b Mon Sep 17 00:00:00 2001 From: Saketh Are Date: Fri, 25 Nov 2022 12:00:00 -0500 Subject: [PATCH 037/188] Migrate ping and ttl tests to network crate (#8110) These tests verify the behavior for basic pings, including interactions with TTL. This PR migrates them from integration-tests into the chain/network crate. The goals for this migration are to: -utilize the internal APIs of the near-network crate -make the tests await the changes rather than poll -to decrease the public API surface of the near-network crate --- chain/network/src/peer/peer_actor.rs | 1 + chain/network/src/peer_manager/testonly.rs | 8 + .../network/src/peer_manager/tests/routing.rs | 321 +++++++++++++++++- integration-tests/src/tests/network/mod.rs | 1 - .../src/tests/network/multiset.rs | 36 -- .../src/tests/network/routing.rs | 128 ------- integration-tests/src/tests/network/runner.rs | 86 +---- 7 files changed, 331 insertions(+), 250 deletions(-) delete mode 100644 integration-tests/src/tests/network/multiset.rs diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index bff81a300ea..fbf9ac43d83 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -1302,6 +1302,7 @@ impl actix::Handler for PeerActor { if let Some(&t) = self.routed_message_cache.get(&key) { if now <= t + DROP_DUPLICATED_MESSAGES_PERIOD { tracing::debug!(target: "network", "Dropping duplicated message from {} to {:?}", msg.author, msg.target); + self.network_state.config.event_sink.push(Event::RoutedMessageDropped); return; } } diff --git a/chain/network/src/peer_manager/testonly.rs b/chain/network/src/peer_manager/testonly.rs index c34821d41a5..be51154dd98 100644 --- a/chain/network/src/peer_manager/testonly.rs +++ b/chain/network/src/peer_manager/testonly.rs @@ -306,6 +306,14 @@ impl ActorHandler { self.with_state(move |s| async move { s.tier1_advertise_proxies(&clock).await }).await } + pub async fn send_ping(&self, nonce: u64, target: PeerId) { + self.actix + .addr + .send(PeerManagerMessageRequest::PingTo { nonce, target }.with_span_context()) + .await + .unwrap(); + } + pub async fn announce_account(&self, aa: AnnounceAccount) { self.actix .addr diff --git a/chain/network/src/peer_manager/tests/routing.rs b/chain/network/src/peer_manager/tests/routing.rs index 50ced9ef6f2..5ca07fc1fa7 100644 --- a/chain/network/src/peer_manager/tests/routing.rs +++ b/chain/network/src/peer_manager/tests/routing.rs @@ -1,6 +1,6 @@ use crate::broadcast; use crate::network_protocol::testonly as data; -use crate::network_protocol::{Edge, Encoding, Ping, RoutedMessageBody, RoutingTableUpdate}; +use crate::network_protocol::{Edge, Encoding, Ping, Pong, RoutedMessageBody, RoutingTableUpdate}; use crate::peer; use crate::peer_manager; use crate::peer_manager::peer_manager_actor::Event as PME; @@ -269,6 +269,325 @@ async fn simple_remove() { pm2.wait_for_routing_table(&[]).await; } +// Awaits until the expected ping is seen in the event stream. +pub async fn wait_for_ping(events: &mut broadcast::Receiver, want_ping: Ping) { + events + .recv_until(|ev| match ev { + Event::PeerManager(PME::Ping(ping)) => { + if ping == want_ping { + Some(()) + } else { + None + } + } + _ => None, + }) + .await; +} + +// Awaits until the expected pong is seen in the event stream. +pub async fn wait_for_pong(events: &mut broadcast::Receiver, want_pong: Pong) { + events + .recv_until(|ev| match ev { + Event::PeerManager(PME::Pong(pong)) => { + if pong == want_pong { + Some(()) + } else { + None + } + } + _ => None, + }) + .await; +} + +// Awaits until RoutedMessageDropped is seen in the event stream. +pub async fn wait_for_message_dropped(events: &mut broadcast::Receiver) { + events + .recv_until(|ev| match ev { + Event::PeerManager(PME::RoutedMessageDropped) => Some(()), + _ => None, + }) + .await; +} + +// test ping in a two-node network +#[tokio::test] +async fn ping_simple() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + tracing::info!(target:"test", "start two nodes"); + let pm0 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm1 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + + let id0 = pm0.cfg.node_id(); + let id1 = pm1.cfg.node_id(); + + pm0.connect_to(&pm1.peer_info()).await; + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[(id1.clone(), vec![id1.clone()])]).await; + + // capture event streams before pinging + let mut pm0_ev = pm0.events.from_now(); + let mut pm1_ev = pm1.events.from_now(); + + tracing::info!(target:"test", "send ping from {id0} to {id1}"); + pm0.send_ping(0, id1.clone()).await; + + tracing::info!(target:"test", "await ping at {id1}"); + wait_for_ping(&mut pm1_ev, Ping { nonce: 0, source: id0.clone() }).await; + + tracing::info!(target:"test", "await pong at {id0}"); + wait_for_pong(&mut pm0_ev, Pong { nonce: 0, source: id1.clone() }).await; +} + +// test ping without a direct connection +#[tokio::test] +async fn ping_jump() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + tracing::info!(target:"test", "start three nodes"); + let pm0 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm1 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm2 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + + let id0 = pm0.cfg.node_id(); + let id1 = pm1.cfg.node_id(); + let id2 = pm2.cfg.node_id(); + + tracing::info!(target:"test", "connect nodes in a line"); + pm0.connect_to(&pm1.peer_info()).await; + pm1.connect_to(&pm2.peer_info()).await; + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[ + (id1.clone(), vec![id1.clone()]), + (id2.clone(), vec![id1.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id1} routing table"); + pm1.wait_for_routing_table(&[ + (id0.clone(), vec![id0.clone()]), + (id2.clone(), vec![id2.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id2} routing table"); + pm2.wait_for_routing_table(&[ + (id0.clone(), vec![id1.clone()]), + (id1.clone(), vec![id1.clone()]), + ]) + .await; + + // capture event streams before pinging + let mut pm0_ev = pm0.events.from_now(); + let mut pm2_ev = pm2.events.from_now(); + + tracing::info!(target:"test", "send ping from {id0} to {id2}"); + pm0.send_ping(0, id2.clone()).await; + + tracing::info!(target:"test", "await ping at {id2}"); + wait_for_ping(&mut pm2_ev, Ping { nonce: 0, source: id0.clone() }).await; + + tracing::info!(target:"test", "await pong at {id0}"); + wait_for_pong(&mut pm0_ev, Pong { nonce: 0, source: id2.clone() }).await; +} + +// test that ping over an indirect connection with ttl=2 is delivered +#[tokio::test] +async fn test_dont_drop_after_ttl() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + tracing::info!(target:"test", "start three nodes"); + let mut cfg0 = chain.make_config(rng); + cfg0.routed_message_ttl = 2; + let pm0 = start_pm(clock.clock(), TestDB::new(), cfg0, chain.clone()).await; + let pm1 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm2 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + + let id0 = pm0.cfg.node_id(); + let id1 = pm1.cfg.node_id(); + let id2 = pm2.cfg.node_id(); + + tracing::info!(target:"test", "connect nodes in a line"); + pm0.connect_to(&pm1.peer_info()).await; + pm1.connect_to(&pm2.peer_info()).await; + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[ + (id1.clone(), vec![id1.clone()]), + (id2.clone(), vec![id1.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id1} routing table"); + pm1.wait_for_routing_table(&[ + (id0.clone(), vec![id0.clone()]), + (id2.clone(), vec![id2.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id2} routing table"); + pm2.wait_for_routing_table(&[ + (id0.clone(), vec![id1.clone()]), + (id1.clone(), vec![id1.clone()]), + ]) + .await; + + // capture event streams before pinging + let mut pm0_ev = pm0.events.from_now(); + let mut pm2_ev = pm2.events.from_now(); + + tracing::info!(target:"test", "send ping from {id0} to {id2}"); + pm0.send_ping(0, id2.clone()).await; + + tracing::info!(target:"test", "await ping at {id2}"); + wait_for_ping(&mut pm2_ev, Ping { nonce: 0, source: id0.clone() }).await; + + tracing::info!(target:"test", "await pong at {id0}"); + wait_for_pong(&mut pm0_ev, Pong { nonce: 0, source: id2.clone() }).await; +} + +// test that ping over an indirect connection with ttl=1 is dropped +#[tokio::test] +async fn test_drop_after_ttl() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + tracing::info!(target:"test", "start three nodes"); + let mut cfg0 = chain.make_config(rng); + cfg0.routed_message_ttl = 1; + let pm0 = start_pm(clock.clock(), TestDB::new(), cfg0, chain.clone()).await; + let pm1 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm2 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + + let id0 = pm0.cfg.node_id(); + let id1 = pm1.cfg.node_id(); + let id2 = pm2.cfg.node_id(); + + tracing::info!(target:"test", "connect nodes in a line"); + pm0.connect_to(&pm1.peer_info()).await; + pm1.connect_to(&pm2.peer_info()).await; + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[ + (id1.clone(), vec![id1.clone()]), + (id2.clone(), vec![id1.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id1} routing table"); + pm1.wait_for_routing_table(&[ + (id0.clone(), vec![id0.clone()]), + (id2.clone(), vec![id2.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id2} routing table"); + pm2.wait_for_routing_table(&[ + (id0.clone(), vec![id1.clone()]), + (id1.clone(), vec![id1.clone()]), + ]) + .await; + + // capture event stream before pinging + let mut pm1_ev = pm1.events.from_now(); + + tracing::info!(target:"test", "send ping from {id0} to {id2}"); + pm0.send_ping(0, id2.clone()).await; + + tracing::info!(target:"test", "await message dropped at {id1}"); + wait_for_message_dropped(&mut pm1_ev).await; +} + +// test dropping behavior for duplicate messages +#[tokio::test] +async fn test_dropping_duplicate_messages() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + tracing::info!(target:"test", "start three nodes"); + let pm0 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm1 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm2 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + + let id0 = pm0.cfg.node_id(); + let id1 = pm1.cfg.node_id(); + let id2 = pm2.cfg.node_id(); + + tracing::info!(target:"test", "connect nodes in a line"); + pm0.connect_to(&pm1.peer_info()).await; + pm1.connect_to(&pm2.peer_info()).await; + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[ + (id1.clone(), vec![id1.clone()]), + (id2.clone(), vec![id1.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id1} routing table"); + pm1.wait_for_routing_table(&[ + (id0.clone(), vec![id0.clone()]), + (id2.clone(), vec![id2.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id2} routing table"); + pm2.wait_for_routing_table(&[ + (id0.clone(), vec![id1.clone()]), + (id1.clone(), vec![id1.clone()]), + ]) + .await; + + // capture event streams before pinging + let mut pm0_ev = pm0.events.from_now(); + let mut pm1_ev = pm1.events.from_now(); + let mut pm2_ev = pm2.events.from_now(); + + // Send two identical messages. One will be dropped, because the delay between them was less than 50ms. + tracing::info!(target:"test", "send ping from {id0} to {id2}"); + pm0.send_ping(0, id2.clone()).await; + tracing::info!(target:"test", "await ping at {id2}"); + wait_for_ping(&mut pm2_ev, Ping { nonce: 0, source: id0.clone() }).await; + tracing::info!(target:"test", "await pong at {id0}"); + wait_for_pong(&mut pm0_ev, Pong { nonce: 0, source: id2.clone() }).await; + + tracing::info!(target:"test", "send ping from {id0} to {id2}"); + pm0.send_ping(0, id2.clone()).await; + tracing::info!(target:"test", "await message dropped at {id1}"); + wait_for_message_dropped(&mut pm1_ev).await; + + // Send two identical messages but with 300ms delay so they don't get dropped. + tracing::info!(target:"test", "send ping from {id0} to {id2}"); + pm0.send_ping(1, id2.clone()).await; + tracing::info!(target:"test", "await ping at {id2}"); + wait_for_ping(&mut pm2_ev, Ping { nonce: 1, source: id0.clone() }).await; + tracing::info!(target:"test", "await pong at {id0}"); + wait_for_pong(&mut pm0_ev, Pong { nonce: 1, source: id2.clone() }).await; + + clock.advance(time::Duration::milliseconds(300)); + + tracing::info!(target:"test", "send ping from {id0} to {id2}"); + pm0.send_ping(1, id2.clone()).await; + tracing::info!(target:"test", "await ping at {id2}"); + wait_for_ping(&mut pm2_ev, Ping { nonce: 1, source: id0.clone() }).await; + tracing::info!(target:"test", "await pong at {id0}"); + wait_for_pong(&mut pm0_ev, Pong { nonce: 1, source: id2.clone() }).await; +} + // test that TTL is handled property. #[tokio::test] async fn ttl() { diff --git a/integration-tests/src/tests/network/mod.rs b/integration-tests/src/tests/network/mod.rs index d5fc999f415..0738982137d 100644 --- a/integration-tests/src/tests/network/mod.rs +++ b/integration-tests/src/tests/network/mod.rs @@ -1,7 +1,6 @@ mod ban_peers; mod churn_attack; mod full_network; -mod multiset; mod peer_handshake; mod routing; mod runner; diff --git a/integration-tests/src/tests/network/multiset.rs b/integration-tests/src/tests/network/multiset.rs deleted file mode 100644 index 6bf37885343..00000000000 --- a/integration-tests/src/tests/network/multiset.rs +++ /dev/null @@ -1,36 +0,0 @@ -use smart_default::SmartDefault; -/// Generic MultiSet implementation based on a HashMap. -use std::collections::HashMap; -use std::hash::Hash; - -#[derive(Debug, SmartDefault, PartialEq, Eq)] -pub struct MultiSet(HashMap); - -impl MultiSet { - pub fn new() -> Self { - Self(HashMap::new()) - } - - pub fn insert(&mut self, val: T) { - *self.0.entry(val).or_default() += 1; - } - - pub fn is_subset(&self, other: &Self) -> bool { - for (k, v) in &self.0 { - if other.0.get(&k).unwrap_or(&0) < &v { - return false; - } - } - true - } -} - -impl FromIterator for MultiSet { - fn from_iter>(iter: I) -> Self { - let mut s = Self::new(); - for v in iter { - s.insert(v); - } - s - } -} diff --git a/integration-tests/src/tests/network/routing.rs b/integration-tests/src/tests/network/routing.rs index 95341731c4b..5fe590adc1a 100644 --- a/integration-tests/src/tests/network/routing.rs +++ b/integration-tests/src/tests/network/routing.rs @@ -23,88 +23,6 @@ fn account_propagation() -> anyhow::Result<()> { start_test(runner) } -#[test] -fn ping_simple() -> anyhow::Result<()> { - let mut runner = Runner::new(2, 2); - - runner.push(Action::AddEdge { from: 0, to: 1, force: true }); - runner.push(Action::CheckRoutingTable(0, vec![(1, vec![1])])); - runner.push(Action::PingTo { source: 0, nonce: 0, target: 1 }); - runner.push(Action::CheckPingPong(1, vec![Ping { nonce: 0, source: 0 }], vec![])); - runner.push(Action::CheckPingPong(0, vec![], vec![Pong { nonce: 0, source: 1 }])); - - start_test(runner) -} - -#[test] -/// Crate 3 nodes connected in a line and try to use Ping. -fn ping_jump() -> anyhow::Result<()> { - let mut runner = Runner::new(3, 2); - - // Add edges - runner.push(Action::AddEdge { from: 0, to: 1, force: true }); - runner.push(Action::AddEdge { from: 1, to: 2, force: true }); - // Check routing tables and wait for `PeerManager` to update it's routing table - runner.push(Action::CheckRoutingTable(0, vec![(1, vec![1]), (2, vec![1])])); - runner.push(Action::CheckRoutingTable(1, vec![(0, vec![0]), (2, vec![2])])); - runner.push(Action::CheckRoutingTable(2, vec![(0, vec![1]), (1, vec![1])])); - - // Try Pinging from node 0 to 2 - runner.push(Action::PingTo { source: 0, nonce: 0, target: 2 }); - // Check whenever Node 2 got message from 0 - runner.push(Action::CheckPingPong(2, vec![Ping { nonce: 0, source: 0 }], vec![])); - // Check whenever Node 0 got reply from 2. - runner.push(Action::CheckPingPong(0, vec![], vec![Pong { nonce: 0, source: 2 }])); - - start_test(runner) -} - -/// Test routed messages are not dropped if have enough TTL. -/// Spawn three nodes and connect them in a line: -/// -/// 0 ---- 1 ---- 2 -/// -/// Set routed message ttl to 2, so routed message can't pass through more than 2 edges. -/// Send Ping from 0 to 2. It should arrive since there are only 2 edges from 0 to 2. -/// Check Ping arrive at node 2 and later Pong arrive at node 0. -#[test] -fn test_dont_drop_after_ttl() -> anyhow::Result<()> { - let mut runner = Runner::new(3, 1).routed_message_ttl(2); - - runner.push(Action::AddEdge { from: 0, to: 1, force: true }); - runner.push(Action::AddEdge { from: 1, to: 2, force: true }); - runner.push(Action::CheckRoutingTable(0, vec![(1, vec![1]), (2, vec![1])])); - runner.push(Action::CheckRoutingTable(1, vec![(0, vec![0]), (2, vec![2])])); - runner.push(Action::PingTo { source: 0, nonce: 0, target: 2 }); - runner.push(Action::CheckPingPong(2, vec![Ping { nonce: 0, source: 0 }], vec![])); - runner.push(Action::CheckPingPong(0, vec![], vec![Pong { nonce: 0, source: 2 }])); - - start_test(runner) -} - -/// Test routed messages are dropped if don't have enough TTL. -/// Spawn three nodes and connect them in a line: -/// -/// 0 ---- 1 ---- 2 -/// -/// Set routed message ttl to 1, so routed message can't pass through more than 1 edges. -/// Send Ping from 0 to 2. It should not arrive since there are 2 edges from 0 to 2. -/// Check none of Ping and Pong arrived. -#[test] -fn test_drop_after_ttl() -> anyhow::Result<()> { - let mut runner = Runner::new(3, 1).routed_message_ttl(1); - - runner.push(Action::AddEdge { from: 0, to: 1, force: true }); - runner.push(Action::AddEdge { from: 1, to: 2, force: true }); - runner.push(Action::CheckRoutingTable(0, vec![(1, vec![1]), (2, vec![1])])); - runner.push(Action::PingTo { source: 0, nonce: 0, target: 2 }); - runner.push(Action::Wait(time::Duration::milliseconds(100))); - runner.push(Action::CheckPingPong(2, vec![], vec![])); - runner.push(Action::CheckPingPong(0, vec![], vec![])); - - start_test(runner) -} - #[test] fn blacklist_01() -> anyhow::Result<()> { let mut runner = Runner::new(2, 2).add_to_blacklist(0, Some(1)).use_boot_nodes(vec![0]); @@ -201,49 +119,3 @@ fn archival_node() -> anyhow::Result<()> { start_test(runner) } - -/// Spawn 3 nodes, add edges to form 0---1---2 line. Then send 2 pings from 0 to 2, one should be dropped. -#[test] -fn test_dropping_routing_messages() -> anyhow::Result<()> { - let mut runner = Runner::new(3, 3).max_num_peers(2).ideal_connections(2, 2).enable_outbound(); - - runner.push(Action::SetOptions { target: 0, max_num_peers: Some(1) }); - runner.push(Action::SetOptions { target: 1, max_num_peers: Some(2) }); - runner.push(Action::SetOptions { target: 2, max_num_peers: Some(1) }); - runner.push(Action::AddEdge { from: 0, to: 1, force: true }); - runner.push(Action::AddEdge { from: 1, to: 2, force: true }); - runner.push(Action::CheckRoutingTable(0, vec![(1, vec![1]), (2, vec![1])])); - runner.push(Action::CheckRoutingTable(1, vec![(0, vec![0]), (2, vec![2])])); - runner.push(Action::CheckRoutingTable(2, vec![(0, vec![1]), (1, vec![1])])); - - // Send two identical messages. One will be dropped, because the delay between them was less than 100ms. - runner.push(Action::PingTo { source: 0, nonce: 0, target: 2 }); - runner.push(Action::PingTo { source: 0, nonce: 0, target: 2 }); - runner.push(Action::CheckPingPong(2, vec![Ping { nonce: 0, source: 0 }], vec![])); - runner.push(Action::CheckPingPong(0, vec![], vec![Pong { nonce: 0, source: 2 }])); - - // Send two identical messages but in 300ms interval so they don't get dropped. - runner.push(Action::PingTo { source: 0, nonce: 1, target: 2 }); - runner.push(Action::Wait(time::Duration::milliseconds(300))); - runner.push(Action::PingTo { source: 0, nonce: 1, target: 2 }); - runner.push(Action::CheckPingPong( - 2, - vec![ - Ping { nonce: 0, source: 0 }, - Ping { nonce: 1, source: 0 }, - Ping { nonce: 1, source: 0 }, - ], - vec![], - )); - runner.push(Action::CheckPingPong( - 0, - vec![], - vec![ - Pong { nonce: 0, source: 2 }, - Pong { nonce: 1, source: 2 }, - Pong { nonce: 1, source: 2 }, - ], - )); - - start_test(runner) -} diff --git a/integration-tests/src/tests/network/runner.rs b/integration-tests/src/tests/network/runner.rs index be88980022a..0fbf13ab8f6 100644 --- a/integration-tests/src/tests/network/runner.rs +++ b/integration-tests/src/tests/network/runner.rs @@ -1,4 +1,3 @@ -use crate::tests::network::multiset::MultiSet; use actix::{Actor, Addr}; use anyhow::{anyhow, bail, Context}; use near_chain::test_utils::{KeyValueRuntime, ValidatorSchedule}; @@ -8,7 +7,6 @@ use near_client::{start_client, start_view_client}; use near_crypto::KeyType; use near_network::actix::ActixSystem; use near_network::blacklist; -use near_network::broadcast; use near_network::config; use near_network::tcp; use near_network::test_utils::{ @@ -17,10 +15,9 @@ use near_network::test_utils::{ use near_network::time; use near_network::types::NetworkRecipient; use near_network::types::{ - PeerInfo, PeerManagerMessageRequest, PeerManagerMessageResponse, Ping as NetPing, - Pong as NetPong, ROUTED_MESSAGE_TTL, + PeerInfo, PeerManagerMessageRequest, PeerManagerMessageResponse, ROUTED_MESSAGE_TTL, }; -use near_network::{Event, PeerManagerActor}; +use near_network::PeerManagerActor; use near_o11y::testonly::init_test_logger; use near_o11y::WithSpanContextExt; use near_primitives::block::GenesisId; @@ -130,14 +127,6 @@ pub enum Action { }, CheckRoutingTable(usize, Vec<(usize, Vec)>), CheckAccountId(usize, Vec), - // Send ping from `source` with `nonce` to `target` - PingTo { - source: usize, - target: usize, - nonce: u64, - }, - // Check for `source` received pings and pongs. - CheckPingPong(usize, Vec, Vec), // Send stop signal to some node. Stop(usize), // Wait time in milliseconds @@ -206,36 +195,6 @@ async fn check_account_id( Ok(ControlFlow::Break(())) } -async fn check_ping_pong( - info: &mut RunningInfo, - source: usize, - want_pings: Vec, - want_pongs: Vec, -) -> anyhow::Result { - let want_pings: MultiSet = want_pings - .iter() - .map(|p| NetPing { nonce: p.nonce, source: info.runner.test_config[p.source].peer_id() }) - .collect(); - let want_pongs: MultiSet = want_pongs - .iter() - .map(|p| NetPong { nonce: p.nonce, source: info.runner.test_config[p.source].peer_id() }) - .collect(); - let node: &mut NodeHandle = info.nodes[source].as_mut().unwrap(); - - loop { - if !node.pings.is_subset(&want_pings) { - bail!("got_pings = {:?}, want_pings = {want_pings:?}", node.pings); - } - if !node.pongs.is_subset(&want_pongs) { - bail!("got_pongs = {:?}, want_pongs = {want_pongs:?}", node.pongs); - } - if node.pings == want_pings && node.pongs == want_pongs { - return Ok(ControlFlow::Break(())); - } - node.consume_event().await; - } -} - impl StateMachine { fn new() -> Self { Self { actions: vec![] } @@ -291,16 +250,6 @@ impl StateMachine { Box::pin(check_account_id(info, source, known_validators.clone())) })); } - Action::PingTo { source, nonce, target } => { - self.actions.push(Box::new(move |info: &mut RunningInfo| Box::pin(async move { - debug!(target: "network", num_prev_actions, action = ?action_clone, "runner.rs: Action"); - let target = info.runner.test_config[target].peer_id(); - info.get_node(source)?.actix.addr.send(PeerManagerMessageRequest::PingTo{ - nonce, target, - }.with_span_context()).await?; - Ok(ControlFlow::Break(())) - }))); - } Action::Stop(source) => { self.actions.push(Box::new(move |info: &mut RunningInfo| Box::pin(async move { debug!(target: "network", num_prev_actions, action = ?action_clone, "runner.rs: Action"); @@ -315,11 +264,6 @@ impl StateMachine { Ok(ControlFlow::Break(())) }))); } - Action::CheckPingPong(source, pings, pongs) => { - self.actions.push(Box::new(move |info| { - Box::pin(check_ping_pong(info, source, pings.clone(), pongs.clone())) - })); - } } } } @@ -384,19 +328,6 @@ pub struct Runner { struct NodeHandle { actix: ActixSystem, - events: broadcast::Receiver, - pings: MultiSet, - pongs: MultiSet, -} - -impl NodeHandle { - async fn consume_event(&mut self) { - match self.events.recv().await { - Event::Ping(ping) => self.pings.insert(ping), - Event::Pong(pong) => self.pongs.insert(pong), - _ => {} - } - } } impl Runner { @@ -487,14 +418,6 @@ impl Runner { self } - /// Set routed message ttl. - pub fn routed_message_ttl(mut self, routed_message_ttl: u8) -> Self { - self.apply_all(move |test_config| { - test_config.routed_message_ttl = routed_message_ttl; - }); - self - } - /// Allow message to connect among themselves without triggering new connections. pub fn enable_outbound(mut self) -> Self { self.apply_all(|test_config| { @@ -551,8 +474,6 @@ impl Runner { network_config.outbound_disabled = config.outbound_disabled; network_config.peer_store.boot_nodes = boot_nodes; network_config.archive = config.archive; - let (send_events, recv_events) = broadcast::unbounded_channel(); - network_config.event_sink = send_events.sink(); config.ideal_connections.map(|(lo, hi)| { network_config.ideal_connections_lo = lo; @@ -574,9 +495,6 @@ impl Runner { setup_network_node(account_id, validators, chain_genesis, network_config) }) .await, - events: recv_events, - pings: MultiSet::default(), - pongs: MultiSet::default(), }) } From cd7150ecf3a4ea368b55c80a60044684a13dc327 Mon Sep 17 00:00:00 2001 From: mm-near <91919554+mm-near@users.noreply.github.com> Date: Fri, 25 Nov 2022 18:18:30 +0100 Subject: [PATCH 038/188] remove unused spawn_with_nonce (#8120) Co-authored-by: near-bulldozer[bot] <73298989+near-bulldozer[bot]@users.noreply.github.com> --- chain/network/src/peer/peer_actor.rs | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index fbf9ac43d83..14b641d3b78 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -156,21 +156,9 @@ impl PeerActor { stream: tcp::Stream, force_encoding: Option, network_state: Arc, - ) -> anyhow::Result> { - Self::spawn_with_nonce(clock, stream, force_encoding, network_state, None) - } - - /// Span peer actor, and make it establish a connection with a given nonce. - /// Used mostly for tests. - pub(crate) fn spawn_with_nonce( - clock: time::Clock, - stream: tcp::Stream, - force_encoding: Option, - network_state: Arc, - nonce: Option, ) -> anyhow::Result> { let stream_id = stream.id(); - match Self::spawn_inner(clock, stream, force_encoding, network_state.clone(), nonce) { + match Self::spawn_inner(clock, stream, force_encoding, network_state.clone()) { Ok(it) => Ok(it), Err(reason) => { network_state.config.event_sink.push(Event::ConnectionClosed( @@ -186,7 +174,6 @@ impl PeerActor { stream: tcp::Stream, force_encoding: Option, network_state: Arc, - nonce: Option, ) -> Result, ClosingReason> { let connecting_status = match &stream.type_ { tcp::StreamType::Inbound => ConnectingStatus::Inbound( @@ -202,7 +189,7 @@ impl PeerActor { .start_outbound(peer_id.clone()) .map_err(ClosingReason::OutboundNotAllowed)?, handshake_spec: HandshakeSpec { - partial_edge_info: network_state.propose_edge(&clock, peer_id, nonce), + partial_edge_info: network_state.propose_edge(&clock, peer_id, None), protocol_version: PROTOCOL_VERSION, peer_id: peer_id.clone(), }, From bc58602ce22e5e53a817728826140cd1cff9e4ac Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Sat, 26 Nov 2022 00:04:18 +0000 Subject: [PATCH 039/188] fix: revert accidental protocol change in #8044 (#8116) We have to charge the gas in one block to preserve the old behavior in case of an "out of gas" failure in between. I will maybe submit the change that makes it "right" with a protocol feature flag, for now just revert it. --- Cargo.lock | 1 + runtime/near-vm-logic/Cargo.toml | 1 + runtime/near-vm-logic/src/logic.rs | 20 ++- .../near-vm-logic/src/tests/gas_counter.rs | 136 ++++++++++++++++++ 4 files changed, 147 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce4a4acdb61..7e870de2cdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3610,6 +3610,7 @@ dependencies = [ "borsh", "byteorder", "ed25519-dalek", + "expect-test", "hex", "near-account-id", "near-crypto", diff --git a/runtime/near-vm-logic/Cargo.toml b/runtime/near-vm-logic/Cargo.toml index d64c1d4306c..eace29d00d0 100644 --- a/runtime/near-vm-logic/Cargo.toml +++ b/runtime/near-vm-logic/Cargo.toml @@ -36,6 +36,7 @@ near-vm-errors = { path = "../near-vm-errors" } [dev-dependencies] hex.workspace = true serde_json = { workspace = true, features = ["preserve_order"] } +expect-test.workspace = true [features] default = [] diff --git a/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs index f545ebc7015..77032f6045e 100644 --- a/runtime/near-vm-logic/src/logic.rs +++ b/runtime/near-vm-logic/src/logic.rs @@ -1223,23 +1223,21 @@ impl<'a> VMLogic<'a> { /// pay for the content transmitted through the dependency upon the actual creation of the /// DataReceipt. fn pay_gas_for_new_receipt(&mut self, sir: bool, data_dependencies: &[bool]) -> Result<()> { - self.pay_action_base(ActionCosts::new_action_receipt, sir)?; - let mut burn_gas = 0u64; + let fees_config_cfg = &self.fees_config; + let mut burn_gas = fees_config_cfg.fee(ActionCosts::new_action_receipt).send_fee(sir); + let mut use_gas = fees_config_cfg.fee(ActionCosts::new_action_receipt).exec_fee(); for dep in data_dependencies { // Both creation and execution for data receipts are considered burnt gas. burn_gas = burn_gas - .checked_add( - self.fees_config.fee(ActionCosts::new_data_receipt_base).send_fee(*dep), - ) + .checked_add(fees_config_cfg.fee(ActionCosts::new_data_receipt_base).send_fee(*dep)) .ok_or(HostError::IntegerOverflow)? - .checked_add(self.fees_config.fee(ActionCosts::new_data_receipt_base).exec_fee()) + .checked_add(fees_config_cfg.fee(ActionCosts::new_data_receipt_base).exec_fee()) .ok_or(HostError::IntegerOverflow)?; } - self.gas_counter.pay_action_accumulated( - burn_gas, - burn_gas, - ActionCosts::new_data_receipt_base, - ) + use_gas = use_gas.checked_add(burn_gas).ok_or(HostError::IntegerOverflow)?; + // This should go to `new_data_receipt_base` and `new_action_receipt` in parts. + // But we have to keep charing these two together unless we make a protocol change. + self.gas_counter.pay_action_accumulated(burn_gas, use_gas, ActionCosts::new_action_receipt) } /// A helper function to subtract balance on transfer or attached deposit for promises. diff --git a/runtime/near-vm-logic/src/tests/gas_counter.rs b/runtime/near-vm-logic/src/tests/gas_counter.rs index e8f4a4606b2..cbcdcb2de16 100644 --- a/runtime/near-vm-logic/src/tests/gas_counter.rs +++ b/runtime/near-vm-logic/src/tests/gas_counter.rs @@ -4,7 +4,11 @@ use crate::tests::helpers::*; use crate::tests::vm_logic_builder::VMLogicBuilder; use crate::types::Gas; use crate::{VMConfig, VMLogic}; +use expect_test::expect; +use near_primitives::config::ActionCosts; +use near_primitives::runtime::fees::Fee; use near_primitives::transaction::{Action, FunctionCallAction}; +use near_vm_errors::{HostError, VMLogicError}; #[test] fn test_dont_burn_gas_when_exceeding_attached_gas_limit() { @@ -219,12 +223,144 @@ fn function_call_no_weight_refund() { assert!(outcome.used_gas < gas_limit); } +/// Check consistent result when exceeding gas limit on a specific action gas parameter. +/// +/// Increases an action cost to a high value and then watch an execution run out +/// of gas. Then make sure the exact result is still the same. This prevents +/// accidental protocol changes where gas is deducted in different order. +#[track_caller] +fn check_action_gas_exceeds_limit( + cost: ActionCosts, + num_action_paid: u64, + exercise_action: impl FnOnce(&mut VMLogic) -> Result<(), VMLogicError>, +) { + // Create a logic parametrized such that it will fail with out-of-gas when specified action is deducted. + let gas_limit = 10u64.pow(13); + let gas_attached = gas_limit; + let fee = Fee { + send_sir: gas_limit / num_action_paid + 1, + send_not_sir: gas_limit / num_action_paid + 10, + execution: 1, // exec part is `used`, make it small + }; + let mut logic_builder = VMLogicBuilder::default().max_gas_burnt(gas_limit).gas_fee(cost, fee); + let mut logic = logic_builder.build_with_prepaid_gas(gas_attached); + + let result = exercise_action(&mut logic); + assert!(result.is_err(), "expected out-of-gas error for {cost:?} but was ok"); + assert_eq!(result.unwrap_err(), VMLogicError::HostError(HostError::GasLimitExceeded)); + + // When gas limit is exceeded, we always set burnt_gas := prepaid and then promise_gas := 0. + assert_eq!( + gas_attached, + logic.gas_counter().burnt_gas(), + "burnt gas should be all attached gas", + ); + assert_eq!( + gas_attached, + logic.gas_counter().used_gas(), + "used gas should be no more than burnt gas", + ); +} + +/// Check consistent result when exceeding attached gas on a specific action gas parameter. +/// +/// Very similar to `check_action_gas_exceeds_limit` but we hit a different +/// limit and return a different error. +/// +/// This case is more interesting because the burnt gas can be below used gas, +/// when the prepaid gas was exceeded by burnt burnt + promised gas but not by +/// burnt gas alone. +#[track_caller] +fn check_action_gas_exceeds_attached( + cost: ActionCosts, + num_action_paid: u64, + expected: expect_test::Expect, + exercise_action: impl FnOnce(&mut VMLogic) -> Result<(), VMLogicError>, +) { + // Create a logic parametrized such that it will fail with out-of-gas when specified action is deducted. + let gas_limit = 10u64.pow(14); + let gas_attached = 10u64.pow(13); + let fee = Fee { + send_sir: 1, // make burnt gas small + send_not_sir: 10, // make it easy to distinguish `sir` / `not_sir` + execution: gas_attached / num_action_paid + 1, + }; + let mut logic_builder = VMLogicBuilder::default().max_gas_burnt(gas_limit).gas_fee(cost, fee); + let mut logic = logic_builder.build_with_prepaid_gas(gas_attached); + + let result = exercise_action(&mut logic); + assert!(result.is_err(), "expected out-of-gas error for {cost:?} but was ok"); + assert_eq!(result.unwrap_err(), VMLogicError::HostError(HostError::GasExceeded)); + + let actual = format!( + "{} burnt {} used", + logic.gas_counter().burnt_gas(), + logic.gas_counter().used_gas() + ); + expected.assert_eq(&actual); +} + +#[test] +fn out_of_gas_new_action_receipt() { + // two different ways to create an action receipts, first check exceeding the burnt limit + check_action_gas_exceeds_limit(ActionCosts::new_action_receipt, 1, create_action_receipt); + check_action_gas_exceeds_limit(ActionCosts::new_action_receipt, 2, create_promise_dependency); + + // the same again, but for prepaid gas + check_action_gas_exceeds_attached( + ActionCosts::new_action_receipt, + 1, + expect!["8644846690 burnt 10000000000000 used"], + create_action_receipt, + ); + + check_action_gas_exceeds_attached( + ActionCosts::new_action_receipt, + 2, + expect!["9411968532130 burnt 10000000000000 used"], + create_promise_dependency, + ); +} + +#[test] +fn out_of_gas_new_data_receipt() { + check_action_gas_exceeds_limit( + ActionCosts::new_data_receipt_base, + 1, + create_promise_dependency, + ); + + check_action_gas_exceeds_attached( + ActionCosts::new_data_receipt_base, + 1, + expect!["10000000000000 burnt 10000000000000 used"], + create_promise_dependency, + ); +} + +fn create_action_receipt(logic: &mut VMLogic) -> Result<(), VMLogicError> { + promise_batch_create(logic, "rick.test")?; + Ok(()) +} + +fn create_promise_dependency(logic: &mut VMLogic) -> Result<(), VMLogicError> { + let account_id = "rick.test"; + let idx = promise_batch_create(logic, account_id)?; + logic.promise_batch_then(idx, account_id.len() as _, account_id.as_ptr() as _)?; + Ok(()) +} + impl VMLogicBuilder { fn max_gas_burnt(mut self, max_gas_burnt: Gas) -> Self { self.config.limit_config.max_gas_burnt = max_gas_burnt; self } + fn gas_fee(mut self, cost: ActionCosts, fee: Fee) -> Self { + self.fees_config.action_fees[cost] = fee; + self + } + fn build_with_prepaid_gas(&mut self, prepaid_gas: Gas) -> VMLogic<'_> { let mut context = get_context(vec![], false); context.prepaid_gas = prepaid_gas; From fe6c4b9c395fed6b39a25081fb1133dcf453ad29 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Mon, 28 Nov 2022 12:17:12 +0100 Subject: [PATCH 040/188] fixed edge updates (#8121) * removed unnecessary contention on connection::Pool * added a proper lock on the edge update * removed unnecessary broadcast of the new edge: add_edges already does the broadcast anyway. --- chain/network/src/peer/peer_actor.rs | 9 +++------ .../src/peer_manager/connection/mod.rs | 19 ++++++++----------- .../src/peer_manager/network_state/mod.rs | 2 +- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index 14b641d3b78..4ecfcf301c0 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -1,4 +1,5 @@ use crate::accounts_data; +use crate::concurrency::arc_mutex::ArcMutex; use crate::concurrency::atomic_cell::AtomicCell; use crate::concurrency::demux; use crate::network_protocol::{ @@ -500,7 +501,7 @@ impl PeerActor { let conn = Arc::new(connection::Connection { addr: ctx.address(), peer_info: peer_info.clone(), - edge: AtomicCell::new(edge), + edge: ArcMutex::new(edge), genesis_id: handshake.sender_chain_info.genesis_id.clone(), tracked_shards: handshake.sender_chain_info.tracked_shards.clone(), archival: handshake.sender_chain_info.archival, @@ -592,10 +593,6 @@ impl PeerActor { incremental: false, requesting_full_sync: true, })); - // Only broadcast the new edge from the outbound endpoint. - act.network_state.tier2.broadcast_message(Arc::new(PeerMessage::SyncRoutingTable( - RoutingTableUpdate::from_edges(vec![conn.edge.load()]), - ))); } // Exchange peers periodically. @@ -660,7 +657,7 @@ impl PeerActor { act.sync_routing_table(); act.network_state.config.event_sink.push(Event::HandshakeCompleted(HandshakeCompletedEvent{ stream_id: act.stream_id, - edge: conn.edge.load(), + edge: conn.edge.load().as_ref().clone(), })); }, Err(err) => { diff --git a/chain/network/src/peer_manager/connection/mod.rs b/chain/network/src/peer_manager/connection/mod.rs index d2ac6123412..00f068ac157 100644 --- a/chain/network/src/peer_manager/connection/mod.rs +++ b/chain/network/src/peer_manager/connection/mod.rs @@ -46,7 +46,7 @@ pub(crate) struct Connection { pub addr: actix::Addr, pub peer_info: PeerInfo, - pub edge: AtomicCell, + pub edge: ArcMutex, /// Chain Id and hash of genesis block. pub genesis_id: GenesisId, /// Shards that the peer is tracking. @@ -297,17 +297,14 @@ impl Pool { } /// Update the edge in the pool (if it is newer). pub fn update_edge(&self, new_edge: &Edge) { - self.0.update(|pool| { - let other = new_edge.other(&pool.me); - if let Some(other) = other { - if let Some(connection) = pool.ready.get_mut(other) { - let edge = connection.edge.load(); - if edge.nonce() < new_edge.nonce() { - connection.edge.store(new_edge.clone()); - } - } + let pool = self.load(); + let Some(other) = new_edge.other(&pool.me) else { return }; + let Some(conn) = pool.ready.get(other) else { return }; + conn.edge.update(|e| { + if e.nonce() < new_edge.nonce() { + *e = new_edge.clone(); } - }) + }); } /// Send message to peer that belongs to our active set diff --git a/chain/network/src/peer_manager/network_state/mod.rs b/chain/network/src/peer_manager/network_state/mod.rs index 78295454d99..d68fb821ab8 100644 --- a/chain/network/src/peer_manager/network_state/mod.rs +++ b/chain/network/src/peer_manager/network_state/mod.rs @@ -258,7 +258,7 @@ impl NetworkState { // Verify and broadcast the edge of the connection. Only then insert the new // connection to TIER2 pool, so that nothing is broadcasted to conn. // TODO(gprusak): consider actually banning the peer for consistency. - this.add_edges(&clock, vec![conn.edge.load()]) + this.add_edges(&clock, vec![conn.edge.load().as_ref().clone()]) .await .map_err(|_: ReasonForBan| RegisterPeerError::InvalidEdge)?; this.tier2.insert_ready(conn.clone()).map_err(RegisterPeerError::PoolError)?; From 4ce49adc1c7af1c0f28ff13191596a4635a0793f Mon Sep 17 00:00:00 2001 From: mm-near <91919554+mm-near@users.noreply.github.com> Date: Mon, 28 Nov 2022 15:08:47 +0100 Subject: [PATCH 041/188] [Refactor] Split sync.rs into separate files (#8122) Moving different sync parts (HeaderSync, StateSync, BlockSync and EpochSync) into separate files. This is only a refactoring - no behaviour changes. --- chain/client/src/client.rs | 5 +- chain/client/src/client_actor.rs | 2 +- chain/client/src/sync.rs | 1781 ------------------------------ chain/client/src/sync/block.rs | 408 +++++++ chain/client/src/sync/epoch.rs | 87 ++ chain/client/src/sync/header.rs | 591 ++++++++++ chain/client/src/sync/mod.rs | 4 + chain/client/src/sync/state.rs | 759 +++++++++++++ chain/client/src/view_client.rs | 2 +- tools/mock-node/src/lib.rs | 4 +- 10 files changed, 1857 insertions(+), 1786 deletions(-) delete mode 100644 chain/client/src/sync.rs create mode 100644 chain/client/src/sync/block.rs create mode 100644 chain/client/src/sync/epoch.rs create mode 100644 chain/client/src/sync/header.rs create mode 100644 chain/client/src/sync/mod.rs create mode 100644 chain/client/src/sync/state.rs diff --git a/chain/client/src/client.rs b/chain/client/src/client.rs index 8a532724fe5..ed0005a0fb1 100644 --- a/chain/client/src/client.rs +++ b/chain/client/src/client.rs @@ -49,7 +49,10 @@ use near_primitives::validator_signer::ValidatorSigner; use crate::adapter::ProcessTxResponse; use crate::debug::BlockProductionTracker; use crate::debug::PRODUCTION_TIMES_CACHE_SIZE; -use crate::sync::{BlockSync, EpochSync, HeaderSync, StateSync, StateSyncResult}; +use crate::sync::block::BlockSync; +use crate::sync::epoch::EpochSync; +use crate::sync::header::HeaderSync; +use crate::sync::state::{StateSync, StateSyncResult}; use crate::{metrics, SyncStatus}; use near_client_primitives::types::{Error, ShardSyncDownload, ShardSyncStatus}; use near_network::types::{AccountKeys, ChainInfo, PeerManagerMessageRequest, SetChainInfo}; diff --git a/chain/client/src/client_actor.rs b/chain/client/src/client_actor.rs index 2f0d14239f3..fbbda234a1a 100644 --- a/chain/client/src/client_actor.rs +++ b/chain/client/src/client_actor.rs @@ -16,7 +16,7 @@ use crate::info::{ display_sync_status, get_validator_epoch_stats, InfoHelper, ValidatorInfoHelper, }; use crate::metrics::PARTIAL_ENCODED_CHUNK_RESPONSE_DELAY; -use crate::sync::{StateSync, StateSyncResult}; +use crate::sync::state::{StateSync, StateSyncResult}; use crate::{metrics, StatusResponse}; use actix::dev::SendError; use actix::{Actor, Addr, Arbiter, AsyncContext, Context, Handler, Message}; diff --git a/chain/client/src/sync.rs b/chain/client/src/sync.rs deleted file mode 100644 index 94504574999..00000000000 --- a/chain/client/src/sync.rs +++ /dev/null @@ -1,1781 +0,0 @@ -use near_chain::{check_known, near_chain_primitives, ChainStoreAccess, Error}; -use std::cmp::min; -use std::collections::{HashMap, HashSet}; -use std::ops::Add; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; -use std::time::Duration as TimeDuration; - -use ansi_term::Color::{Purple, Yellow}; -use chrono::{DateTime, Duration}; -use futures::{future, FutureExt}; -use rand::seq::{IteratorRandom, SliceRandom}; -use rand::{thread_rng, Rng}; -use tracing::{debug, error, info, warn}; - -use near_chain::{Chain, RuntimeAdapter}; -use near_network::types::{ - HighestHeightPeerInfo, NetworkRequests, NetworkResponses, PeerManagerAdapter, -}; -use near_primitives::block::Tip; -use near_primitives::hash::CryptoHash; -use near_primitives::network::PeerId; -use near_primitives::syncing::get_num_state_parts; -use near_primitives::time::{Clock, Utc}; -use near_primitives::types::validator_stake::ValidatorStake; -use near_primitives::types::{ - AccountId, BlockHeight, BlockHeightDelta, EpochId, ShardId, StateRoot, -}; -use near_primitives::utils::to_timestamp; - -use near_chain::chain::{ApplyStatePartsRequest, StateSplitRequest}; -use near_client_primitives::types::{ - DownloadStatus, ShardSyncDownload, ShardSyncStatus, StateSplitApplyingStatus, SyncStatus, -}; -use near_network::types::AccountOrPeerIdOrHash; -use near_network::types::PeerManagerMessageRequest; -use near_o11y::WithSpanContextExt; -use near_primitives::shard_layout::ShardUId; - -/// Maximum number of block headers send over the network. -pub const MAX_BLOCK_HEADERS: u64 = 512; - -/// Maximum number of block header hashes to send as part of a locator. -pub const MAX_BLOCK_HEADER_HASHES: usize = 20; - -/// Maximum number of block requested at once in BlockSync -const MAX_BLOCK_REQUESTS: usize = 5; - -const BLOCK_REQUEST_TIMEOUT: i64 = 2; - -/// Maximum number of state parts to request per peer on each round when node is trying to download the state. -pub const MAX_STATE_PART_REQUEST: u64 = 16; -/// Number of state parts already requested stored as pending. -/// This number should not exceed MAX_STATE_PART_REQUEST times (number of peers in the network). -pub const MAX_PENDING_PART: u64 = MAX_STATE_PART_REQUEST * 10000; - -pub const NS_PER_SECOND: u128 = 1_000_000_000; - -/// Helper to keep track of the Epoch Sync -// TODO #3488 -#[allow(dead_code)] -pub struct EpochSync { - network_adapter: Arc, - /// Datastructure to keep track of when the last request to each peer was made. - /// Peers do not respond to Epoch Sync requests more frequently than once per a certain time - /// interval, thus there's no point in requesting more frequently. - peer_to_last_request_time: HashMap>, - /// Tracks all the peers who have reported that we are already up to date - peers_reporting_up_to_date: HashSet, - /// The last epoch we are synced to - current_epoch_id: EpochId, - /// The next epoch id we need to sync - next_epoch_id: EpochId, - /// The block producers set to validate the light client block view for the next epoch - next_block_producers: Vec, - /// The last epoch id that we have requested - requested_epoch_id: EpochId, - /// When and to whom was the last request made - last_request_time: DateTime, - last_request_peer_id: Option, - - /// How long to wait for a response before re-requesting the same light client block view - request_timeout: Duration, - /// How frequently to send request to the same peer - peer_timeout: Duration, - - /// True, if all peers agreed that we're at the last Epoch. - /// Only finalization is needed. - have_all_epochs: bool, - /// Whether the Epoch Sync was performed to completion previously. - /// Current state machine allows for only one Epoch Sync. - pub done: bool, - - pub sync_hash: CryptoHash, - - received_epoch: bool, - - is_just_started: bool, -} - -impl EpochSync { - pub fn new( - network_adapter: Arc, - genesis_epoch_id: EpochId, - genesis_next_epoch_id: EpochId, - first_epoch_block_producers: Vec, - request_timeout: TimeDuration, - peer_timeout: TimeDuration, - ) -> Self { - Self { - network_adapter, - peer_to_last_request_time: HashMap::new(), - peers_reporting_up_to_date: HashSet::new(), - current_epoch_id: genesis_epoch_id.clone(), - next_epoch_id: genesis_next_epoch_id, - next_block_producers: first_epoch_block_producers, - requested_epoch_id: genesis_epoch_id, - last_request_time: Clock::utc(), - last_request_peer_id: None, - request_timeout: Duration::from_std(request_timeout).unwrap(), - peer_timeout: Duration::from_std(peer_timeout).unwrap(), - received_epoch: false, - have_all_epochs: false, - done: false, - sync_hash: CryptoHash::default(), - is_just_started: true, - } - } -} - -/// Helper to keep track of sync headers. -/// Handles major re-orgs by finding closest header that matches and re-downloading headers from that point. -pub struct HeaderSync { - network_adapter: Arc, - history_locator: Vec<(BlockHeight, CryptoHash)>, - prev_header_sync: (DateTime, BlockHeight, BlockHeight, BlockHeight), - syncing_peer: Option, - stalling_ts: Option>, - - initial_timeout: Duration, - progress_timeout: Duration, - stall_ban_timeout: Duration, - expected_height_per_second: u64, -} - -impl HeaderSync { - pub fn new( - network_adapter: Arc, - initial_timeout: TimeDuration, - progress_timeout: TimeDuration, - stall_ban_timeout: TimeDuration, - expected_height_per_second: u64, - ) -> Self { - HeaderSync { - network_adapter, - history_locator: vec![], - prev_header_sync: (Clock::utc(), 0, 0, 0), - syncing_peer: None, - stalling_ts: None, - initial_timeout: Duration::from_std(initial_timeout).unwrap(), - progress_timeout: Duration::from_std(progress_timeout).unwrap(), - stall_ban_timeout: Duration::from_std(stall_ban_timeout).unwrap(), - expected_height_per_second, - } - } - - pub fn run( - &mut self, - sync_status: &mut SyncStatus, - chain: &Chain, - highest_height: BlockHeight, - highest_height_peers: &[HighestHeightPeerInfo], - ) -> Result<(), near_chain::Error> { - let _span = tracing::debug_span!(target: "sync", "run", sync = "HeaderSync").entered(); - let header_head = chain.header_head()?; - if !self.header_sync_due(sync_status, &header_head, highest_height) { - return Ok(()); - } - - let enable_header_sync = match sync_status { - SyncStatus::HeaderSync { .. } - | SyncStatus::BodySync { .. } - | SyncStatus::StateSyncDone => true, - SyncStatus::NoSync | SyncStatus::AwaitingPeers | SyncStatus::EpochSync { .. } => { - debug!(target: "sync", "Sync: initial transition to Header sync. Header head {} at {}", - header_head.last_block_hash, header_head.height, - ); - self.history_locator.retain(|&x| x.0 == 0); - true - } - SyncStatus::StateSync { .. } => false, - }; - - if enable_header_sync { - let start_height = match sync_status.start_height() { - Some(height) => height, - None => chain.head()?.height, - }; - *sync_status = SyncStatus::HeaderSync { - start_height, - current_height: header_head.height, - highest_height, - }; - self.syncing_peer = None; - if let Some(peer) = highest_height_peers.choose(&mut thread_rng()).cloned() { - if peer.highest_block_height > header_head.height { - self.syncing_peer = self.request_headers(chain, peer); - } - } - } - - Ok(()) - } - - fn compute_expected_height( - &self, - old_height: BlockHeight, - time_delta: Duration, - ) -> BlockHeight { - (old_height as u128 - + (time_delta.num_nanoseconds().unwrap() as u128 - * self.expected_height_per_second as u128 - / NS_PER_SECOND)) as u64 - } - - fn header_sync_due( - &mut self, - sync_status: &SyncStatus, - header_head: &Tip, - highest_height: BlockHeight, - ) -> bool { - let now = Clock::utc(); - let (timeout, old_expected_height, prev_height, prev_highest_height) = - self.prev_header_sync; - - // Received all necessary header, can request more. - let all_headers_received = - header_head.height >= min(prev_height + MAX_BLOCK_HEADERS - 4, prev_highest_height); - - // Did we receive as many headers as we expected from the peer? Request more or ban peer. - let stalling = header_head.height <= old_expected_height && now > timeout; - - // Always enable header sync on initial state transition from NoSync / NoSyncFewBlocksBehind / AwaitingPeers. - let force_sync = match sync_status { - SyncStatus::NoSync | SyncStatus::AwaitingPeers => true, - _ => false, - }; - - if force_sync || all_headers_received || stalling { - self.prev_header_sync = ( - now + self.initial_timeout, - self.compute_expected_height(header_head.height, self.initial_timeout), - header_head.height, - highest_height, - ); - - if stalling { - if self.stalling_ts.is_none() { - self.stalling_ts = Some(now); - } - } else { - self.stalling_ts = None; - } - - if all_headers_received { - self.stalling_ts = None; - } else { - if let Some(ref stalling_ts) = self.stalling_ts { - if let Some(ref peer) = self.syncing_peer { - match sync_status { - SyncStatus::HeaderSync { highest_height, .. } => { - if now > *stalling_ts + self.stall_ban_timeout - && *highest_height == peer.highest_block_height - { - warn!(target: "sync", "Sync: ban a fraudulent peer: {}, claimed height: {}", - peer.peer_info, peer.highest_block_height); - self.network_adapter.do_send( - PeerManagerMessageRequest::NetworkRequests( - NetworkRequests::BanPeer { - peer_id: peer.peer_info.id.clone(), - ban_reason: - near_network::types::ReasonForBan::HeightFraud, - }, - ) - .with_span_context(), - ); - // This peer is fraudulent, let's skip this beat and wait for - // the next one when this peer is not in the list anymore. - self.syncing_peer = None; - return false; - } - } - _ => (), - } - } - } - } - self.syncing_peer = None; - true - } else { - // Resetting the timeout as long as we make progress. - let ns_time_till_timeout = - (to_timestamp(timeout).saturating_sub(to_timestamp(now))) as u128; - let remaining_expected_height = (self.expected_height_per_second as u128 - * ns_time_till_timeout - / NS_PER_SECOND) as u64; - if header_head.height >= old_expected_height.saturating_sub(remaining_expected_height) { - let new_expected_height = - self.compute_expected_height(header_head.height, self.progress_timeout); - self.prev_header_sync = ( - now + self.progress_timeout, - new_expected_height, - prev_height, - prev_highest_height, - ); - } - false - } - } - - /// Request headers from a given peer to advance the chain. - fn request_headers( - &mut self, - chain: &Chain, - peer: HighestHeightPeerInfo, - ) -> Option { - if let Ok(locator) = self.get_locator(chain) { - debug!(target: "sync", "Sync: request headers: asking {} for headers, {:?}", peer.peer_info.id, locator); - self.network_adapter.do_send( - PeerManagerMessageRequest::NetworkRequests(NetworkRequests::BlockHeadersRequest { - hashes: locator, - peer_id: peer.peer_info.id.clone(), - }) - .with_span_context(), - ); - return Some(peer); - } - None - } - - fn get_locator(&mut self, chain: &Chain) -> Result, near_chain::Error> { - let tip = chain.header_head()?; - let genesis_height = chain.genesis().height(); - let heights = get_locator_heights(tip.height - genesis_height) - .into_iter() - .map(|h| h + genesis_height) - .collect::>(); - - // For each height we need, we either check if something is close enough from last locator, or go to the db. - let mut locator: Vec<(u64, CryptoHash)> = vec![(tip.height, tip.last_block_hash)]; - for h in heights { - if let Some(x) = close_enough(&self.history_locator, h) { - locator.push(x); - } else { - // Walk backwards to find last known hash. - let last_loc = *locator.last().unwrap(); - if let Ok(header) = chain.get_block_header_by_height(h) { - if header.height() != last_loc.0 { - locator.push((header.height(), *header.hash())); - } - } - } - } - locator.dedup_by(|a, b| a.0 == b.0); - debug!(target: "sync", "Sync: locator: {:?}", locator); - self.history_locator = locator.clone(); - Ok(locator.iter().map(|x| x.1).collect()) - } -} - -/// Check if there is a close enough value to provided height in the locator. -fn close_enough(locator: &[(u64, CryptoHash)], height: u64) -> Option<(u64, CryptoHash)> { - if locator.len() == 0 { - return None; - } - // Check boundaries, if lower than the last. - if locator.last().unwrap().0 >= height { - return locator.last().map(|x| *x); - } - // Higher than first and first is within acceptable gap. - if locator[0].0 < height && height.saturating_sub(127) < locator[0].0 { - return Some(locator[0]); - } - for h in locator.windows(2) { - if height <= h[0].0 && height > h[1].0 { - if h[0].0 - height < height - h[1].0 { - return Some(h[0]); - } else { - return Some(h[1]); - } - } - } - None -} - -/// Given height stepping back to 0 in powers of 2 steps. -fn get_locator_heights(height: u64) -> Vec { - let mut current = height; - let mut heights = vec![]; - while current > 0 { - heights.push(current); - if heights.len() >= MAX_BLOCK_HEADER_HASHES as usize - 1 { - break; - } - let next = 2u64.pow(heights.len() as u32); - current = if current > next { current - next } else { 0 }; - } - heights.push(0); - heights -} - -#[derive(Clone)] -pub struct BlockSyncRequest { - // head of the chain at the time of the request - head: CryptoHash, - // when the block was requested - when: DateTime, -} - -/// Helper to track block syncing. -pub struct BlockSync { - network_adapter: Arc, - last_request: Option, - /// How far to fetch blocks vs fetch state. - block_fetch_horizon: BlockHeightDelta, - /// Whether to enforce block sync - archive: bool, -} - -impl BlockSync { - pub fn new( - network_adapter: Arc, - block_fetch_horizon: BlockHeightDelta, - archive: bool, - ) -> Self { - BlockSync { network_adapter, last_request: None, block_fetch_horizon, archive } - } - - /// Runs check if block sync is needed, if it's needed and it's too far - sync state is started instead (returning true). - /// Otherwise requests recent blocks from peers. - pub fn run( - &mut self, - sync_status: &mut SyncStatus, - chain: &Chain, - highest_height: BlockHeight, - highest_height_peers: &[HighestHeightPeerInfo], - ) -> Result { - let _span = tracing::debug_span!(target: "sync", "run", sync = "BlockSync").entered(); - if self.block_sync_due(chain)? { - if self.block_sync(chain, highest_height_peers)? { - debug!(target: "sync", "Sync: transition to State Sync."); - return Ok(true); - } - } - - let head = chain.head()?; - let start_height = match sync_status.start_height() { - Some(height) => height, - None => head.height, - }; - *sync_status = - SyncStatus::BodySync { start_height, current_height: head.height, highest_height }; - Ok(false) - } - - /// Check if state download is required - fn check_state_needed(&self, chain: &Chain) -> Result { - let head = chain.head()?; - let header_head = chain.header_head()?; - - // If latest block is up to date return early. - // No state download is required, neither any blocks need to be fetched. - if head.height >= header_head.height { - return Ok(false); - } - - // Don't run State Sync if header head is not more than one epoch ahead. - if head.epoch_id != header_head.epoch_id && head.next_epoch_id != header_head.epoch_id { - if head.height < header_head.height.saturating_sub(self.block_fetch_horizon) - && !self.archive - { - // Epochs are different and we are too far from horizon, State Sync is needed - return Ok(true); - } - } - - Ok(false) - } - - /// Returns true if state download is required (last known block is too far). - /// Otherwise request recent blocks from peers round robin. - fn block_sync( - &mut self, - chain: &Chain, - highest_height_peers: &[HighestHeightPeerInfo], - ) -> Result { - if self.check_state_needed(chain)? { - return Ok(true); - } - - let chain_head = chain.head()?; - // update last request now because we want to update it whether or not the rest of the logic - // succeeds - self.last_request = - Some(BlockSyncRequest { head: chain_head.last_block_hash, when: Clock::utc() }); - - // reference_hash is the last block on the canonical chain that is in store (processed) - let reference_hash = { - let reference_hash = chain_head.last_block_hash; - - // The current chain head may not be on the canonical chain. - // Now we find the most recent block we know on the canonical chain. - // In practice the forks from the last final block are very short, so it is - // acceptable to perform this on each request - let header = chain.get_block_header(&reference_hash)?; - let mut candidate = (header.height(), *header.hash(), *header.prev_hash()); - - // First go back until we find the common block - while match chain.get_block_header_by_height(candidate.0) { - Ok(header) => header.hash() != &candidate.1, - Err(e) => match e { - near_chain::Error::DBNotFoundErr(_) => true, - _ => return Err(e), - }, - } { - let prev_header = chain.get_block_header(&candidate.2)?; - candidate = (prev_header.height(), *prev_header.hash(), *prev_header.prev_hash()); - } - - // Then go forward for as long as we know the next block - let mut ret_hash = candidate.1; - loop { - match chain.store().get_next_block_hash(&ret_hash) { - Ok(hash) => { - if chain.block_exists(&hash)? { - ret_hash = hash; - } else { - break; - } - } - Err(e) => match e { - near_chain::Error::DBNotFoundErr(_) => break, - _ => return Err(e), - }, - } - } - - ret_hash - }; - - // Look ahead for MAX_BLOCK_REQUESTS blocks and add the ones we don't have yet - let mut requests = vec![]; - let mut next_hash = reference_hash; - for _ in 0..MAX_BLOCK_REQUESTS { - match chain.store().get_next_block_hash(&next_hash) { - Ok(hash) => next_hash = hash, - Err(e) => match e { - near_chain::Error::DBNotFoundErr(_) => break, - _ => return Err(e), - }, - } - if let Ok(()) = check_known(chain, &next_hash)? { - let next_height = chain.get_block_header(&next_hash)?.height(); - requests.push((next_height, next_hash)); - } - } - - let header_head = chain.header_head()?; - - let gc_stop_height = chain.runtime_adapter.get_gc_stop_height(&header_head.last_block_hash); - - for request in requests { - let (height, hash) = request; - let request_from_archival = self.archive && height < gc_stop_height; - let peer = if request_from_archival { - let archival_peer_iter = highest_height_peers.iter().filter(|p| p.archival); - archival_peer_iter.choose(&mut rand::thread_rng()) - } else { - let peer_iter = highest_height_peers.iter(); - peer_iter.choose(&mut rand::thread_rng()) - }; - - if let Some(peer) = peer { - debug!(target: "sync", "Block sync: {}/{} requesting block {} at height {} from {} (out of {} peers)", - chain_head.height, header_head.height, hash, height, peer.peer_info.id, highest_height_peers.len()); - self.network_adapter.do_send( - PeerManagerMessageRequest::NetworkRequests(NetworkRequests::BlockRequest { - hash, - peer_id: peer.peer_info.id.clone(), - }) - .with_span_context(), - ); - } else { - warn!(target: "sync", "Block sync: {}/{} No available {}peers to request block {} from", - chain_head.height, header_head.height, if request_from_archival { "archival " } else { "" }, hash); - } - } - - Ok(false) - } - - /// Check if we should run block body sync and ask for more full blocks. - /// Block sync is due either if the chain head has changed since the last request - /// or if time since the last request is > BLOCK_REQUEST_TIMEOUT - fn block_sync_due(&mut self, chain: &Chain) -> Result { - match &self.last_request { - None => Ok(true), - Some(request) => Ok(chain.head()?.last_block_hash != request.head - || Clock::utc() - request.when > Duration::seconds(BLOCK_REQUEST_TIMEOUT)), - } - } -} - -pub enum StateSyncResult { - /// No shard has changed its status - Unchanged, - /// At least one shard has changed its status - /// Boolean parameter specifies whether the client needs to start fetching the block - Changed(bool), - /// The state for all shards was downloaded. - Completed, -} - -struct PendingRequestStatus { - missing_parts: usize, - wait_until: DateTime, -} - -impl PendingRequestStatus { - fn new(timeout: Duration) -> Self { - Self { missing_parts: 1, wait_until: Clock::utc().add(timeout) } - } - fn expired(&self) -> bool { - Clock::utc() > self.wait_until - } -} - -/// Private to public API conversion. -fn make_account_or_peer_id_or_hash( - from: near_network::types::AccountOrPeerIdOrHash, -) -> near_client_primitives::types::AccountOrPeerIdOrHash { - type From = near_network::types::AccountOrPeerIdOrHash; - type To = near_client_primitives::types::AccountOrPeerIdOrHash; - match from { - From::AccountId(a) => To::AccountId(a), - From::PeerId(p) => To::PeerId(p), - From::Hash(h) => To::Hash(h), - } -} - -/// Helper to track state sync. -pub struct StateSync { - network_adapter: Arc, - - state_sync_time: HashMap>, - last_time_block_requested: Option>, - - last_part_id_requested: HashMap<(AccountOrPeerIdOrHash, ShardId), PendingRequestStatus>, - /// Map from which part we requested to whom. - requested_target: lru::LruCache<(u64, CryptoHash), AccountOrPeerIdOrHash>, - - timeout: Duration, - - /// Maps shard_id to result of applying downloaded state - state_parts_apply_results: HashMap>, - - /// Maps shard_id to result of splitting state for resharding - split_state_roots: HashMap, Error>>, -} - -impl StateSync { - pub fn new(network_adapter: Arc, timeout: TimeDuration) -> Self { - StateSync { - network_adapter, - state_sync_time: Default::default(), - last_time_block_requested: None, - last_part_id_requested: Default::default(), - requested_target: lru::LruCache::new(MAX_PENDING_PART as usize), - timeout: Duration::from_std(timeout).unwrap(), - state_parts_apply_results: HashMap::new(), - split_state_roots: HashMap::new(), - } - } - - pub fn sync_block_status( - &mut self, - prev_hash: &CryptoHash, - chain: &Chain, - now: DateTime, - ) -> Result<(bool, bool), near_chain::Error> { - let (request_block, have_block) = if !chain.block_exists(prev_hash)? { - match self.last_time_block_requested { - None => (true, false), - Some(last_time) => { - if now - last_time >= self.timeout { - error!(target: "sync", "State sync: block request for {} timed out in {} seconds", prev_hash, self.timeout.num_seconds()); - (true, false) - } else { - (false, false) - } - } - } - } else { - self.last_time_block_requested = None; - (false, true) - }; - if request_block { - self.last_time_block_requested = Some(now); - }; - Ok((request_block, have_block)) - } - - // In the tuple of bools returned by this function, the first one - // indicates whether something has changed in `new_shard_sync`, - // and therefore whether the client needs to update its - // `sync_status`. The second indicates whether state sync is - // finished, in which case the client will transition to block sync - pub fn sync_shards_status( - &mut self, - me: &Option, - sync_hash: CryptoHash, - new_shard_sync: &mut HashMap, - chain: &mut Chain, - runtime_adapter: &Arc, - highest_height_peers: &[HighestHeightPeerInfo], - tracking_shards: Vec, - now: DateTime, - state_parts_task_scheduler: &dyn Fn(ApplyStatePartsRequest), - state_split_scheduler: &dyn Fn(StateSplitRequest), - ) -> Result<(bool, bool), near_chain::Error> { - let mut all_done = true; - let mut update_sync_status = false; - let init_sync_download = ShardSyncDownload { - downloads: vec![ - DownloadStatus { - start_time: now, - prev_update_time: now, - run_me: Arc::new(AtomicBool::new(true)), - error: false, - done: false, - state_requests_count: 0, - last_target: None, - }; - 1 - ], - status: ShardSyncStatus::StateDownloadHeader, - }; - - let prev_hash = *chain.get_block_header(&sync_hash)?.prev_hash(); - let prev_epoch_id = chain.get_block_header(&prev_hash)?.epoch_id().clone(); - let epoch_id = chain.get_block_header(&sync_hash)?.epoch_id().clone(); - if chain.runtime_adapter.get_shard_layout(&prev_epoch_id)? - != chain.runtime_adapter.get_shard_layout(&epoch_id)? - { - error!("cannot sync to the first epoch after sharding upgrade"); - panic!("cannot sync to the first epoch after sharding upgrade. Please wait for the next epoch or find peers that are more up to date"); - } - let split_states = runtime_adapter.will_shard_layout_change_next_epoch(&prev_hash)?; - - for shard_id in tracking_shards { - let mut download_timeout = false; - let mut need_shard = false; - let shard_sync_download = new_shard_sync.entry(shard_id).or_insert_with(|| { - need_shard = true; - update_sync_status = true; - init_sync_download.clone() - }); - let old_status = shard_sync_download.status.clone(); - let mut this_done = false; - match &shard_sync_download.status { - ShardSyncStatus::StateDownloadHeader => { - if shard_sync_download.downloads[0].done { - 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); - *shard_sync_download = ShardSyncDownload { - downloads: vec![ - DownloadStatus { - start_time: now, - prev_update_time: now, - run_me: Arc::new(AtomicBool::new(true)), - error: false, - done: false, - state_requests_count: 0, - last_target: None, - }; - state_num_parts as usize - ], - status: ShardSyncStatus::StateDownloadParts, - }; - need_shard = true; - } else { - let prev = shard_sync_download.downloads[0].prev_update_time; - let error = shard_sync_download.downloads[0].error; - download_timeout = now - prev > self.timeout; - if download_timeout || error { - shard_sync_download.downloads[0].run_me.store(true, Ordering::SeqCst); - shard_sync_download.downloads[0].error = false; - shard_sync_download.downloads[0].prev_update_time = now; - } - if shard_sync_download.downloads[0].run_me.load(Ordering::SeqCst) { - need_shard = true; - } - } - } - ShardSyncStatus::StateDownloadParts => { - let mut parts_done = true; - for part_download in shard_sync_download.downloads.iter_mut() { - if !part_download.done { - parts_done = false; - let prev = part_download.prev_update_time; - let error = part_download.error; - let part_timeout = now - prev > self.timeout; - if part_timeout || error { - download_timeout |= part_timeout; - part_download.run_me.store(true, Ordering::SeqCst); - part_download.error = false; - part_download.prev_update_time = now; - } - if part_download.run_me.load(Ordering::SeqCst) { - need_shard = true; - } - } - } - if parts_done { - *shard_sync_download = ShardSyncDownload { - downloads: vec![], - status: ShardSyncStatus::StateDownloadScheduling, - }; - } - } - ShardSyncStatus::StateDownloadScheduling => { - 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); - match chain.schedule_apply_state_parts( - shard_id, - sync_hash, - state_num_parts, - state_parts_task_scheduler, - ) { - Ok(()) => { - *shard_sync_download = ShardSyncDownload { - downloads: vec![], - status: ShardSyncStatus::StateDownloadApplying, - } - } - Err(e) => { - // Cannot finalize the downloaded state. - // The reasonable behavior here is to start from the very beginning. - error!(target: "sync", "State sync finalizing error, shard = {}, hash = {}: {:?}", shard_id, sync_hash, e); - *shard_sync_download = init_sync_download.clone(); - chain.clear_downloaded_parts(shard_id, sync_hash, state_num_parts)?; - } - } - } - ShardSyncStatus::StateDownloadApplying => { - let result = self.state_parts_apply_results.remove(&shard_id); - if let Some(result) = result { - match chain.set_state_finalize(shard_id, sync_hash, result) { - Ok(()) => { - *shard_sync_download = ShardSyncDownload { - downloads: vec![], - status: ShardSyncStatus::StateDownloadComplete, - } - } - Err(e) => { - // Cannot finalize the downloaded state. - // The reasonable behavior here is to start from the very beginning. - error!(target: "sync", "State sync finalizing error, shard = {}, hash = {}: {:?}", shard_id, sync_hash, e); - *shard_sync_download = init_sync_download.clone(); - 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, - )?; - } - } - } - } - ShardSyncStatus::StateDownloadComplete => { - 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)?; - if split_states { - *shard_sync_download = ShardSyncDownload { - downloads: vec![], - status: ShardSyncStatus::StateSplitScheduling, - } - } else { - *shard_sync_download = ShardSyncDownload { - downloads: vec![], - status: ShardSyncStatus::StateSyncDone, - }; - this_done = true; - } - } - ShardSyncStatus::StateSplitScheduling => { - debug_assert!(split_states); - let status = Arc::new(StateSplitApplyingStatus::new()); - chain.build_state_for_split_shards_preprocessing( - &sync_hash, - shard_id, - state_split_scheduler, - status.clone(), - )?; - debug!(target: "sync", "State sync split scheduled: me {:?}, shard = {}, hash = {}", me, shard_id, sync_hash); - *shard_sync_download = ShardSyncDownload { - downloads: vec![], - status: ShardSyncStatus::StateSplitApplying(status), - }; - } - ShardSyncStatus::StateSplitApplying(_status) => { - debug_assert!(split_states); - let result = self.split_state_roots.remove(&shard_id); - if let Some(state_roots) = result { - chain - .build_state_for_split_shards_postprocessing(&sync_hash, state_roots)?; - *shard_sync_download = ShardSyncDownload { - downloads: vec![], - status: ShardSyncStatus::StateSyncDone, - }; - this_done = true; - } - } - ShardSyncStatus::StateSyncDone => { - this_done = true; - } - } - all_done &= this_done; - - if download_timeout { - warn!(target: "sync", "State sync didn't download the state for shard {} in {} seconds, sending StateRequest again", shard_id, self.timeout.num_seconds()); - info!(target: "sync", "State sync status: me {:?}, sync_hash {}, phase {}", - me, - sync_hash, - match shard_sync_download.status { - ShardSyncStatus::StateDownloadHeader => format!("{} requests sent {}, last target {:?}", - Purple.bold().paint(format!("HEADER")), - shard_sync_download.downloads[0].state_requests_count, - shard_sync_download.downloads[0].last_target), - ShardSyncStatus::StateDownloadParts => { let mut text = "".to_string(); - for (i, download) in shard_sync_download.downloads.iter().enumerate() { - text.push_str(&format!("[{}: {}, {}, {:?}] ", - Yellow.bold().paint(i.to_string()), - download.done, - download.state_requests_count, - download.last_target)); - } - format!("{} [{}: is_done, requests sent, last target] {}", - Purple.bold().paint("PARTS"), - Yellow.bold().paint("part_id"), - text) - } - _ => unreachable!("timeout cannot happen when all state is downloaded"), - }, - ); - } - - // Execute syncing for shard `shard_id` - if need_shard { - update_sync_status = true; - *shard_sync_download = self.request_shard( - me, - shard_id, - chain, - runtime_adapter, - sync_hash, - shard_sync_download.clone(), - highest_height_peers, - )?; - } - update_sync_status |= shard_sync_download.status != old_status; - } - - Ok((update_sync_status, all_done)) - } - - pub fn set_apply_result(&mut self, shard_id: ShardId, apply_result: Result<(), Error>) { - self.state_parts_apply_results.insert(shard_id, apply_result); - } - - pub fn set_split_result( - &mut self, - shard_id: ShardId, - result: Result, Error>, - ) { - self.split_state_roots.insert(shard_id, result); - } - - /// Find the hash of the first block on the same epoch (and chain) of block with hash `sync_hash`. - pub fn get_epoch_start_sync_hash( - chain: &Chain, - sync_hash: &CryptoHash, - ) -> Result { - let mut header = chain.get_block_header(sync_hash)?; - let mut epoch_id = header.epoch_id().clone(); - let mut hash = *header.hash(); - let mut prev_hash = *header.prev_hash(); - loop { - if prev_hash == CryptoHash::default() { - return Ok(hash); - } - header = chain.get_block_header(&prev_hash)?; - if &epoch_id != header.epoch_id() { - return Ok(hash); - } - epoch_id = header.epoch_id().clone(); - hash = *header.hash(); - prev_hash = *header.prev_hash(); - } - } - - fn sent_request_part( - &mut self, - target: AccountOrPeerIdOrHash, - part_id: u64, - shard_id: ShardId, - sync_hash: CryptoHash, - ) { - self.requested_target.put((part_id, sync_hash), target.clone()); - - let timeout = self.timeout; - self.last_part_id_requested - .entry((target, shard_id)) - .and_modify(|pending_request| { - pending_request.missing_parts += 1; - }) - .or_insert_with(|| PendingRequestStatus::new(timeout)); - } - - pub fn received_requested_part( - &mut self, - part_id: u64, - shard_id: ShardId, - sync_hash: CryptoHash, - ) { - let key = (part_id, sync_hash); - if let Some(target) = self.requested_target.get(&key) { - if self.last_part_id_requested.get_mut(&(target.clone(), shard_id)).map_or( - false, - |request| { - request.missing_parts = request.missing_parts.saturating_sub(1); - request.missing_parts == 0 - }, - ) { - self.last_part_id_requested.remove(&(target.clone(), shard_id)); - } - } - } - - /// Find possible targets to download state from. - /// Candidates are validators at current epoch and peers at highest height. - /// Only select candidates that we have no pending request currently ongoing. - fn possible_targets( - &mut self, - me: &Option, - shard_id: ShardId, - chain: &Chain, - runtime_adapter: &Arc, - sync_hash: CryptoHash, - highest_height_peers: &[HighestHeightPeerInfo], - ) -> Result, Error> { - // Remove candidates from pending list if request expired due to timeout - self.last_part_id_requested.retain(|_, request| !request.expired()); - - let prev_block_hash = *chain.get_block_header(&sync_hash)?.prev_hash(); - let epoch_hash = runtime_adapter.get_epoch_id_from_prev_block(&prev_block_hash)?; - - Ok(runtime_adapter - .get_epoch_block_producers_ordered(&epoch_hash, &sync_hash)? - .iter() - .filter_map(|(validator_stake, _slashed)| { - let account_id = validator_stake.account_id(); - if runtime_adapter.cares_about_shard( - Some(account_id), - &prev_block_hash, - shard_id, - false, - ) { - if me.as_ref().map(|me| me != account_id).unwrap_or(true) { - Some(AccountOrPeerIdOrHash::AccountId(account_id.clone())) - } else { - None - } - } else { - None - } - }) - .chain(highest_height_peers.iter().filter_map(|peer| { - if peer.tracked_shards.contains(&shard_id) { - Some(AccountOrPeerIdOrHash::PeerId(peer.peer_info.id.clone())) - } else { - None - } - })) - .filter(|candidate| { - !self.last_part_id_requested.contains_key(&(candidate.clone(), shard_id)) - }) - .collect::>()) - } - - /// Returns new ShardSyncDownload if successful, otherwise returns given shard_sync_download - pub fn request_shard( - &mut self, - me: &Option, - shard_id: ShardId, - chain: &Chain, - runtime_adapter: &Arc, - sync_hash: CryptoHash, - shard_sync_download: ShardSyncDownload, - highest_height_peers: &[HighestHeightPeerInfo], - ) -> Result { - let possible_targets = self.possible_targets( - me, - shard_id, - chain, - runtime_adapter, - sync_hash, - highest_height_peers, - )?; - - if possible_targets.is_empty() { - return Ok(shard_sync_download); - } - - // Downloading strategy starts here - let mut new_shard_sync_download = shard_sync_download.clone(); - - match shard_sync_download.status { - ShardSyncStatus::StateDownloadHeader => { - let target = possible_targets.choose(&mut thread_rng()).cloned().unwrap(); - assert!(new_shard_sync_download.downloads[0].run_me.load(Ordering::SeqCst)); - new_shard_sync_download.downloads[0].run_me.store(false, Ordering::SeqCst); - new_shard_sync_download.downloads[0].state_requests_count += 1; - new_shard_sync_download.downloads[0].last_target = - Some(make_account_or_peer_id_or_hash(target.clone())); - let run_me = new_shard_sync_download.downloads[0].run_me.clone(); - near_performance_metrics::actix::spawn( - std::any::type_name::(), - self.network_adapter - .send( - PeerManagerMessageRequest::NetworkRequests( - NetworkRequests::StateRequestHeader { shard_id, sync_hash, target }, - ) - .with_span_context(), - ) - .then(move |result| { - if let Ok(NetworkResponses::RouteNotFound) = - result.map(|f| f.as_network_response()) - { - // Send a StateRequestHeader on the next iteration - run_me.store(true, Ordering::SeqCst); - } - future::ready(()) - }), - ); - } - ShardSyncStatus::StateDownloadParts => { - let possible_targets_sampler = - SamplerLimited::new(possible_targets, MAX_STATE_PART_REQUEST); - - // Iterate over all parts that needs to be requested (i.e. download.run_me is true). - // Parts are ordered such that its index match its part_id. - // Finally, for every part that needs to be requested it is selected one peer (target) randomly - // to request the part from - for ((part_id, download), target) in new_shard_sync_download - .downloads - .iter_mut() - .enumerate() - .filter(|(_, download)| download.run_me.load(Ordering::SeqCst)) - .zip(possible_targets_sampler) - { - self.sent_request_part(target.clone(), part_id as u64, shard_id, sync_hash); - download.run_me.store(false, Ordering::SeqCst); - download.state_requests_count += 1; - download.last_target = Some(make_account_or_peer_id_or_hash(target.clone())); - let run_me = download.run_me.clone(); - - near_performance_metrics::actix::spawn( - std::any::type_name::(), - self.network_adapter - .send( - PeerManagerMessageRequest::NetworkRequests( - NetworkRequests::StateRequestPart { - shard_id, - sync_hash, - part_id: part_id as u64, - target: target.clone(), - }, - ) - .with_span_context(), - ) - .then(move |result| { - if let Ok(NetworkResponses::RouteNotFound) = - result.map(|f| f.as_network_response()) - { - // Send a StateRequestPart on the next iteration - run_me.store(true, Ordering::SeqCst); - } - future::ready(()) - }), - ); - } - } - _ => {} - } - - Ok(new_shard_sync_download) - } - - pub fn run( - &mut self, - me: &Option, - sync_hash: CryptoHash, - new_shard_sync: &mut HashMap, - chain: &mut Chain, - runtime_adapter: &Arc, - highest_height_peers: &[HighestHeightPeerInfo], - tracking_shards: Vec, - state_parts_task_scheduler: &dyn Fn(ApplyStatePartsRequest), - state_split_scheduler: &dyn Fn(StateSplitRequest), - ) -> Result { - let _span = tracing::debug_span!(target: "sync", "run", sync = "StateSync").entered(); - debug!(target: "sync", %sync_hash, ?tracking_shards, "syncing state"); - let prev_hash = *chain.get_block_header(&sync_hash)?.prev_hash(); - let now = Clock::utc(); - - let (request_block, have_block) = self.sync_block_status(&prev_hash, chain, now)?; - - if tracking_shards.is_empty() { - // This case is possible if a validator cares about the same shards in the new epoch as - // in the previous (or about a subset of them), return success right away - - return if !have_block { - Ok(StateSyncResult::Changed(request_block)) - } else { - Ok(StateSyncResult::Completed) - }; - } - - let (update_sync_status, all_done) = self.sync_shards_status( - me, - sync_hash, - new_shard_sync, - chain, - runtime_adapter, - highest_height_peers, - tracking_shards, - now, - state_parts_task_scheduler, - state_split_scheduler, - )?; - - if have_block && all_done { - self.state_sync_time.clear(); - return Ok(StateSyncResult::Completed); - } - - Ok(if update_sync_status || request_block { - StateSyncResult::Changed(request_block) - } else { - StateSyncResult::Unchanged - }) - } -} - -/// Create an abstract collection of elements to be shuffled. -/// Each element will appear in the shuffled output exactly `limit` times. -/// Use it as an iterator to access the shuffled collection. -/// -/// ```rust,ignore -/// let sampler = SamplerLimited::new(vec![1, 2, 3], 2); -/// -/// let res = sampler.collect::>(); -/// -/// assert!(res.len() == 6); -/// assert!(res.iter().filter(|v| v == 1).count() == 2); -/// assert!(res.iter().filter(|v| v == 2).count() == 2); -/// assert!(res.iter().filter(|v| v == 3).count() == 2); -/// ``` -/// -/// Out of the 90 possible values of `res` in the code above on of them is: -/// -/// ``` -/// vec![1, 2, 1, 3, 3, 2]; -/// ``` -struct SamplerLimited { - data: Vec, - limit: Vec, -} - -impl SamplerLimited { - fn new(data: Vec, limit: u64) -> Self { - if limit == 0 { - Self { data: vec![], limit: vec![] } - } else { - let len = data.len(); - Self { data, limit: vec![limit; len] } - } - } -} - -impl Iterator for SamplerLimited { - type Item = T; - - fn next(&mut self) -> Option { - if self.limit.is_empty() { - None - } else { - let len = self.limit.len(); - let ix = thread_rng().gen_range(0..len); - self.limit[ix] -= 1; - - if self.limit[ix] == 0 { - if ix + 1 != len { - self.limit[ix] = self.limit[len - 1]; - self.data.swap(ix, len - 1); - } - - self.limit.pop(); - self.data.pop() - } else { - Some(self.data[ix].clone()) - } - } - } -} - -#[cfg(test)] -mod test { - use std::sync::Arc; - use std::thread; - - use near_chain::test_utils::{ - process_block_sync, setup, setup_with_validators, wait_for_all_blocks_in_processing, - ValidatorSchedule, - }; - use near_chain::{BlockProcessingArtifact, ChainGenesis, Provenance}; - use near_crypto::{KeyType, PublicKey}; - use near_network::test_utils::MockPeerManagerAdapter; - use near_o11y::testonly::TracingCapture; - use near_primitives::block::{Approval, Block, GenesisId}; - use near_primitives::network::PeerId; - use near_primitives::utils::MaybeValidated; - - use super::*; - use crate::test_utils::TestEnv; - use near_network::types::{BlockInfo, FullPeerInfo, PeerInfo}; - use near_primitives::merkle::PartialMerkleTree; - use near_primitives::types::EpochId; - use near_primitives::validator_signer::InMemoryValidatorSigner; - use near_primitives::version::PROTOCOL_VERSION; - use num_rational::Ratio; - use std::collections::HashSet; - - #[test] - fn test_get_locator_heights() { - assert_eq!(get_locator_heights(0), vec![0]); - assert_eq!(get_locator_heights(1), vec![1, 0]); - assert_eq!(get_locator_heights(2), vec![2, 0]); - assert_eq!(get_locator_heights(3), vec![3, 1, 0]); - assert_eq!(get_locator_heights(10), vec![10, 8, 4, 0]); - assert_eq!(get_locator_heights(100), vec![100, 98, 94, 86, 70, 38, 0]); - assert_eq!( - get_locator_heights(1000), - vec![1000, 998, 994, 986, 970, 938, 874, 746, 490, 0] - ); - // Locator is still reasonable size even given large height. - assert_eq!( - get_locator_heights(10000), - vec![10000, 9998, 9994, 9986, 9970, 9938, 9874, 9746, 9490, 8978, 7954, 5906, 1810, 0,] - ); - } - - /// Starts two chains that fork of genesis and checks that they can sync heaaders to the longest. - #[test] - fn test_sync_headers_fork() { - let mock_adapter = Arc::new(MockPeerManagerAdapter::default()); - let mut header_sync = HeaderSync::new( - mock_adapter.clone(), - TimeDuration::from_secs(10), - TimeDuration::from_secs(2), - TimeDuration::from_secs(120), - 1_000_000_000, - ); - let (mut chain, _, signer) = setup(); - for _ in 0..3 { - let prev = chain.get_block(&chain.head().unwrap().last_block_hash).unwrap(); - let block = Block::empty(&prev, &*signer); - process_block_sync( - &mut chain, - &None, - block.into(), - Provenance::PRODUCED, - &mut BlockProcessingArtifact::default(), - ) - .unwrap(); - } - let (mut chain2, _, signer2) = setup(); - for _ in 0..5 { - let prev = chain2.get_block(&chain2.head().unwrap().last_block_hash).unwrap(); - let block = Block::empty(&prev, &*signer2); - process_block_sync( - &mut chain2, - &None, - block.into(), - Provenance::PRODUCED, - &mut BlockProcessingArtifact::default(), - ) - .unwrap(); - } - let mut sync_status = SyncStatus::NoSync; - let peer1 = FullPeerInfo { - peer_info: PeerInfo::random(), - chain_info: near_network::types::PeerChainInfo { - genesis_id: GenesisId { - chain_id: "unittest".to_string(), - hash: *chain.genesis().hash(), - }, - tracked_shards: vec![], - archival: false, - last_block: Some(BlockInfo { - height: chain2.head().unwrap().height, - hash: chain2.head().unwrap().last_block_hash, - }), - }, - }; - let head = chain.head().unwrap(); - assert!(header_sync - .run( - &mut sync_status, - &mut chain, - head.height, - &[>>::into(peer1.clone()).unwrap()] - ) - .is_ok()); - assert!(sync_status.is_syncing()); - // Check that it queried last block, and then stepped down to genesis block to find common block with the peer. - - let item = mock_adapter.pop().unwrap().as_network_requests(); - assert_eq!( - item, - NetworkRequests::BlockHeadersRequest { - hashes: [3, 1, 0] - .iter() - .map(|i| *chain.get_block_by_height(*i).unwrap().hash()) - .collect(), - peer_id: peer1.peer_info.id - } - ); - } - - /// Sets up `HeaderSync` with particular tolerance for slowness, and makes sure that a peer that - /// sends headers below the threshold gets banned, and the peer that sends them faster doesn't get - /// banned. - /// Also makes sure that if `header_sync_due` is checked more frequently than the `progress_timeout` - /// the peer doesn't get banned. (specifically, that the expected height downloaded gets properly - /// adjusted for time passed) - #[test] - fn test_slow_header_sync() { - let network_adapter = Arc::new(MockPeerManagerAdapter::default()); - let highest_height = 1000; - - // Setup header_sync with expectation of 25 headers/second - let mut header_sync = HeaderSync::new( - network_adapter.clone(), - TimeDuration::from_secs(1), - TimeDuration::from_secs(1), - TimeDuration::from_secs(3), - 25, - ); - - let set_syncing_peer = |header_sync: &mut HeaderSync| { - header_sync.syncing_peer = Some(HighestHeightPeerInfo { - peer_info: PeerInfo { - id: PeerId::new(PublicKey::empty(KeyType::ED25519)), - addr: None, - account_id: None, - }, - genesis_id: Default::default(), - highest_block_height: 0, - highest_block_hash: Default::default(), - tracked_shards: vec![], - archival: false, - }); - header_sync.syncing_peer.as_mut().unwrap().highest_block_height = highest_height; - }; - set_syncing_peer(&mut header_sync); - - let vs = ValidatorSchedule::new().block_producers_per_epoch(vec![vec![ - "test0", "test1", "test2", "test3", "test4", - ] - .iter() - .map(|x| x.parse().unwrap()) - .collect()]); - let (chain, _, signers) = setup_with_validators(vs, 1000, 100); - let genesis = chain.get_block(&chain.genesis().hash().clone()).unwrap(); - - let mut last_block = &genesis; - let mut all_blocks = vec![]; - let mut block_merkle_tree = PartialMerkleTree::default(); - for i in 0..61 { - let current_height = 3 + i * 5; - - let approvals = [None, None, Some("test3"), Some("test4")] - .iter() - .map(|account_id| { - account_id.map(|account_id| { - let signer = InMemoryValidatorSigner::from_seed( - account_id.parse().unwrap(), - KeyType::ED25519, - account_id, - ); - Approval::new( - *last_block.hash(), - last_block.header().height(), - current_height, - &signer, - ) - .signature - }) - }) - .collect(); - let (epoch_id, next_epoch_id) = - if last_block.header().prev_hash() == &CryptoHash::default() { - (last_block.header().next_epoch_id().clone(), EpochId(*last_block.hash())) - } else { - ( - last_block.header().epoch_id().clone(), - last_block.header().next_epoch_id().clone(), - ) - }; - let block = Block::produce( - PROTOCOL_VERSION, - PROTOCOL_VERSION, - last_block.header(), - current_height, - last_block.header().block_ordinal() + 1, - last_block.chunks().iter().cloned().collect(), - epoch_id, - next_epoch_id, - None, - approvals, - Ratio::new(0, 1), - 0, - 100, - Some(0), - vec![], - vec![], - &*signers[3], - *last_block.header().next_bp_hash(), - block_merkle_tree.root(), - None, - ); - block_merkle_tree.insert(*block.hash()); - - all_blocks.push(block); - - last_block = &all_blocks[all_blocks.len() - 1]; - } - - let mut last_added_block_ord = 0; - // First send 30 heights every second for a while and make sure it doesn't get - // banned - for _iter in 0..12 { - let block = &all_blocks[last_added_block_ord]; - let current_height = block.header().height(); - set_syncing_peer(&mut header_sync); - header_sync.header_sync_due( - &SyncStatus::HeaderSync { - start_height: current_height, - current_height, - highest_height, - }, - &Tip::from_header(block.header()), - highest_height, - ); - - last_added_block_ord += 3; - - thread::sleep(TimeDuration::from_millis(500)); - } - // 6 blocks / second is fast enough, we should not have banned the peer - assert!(network_adapter.requests.read().unwrap().is_empty()); - - // Now the same, but only 20 heights / sec - for _iter in 0..12 { - let block = &all_blocks[last_added_block_ord]; - let current_height = block.header().height(); - set_syncing_peer(&mut header_sync); - header_sync.header_sync_due( - &SyncStatus::HeaderSync { - start_height: current_height, - current_height, - highest_height, - }, - &Tip::from_header(block.header()), - highest_height, - ); - - last_added_block_ord += 2; - - thread::sleep(TimeDuration::from_millis(500)); - } - // This time the peer should be banned, because 4 blocks/s is not fast enough - let ban_peer = network_adapter.requests.write().unwrap().pop_back().unwrap(); - - if let NetworkRequests::BanPeer { .. } = ban_peer.as_network_requests() { - /* expected */ - } else { - assert!(false); - } - } - - /// Helper function for block sync tests - fn collect_hashes_from_network_adapter( - network_adapter: &MockPeerManagerAdapter, - ) -> HashSet { - let mut network_request = network_adapter.requests.write().unwrap(); - network_request - .drain(..) - .map(|request| match request { - PeerManagerMessageRequest::NetworkRequests(NetworkRequests::BlockRequest { - hash, - .. - }) => hash, - _ => panic!("unexpected network request {:?}", request), - }) - .collect() - } - - fn check_hashes_from_network_adapter( - network_adapter: &MockPeerManagerAdapter, - expected_hashes: Vec, - ) { - let collected_hashes = collect_hashes_from_network_adapter(network_adapter); - assert_eq!(collected_hashes, expected_hashes.into_iter().collect()); - } - - fn create_highest_height_peer_infos(num_peers: usize) -> Vec { - (0..num_peers) - .map(|_| HighestHeightPeerInfo { - peer_info: PeerInfo { - id: PeerId::new(PublicKey::empty(KeyType::ED25519)), - addr: None, - account_id: None, - }, - genesis_id: Default::default(), - highest_block_height: 0, - highest_block_hash: Default::default(), - tracked_shards: vec![], - archival: false, - }) - .collect() - } - - #[test] - fn test_block_sync() { - let mut capture = TracingCapture::enable(); - let network_adapter = Arc::new(MockPeerManagerAdapter::default()); - let block_fetch_horizon = 10; - let mut block_sync = BlockSync::new(network_adapter.clone(), block_fetch_horizon, false); - let mut chain_genesis = ChainGenesis::test(); - chain_genesis.epoch_length = 100; - let mut env = TestEnv::builder(chain_genesis).clients_count(2).build(); - let mut blocks = vec![]; - for i in 1..5 * MAX_BLOCK_REQUESTS + 1 { - let block = env.clients[0].produce_block(i as u64).unwrap().unwrap(); - blocks.push(block.clone()); - env.process_block(0, block, Provenance::PRODUCED); - } - let block_headers = blocks.iter().map(|b| b.header().clone()).collect::>(); - let peer_infos = create_highest_height_peer_infos(2); - let mut challenges = vec![]; - env.clients[1].chain.sync_block_headers(block_headers, &mut challenges).unwrap(); - assert!(challenges.is_empty()); - - // fetch three blocks at a time - for i in 0..3 { - let is_state_sync = block_sync.block_sync(&env.clients[1].chain, &peer_infos).unwrap(); - assert!(!is_state_sync); - - let expected_blocks: Vec<_> = - blocks[i * MAX_BLOCK_REQUESTS..(i + 1) * MAX_BLOCK_REQUESTS].to_vec(); - check_hashes_from_network_adapter( - &network_adapter, - expected_blocks.iter().map(|b| *b.hash()).collect(), - ); - - for block in expected_blocks { - env.process_block(1, block, Provenance::NONE); - } - } - - // Now test when the node receives the block out of order - // fetch the next three blocks - let is_state_sync = block_sync.block_sync(&env.clients[1].chain, &peer_infos).unwrap(); - assert!(!is_state_sync); - check_hashes_from_network_adapter( - &network_adapter, - (3 * MAX_BLOCK_REQUESTS..4 * MAX_BLOCK_REQUESTS).map(|h| *blocks[h].hash()).collect(), - ); - // assumes that we only get block[4*MAX_BLOCK_REQUESTS-1] - let _ = env.clients[1].process_block_test( - MaybeValidated::from(blocks[4 * MAX_BLOCK_REQUESTS - 1].clone()), - Provenance::NONE, - ); - // the next block sync should not request block[4*MAX_BLOCK_REQUESTS-1] again - let is_state_sync = block_sync.block_sync(&env.clients[1].chain, &peer_infos).unwrap(); - assert!(!is_state_sync); - check_hashes_from_network_adapter( - &network_adapter, - (3 * MAX_BLOCK_REQUESTS..4 * MAX_BLOCK_REQUESTS - 1) - .map(|h| *blocks[h].hash()) - .collect(), - ); - - // Receive all blocks. Should not request more. As an extra - // complication, pause the processing of one block. - env.pause_block_processing(&mut capture, blocks[4 * MAX_BLOCK_REQUESTS - 1].hash()); - for i in 3 * MAX_BLOCK_REQUESTS..5 * MAX_BLOCK_REQUESTS { - let _ = env.clients[1] - .process_block_test(MaybeValidated::from(blocks[i].clone()), Provenance::NONE); - } - block_sync.block_sync(&env.clients[1].chain, &peer_infos).unwrap(); - let requested_block_hashes = collect_hashes_from_network_adapter(&network_adapter); - assert!(requested_block_hashes.is_empty(), "{:?}", requested_block_hashes); - - // Now finish paused processing processing and sanity check that we - // still are fully synced. - env.resume_block_processing(blocks[4 * MAX_BLOCK_REQUESTS - 1].hash()); - wait_for_all_blocks_in_processing(&mut env.clients[1].chain); - let requested_block_hashes = collect_hashes_from_network_adapter(&network_adapter); - assert!(requested_block_hashes.is_empty(), "{:?}", requested_block_hashes); - } - - #[test] - fn test_block_sync_archival() { - let network_adapter = Arc::new(MockPeerManagerAdapter::default()); - let block_fetch_horizon = 10; - let mut block_sync = BlockSync::new(network_adapter.clone(), block_fetch_horizon, true); - let mut chain_genesis = ChainGenesis::test(); - chain_genesis.epoch_length = 5; - let mut env = TestEnv::builder(chain_genesis).clients_count(2).build(); - let mut blocks = vec![]; - for i in 1..41 { - let block = env.clients[0].produce_block(i).unwrap().unwrap(); - blocks.push(block.clone()); - env.process_block(0, block, Provenance::PRODUCED); - } - let block_headers = blocks.iter().map(|b| b.header().clone()).collect::>(); - let peer_infos = create_highest_height_peer_infos(2); - let mut challenges = vec![]; - env.clients[1].chain.sync_block_headers(block_headers, &mut challenges).unwrap(); - assert!(challenges.is_empty()); - let is_state_sync = block_sync.block_sync(&env.clients[1].chain, &peer_infos).unwrap(); - assert!(!is_state_sync); - let requested_block_hashes = collect_hashes_from_network_adapter(&network_adapter); - // We don't have archival peers, and thus cannot request any blocks - assert_eq!(requested_block_hashes, HashSet::new()); - - let mut peer_infos = create_highest_height_peer_infos(2); - for peer in peer_infos.iter_mut() { - peer.archival = true; - } - let is_state_sync = block_sync.block_sync(&env.clients[1].chain, &peer_infos).unwrap(); - assert!(!is_state_sync); - let requested_block_hashes = collect_hashes_from_network_adapter(&network_adapter); - assert_eq!( - requested_block_hashes, - blocks.iter().take(MAX_BLOCK_REQUESTS).map(|b| *b.hash()).collect::>() - ); - } -} diff --git a/chain/client/src/sync/block.rs b/chain/client/src/sync/block.rs new file mode 100644 index 00000000000..71e7bae36bc --- /dev/null +++ b/chain/client/src/sync/block.rs @@ -0,0 +1,408 @@ +use near_chain::{check_known, ChainStoreAccess}; + +use std::sync::Arc; + +use chrono::{DateTime, Duration}; +use rand::seq::IteratorRandom; + +use tracing::{debug, warn}; + +use near_chain::Chain; +use near_network::types::{HighestHeightPeerInfo, NetworkRequests, PeerManagerAdapter}; +use near_primitives::hash::CryptoHash; + +use near_primitives::time::{Clock, Utc}; + +use near_primitives::types::{BlockHeight, BlockHeightDelta}; + +use near_client_primitives::types::SyncStatus; + +use near_network::types::PeerManagerMessageRequest; +use near_o11y::WithSpanContextExt; + +/// Maximum number of block requested at once in BlockSync +const MAX_BLOCK_REQUESTS: usize = 5; + +const BLOCK_REQUEST_TIMEOUT: i64 = 2; + +#[derive(Clone)] +pub struct BlockSyncRequest { + // head of the chain at the time of the request + head: CryptoHash, + // when the block was requested + when: DateTime, +} + +/// Helper to track block syncing. +pub struct BlockSync { + network_adapter: Arc, + last_request: Option, + /// How far to fetch blocks vs fetch state. + block_fetch_horizon: BlockHeightDelta, + /// Whether to enforce block sync + archive: bool, +} + +impl BlockSync { + pub fn new( + network_adapter: Arc, + block_fetch_horizon: BlockHeightDelta, + archive: bool, + ) -> Self { + BlockSync { network_adapter, last_request: None, block_fetch_horizon, archive } + } + + /// Runs check if block sync is needed, if it's needed and it's too far - sync state is started instead (returning true). + /// Otherwise requests recent blocks from peers. + pub fn run( + &mut self, + sync_status: &mut SyncStatus, + chain: &Chain, + highest_height: BlockHeight, + highest_height_peers: &[HighestHeightPeerInfo], + ) -> Result { + let _span = tracing::debug_span!(target: "sync", "run", sync = "BlockSync").entered(); + if self.block_sync_due(chain)? { + if self.block_sync(chain, highest_height_peers)? { + debug!(target: "sync", "Sync: transition to State Sync."); + return Ok(true); + } + } + + let head = chain.head()?; + let start_height = match sync_status.start_height() { + Some(height) => height, + None => head.height, + }; + *sync_status = + SyncStatus::BodySync { start_height, current_height: head.height, highest_height }; + Ok(false) + } + + /// Check if state download is required + fn check_state_needed(&self, chain: &Chain) -> Result { + let head = chain.head()?; + let header_head = chain.header_head()?; + + // If latest block is up to date return early. + // No state download is required, neither any blocks need to be fetched. + if head.height >= header_head.height { + return Ok(false); + } + + // Don't run State Sync if header head is not more than one epoch ahead. + if head.epoch_id != header_head.epoch_id && head.next_epoch_id != header_head.epoch_id { + if head.height < header_head.height.saturating_sub(self.block_fetch_horizon) + && !self.archive + { + // Epochs are different and we are too far from horizon, State Sync is needed + return Ok(true); + } + } + + Ok(false) + } + + /// Returns true if state download is required (last known block is too far). + /// Otherwise request recent blocks from peers round robin. + fn block_sync( + &mut self, + chain: &Chain, + highest_height_peers: &[HighestHeightPeerInfo], + ) -> Result { + if self.check_state_needed(chain)? { + return Ok(true); + } + + let chain_head = chain.head()?; + // update last request now because we want to update it whether or not the rest of the logic + // succeeds + self.last_request = + Some(BlockSyncRequest { head: chain_head.last_block_hash, when: Clock::utc() }); + + // reference_hash is the last block on the canonical chain that is in store (processed) + let reference_hash = { + let reference_hash = chain_head.last_block_hash; + + // The current chain head may not be on the canonical chain. + // Now we find the most recent block we know on the canonical chain. + // In practice the forks from the last final block are very short, so it is + // acceptable to perform this on each request + let header = chain.get_block_header(&reference_hash)?; + let mut candidate = (header.height(), *header.hash(), *header.prev_hash()); + + // First go back until we find the common block + while match chain.get_block_header_by_height(candidate.0) { + Ok(header) => header.hash() != &candidate.1, + Err(e) => match e { + near_chain::Error::DBNotFoundErr(_) => true, + _ => return Err(e), + }, + } { + let prev_header = chain.get_block_header(&candidate.2)?; + candidate = (prev_header.height(), *prev_header.hash(), *prev_header.prev_hash()); + } + + // Then go forward for as long as we know the next block + let mut ret_hash = candidate.1; + loop { + match chain.store().get_next_block_hash(&ret_hash) { + Ok(hash) => { + if chain.block_exists(&hash)? { + ret_hash = hash; + } else { + break; + } + } + Err(e) => match e { + near_chain::Error::DBNotFoundErr(_) => break, + _ => return Err(e), + }, + } + } + + ret_hash + }; + + // Look ahead for MAX_BLOCK_REQUESTS blocks and add the ones we don't have yet + let mut requests = vec![]; + let mut next_hash = reference_hash; + for _ in 0..MAX_BLOCK_REQUESTS { + match chain.store().get_next_block_hash(&next_hash) { + Ok(hash) => next_hash = hash, + Err(e) => match e { + near_chain::Error::DBNotFoundErr(_) => break, + _ => return Err(e), + }, + } + if let Ok(()) = check_known(chain, &next_hash)? { + let next_height = chain.get_block_header(&next_hash)?.height(); + requests.push((next_height, next_hash)); + } + } + + let header_head = chain.header_head()?; + + let gc_stop_height = chain.runtime_adapter.get_gc_stop_height(&header_head.last_block_hash); + + for request in requests { + let (height, hash) = request; + let request_from_archival = self.archive && height < gc_stop_height; + let peer = if request_from_archival { + let archival_peer_iter = highest_height_peers.iter().filter(|p| p.archival); + archival_peer_iter.choose(&mut rand::thread_rng()) + } else { + let peer_iter = highest_height_peers.iter(); + peer_iter.choose(&mut rand::thread_rng()) + }; + + if let Some(peer) = peer { + debug!(target: "sync", "Block sync: {}/{} requesting block {} at height {} from {} (out of {} peers)", + chain_head.height, header_head.height, hash, height, peer.peer_info.id, highest_height_peers.len()); + self.network_adapter.do_send( + PeerManagerMessageRequest::NetworkRequests(NetworkRequests::BlockRequest { + hash, + peer_id: peer.peer_info.id.clone(), + }) + .with_span_context(), + ); + } else { + warn!(target: "sync", "Block sync: {}/{} No available {}peers to request block {} from", + chain_head.height, header_head.height, if request_from_archival { "archival " } else { "" }, hash); + } + } + + Ok(false) + } + + /// Check if we should run block body sync and ask for more full blocks. + /// Block sync is due either if the chain head has changed since the last request + /// or if time since the last request is > BLOCK_REQUEST_TIMEOUT + fn block_sync_due(&mut self, chain: &Chain) -> Result { + match &self.last_request { + None => Ok(true), + Some(request) => Ok(chain.head()?.last_block_hash != request.head + || Clock::utc() - request.when > Duration::seconds(BLOCK_REQUEST_TIMEOUT)), + } + } +} + +#[cfg(test)] +mod test { + use std::sync::Arc; + + use near_chain::test_utils::wait_for_all_blocks_in_processing; + use near_chain::{ChainGenesis, Provenance}; + use near_crypto::{KeyType, PublicKey}; + use near_network::test_utils::MockPeerManagerAdapter; + use near_o11y::testonly::TracingCapture; + + use near_primitives::network::PeerId; + use near_primitives::utils::MaybeValidated; + + use super::*; + use crate::test_utils::TestEnv; + use near_network::types::PeerInfo; + + use std::collections::HashSet; + + /// Helper function for block sync tests + fn collect_hashes_from_network_adapter( + network_adapter: &MockPeerManagerAdapter, + ) -> HashSet { + let mut network_request = network_adapter.requests.write().unwrap(); + network_request + .drain(..) + .map(|request| match request { + PeerManagerMessageRequest::NetworkRequests(NetworkRequests::BlockRequest { + hash, + .. + }) => hash, + _ => panic!("unexpected network request {:?}", request), + }) + .collect() + } + + fn check_hashes_from_network_adapter( + network_adapter: &MockPeerManagerAdapter, + expected_hashes: Vec, + ) { + let collected_hashes = collect_hashes_from_network_adapter(network_adapter); + assert_eq!(collected_hashes, expected_hashes.into_iter().collect()); + } + + fn create_highest_height_peer_infos(num_peers: usize) -> Vec { + (0..num_peers) + .map(|_| HighestHeightPeerInfo { + peer_info: PeerInfo { + id: PeerId::new(PublicKey::empty(KeyType::ED25519)), + addr: None, + account_id: None, + }, + genesis_id: Default::default(), + highest_block_height: 0, + highest_block_hash: Default::default(), + tracked_shards: vec![], + archival: false, + }) + .collect() + } + + #[test] + fn test_block_sync() { + let mut capture = TracingCapture::enable(); + let network_adapter = Arc::new(MockPeerManagerAdapter::default()); + let block_fetch_horizon = 10; + let mut block_sync = BlockSync::new(network_adapter.clone(), block_fetch_horizon, false); + let mut chain_genesis = ChainGenesis::test(); + chain_genesis.epoch_length = 100; + let mut env = TestEnv::builder(chain_genesis).clients_count(2).build(); + let mut blocks = vec![]; + for i in 1..5 * MAX_BLOCK_REQUESTS + 1 { + let block = env.clients[0].produce_block(i as u64).unwrap().unwrap(); + blocks.push(block.clone()); + env.process_block(0, block, Provenance::PRODUCED); + } + let block_headers = blocks.iter().map(|b| b.header().clone()).collect::>(); + let peer_infos = create_highest_height_peer_infos(2); + let mut challenges = vec![]; + env.clients[1].chain.sync_block_headers(block_headers, &mut challenges).unwrap(); + assert!(challenges.is_empty()); + + // fetch three blocks at a time + for i in 0..3 { + let is_state_sync = block_sync.block_sync(&env.clients[1].chain, &peer_infos).unwrap(); + assert!(!is_state_sync); + + let expected_blocks: Vec<_> = + blocks[i * MAX_BLOCK_REQUESTS..(i + 1) * MAX_BLOCK_REQUESTS].to_vec(); + check_hashes_from_network_adapter( + &network_adapter, + expected_blocks.iter().map(|b| *b.hash()).collect(), + ); + + for block in expected_blocks { + env.process_block(1, block, Provenance::NONE); + } + } + + // Now test when the node receives the block out of order + // fetch the next three blocks + let is_state_sync = block_sync.block_sync(&env.clients[1].chain, &peer_infos).unwrap(); + assert!(!is_state_sync); + check_hashes_from_network_adapter( + &network_adapter, + (3 * MAX_BLOCK_REQUESTS..4 * MAX_BLOCK_REQUESTS).map(|h| *blocks[h].hash()).collect(), + ); + // assumes that we only get block[4*MAX_BLOCK_REQUESTS-1] + let _ = env.clients[1].process_block_test( + MaybeValidated::from(blocks[4 * MAX_BLOCK_REQUESTS - 1].clone()), + Provenance::NONE, + ); + // the next block sync should not request block[4*MAX_BLOCK_REQUESTS-1] again + let is_state_sync = block_sync.block_sync(&env.clients[1].chain, &peer_infos).unwrap(); + assert!(!is_state_sync); + check_hashes_from_network_adapter( + &network_adapter, + (3 * MAX_BLOCK_REQUESTS..4 * MAX_BLOCK_REQUESTS - 1) + .map(|h| *blocks[h].hash()) + .collect(), + ); + + // Receive all blocks. Should not request more. As an extra + // complication, pause the processing of one block. + env.pause_block_processing(&mut capture, blocks[4 * MAX_BLOCK_REQUESTS - 1].hash()); + for i in 3 * MAX_BLOCK_REQUESTS..5 * MAX_BLOCK_REQUESTS { + let _ = env.clients[1] + .process_block_test(MaybeValidated::from(blocks[i].clone()), Provenance::NONE); + } + block_sync.block_sync(&env.clients[1].chain, &peer_infos).unwrap(); + let requested_block_hashes = collect_hashes_from_network_adapter(&network_adapter); + assert!(requested_block_hashes.is_empty(), "{:?}", requested_block_hashes); + + // Now finish paused processing processing and sanity check that we + // still are fully synced. + env.resume_block_processing(blocks[4 * MAX_BLOCK_REQUESTS - 1].hash()); + wait_for_all_blocks_in_processing(&mut env.clients[1].chain); + let requested_block_hashes = collect_hashes_from_network_adapter(&network_adapter); + assert!(requested_block_hashes.is_empty(), "{:?}", requested_block_hashes); + } + + #[test] + fn test_block_sync_archival() { + let network_adapter = Arc::new(MockPeerManagerAdapter::default()); + let block_fetch_horizon = 10; + let mut block_sync = BlockSync::new(network_adapter.clone(), block_fetch_horizon, true); + let mut chain_genesis = ChainGenesis::test(); + chain_genesis.epoch_length = 5; + let mut env = TestEnv::builder(chain_genesis).clients_count(2).build(); + let mut blocks = vec![]; + for i in 1..41 { + let block = env.clients[0].produce_block(i).unwrap().unwrap(); + blocks.push(block.clone()); + env.process_block(0, block, Provenance::PRODUCED); + } + let block_headers = blocks.iter().map(|b| b.header().clone()).collect::>(); + let peer_infos = create_highest_height_peer_infos(2); + let mut challenges = vec![]; + env.clients[1].chain.sync_block_headers(block_headers, &mut challenges).unwrap(); + assert!(challenges.is_empty()); + let is_state_sync = block_sync.block_sync(&env.clients[1].chain, &peer_infos).unwrap(); + assert!(!is_state_sync); + let requested_block_hashes = collect_hashes_from_network_adapter(&network_adapter); + // We don't have archival peers, and thus cannot request any blocks + assert_eq!(requested_block_hashes, HashSet::new()); + + let mut peer_infos = create_highest_height_peer_infos(2); + for peer in peer_infos.iter_mut() { + peer.archival = true; + } + let is_state_sync = block_sync.block_sync(&env.clients[1].chain, &peer_infos).unwrap(); + assert!(!is_state_sync); + let requested_block_hashes = collect_hashes_from_network_adapter(&network_adapter); + assert_eq!( + requested_block_hashes, + blocks.iter().take(MAX_BLOCK_REQUESTS).map(|b| *b.hash()).collect::>() + ); + } +} diff --git a/chain/client/src/sync/epoch.rs b/chain/client/src/sync/epoch.rs new file mode 100644 index 00000000000..6f9a984125e --- /dev/null +++ b/chain/client/src/sync/epoch.rs @@ -0,0 +1,87 @@ +use std::collections::{HashMap, HashSet}; + +use std::sync::Arc; +use std::time::Duration as TimeDuration; + +use chrono::{DateTime, Duration}; + +use near_network::types::PeerManagerAdapter; + +use near_primitives::hash::CryptoHash; +use near_primitives::network::PeerId; + +use near_primitives::time::{Clock, Utc}; +use near_primitives::types::validator_stake::ValidatorStake; +use near_primitives::types::EpochId; + +/// Helper to keep track of the Epoch Sync +// TODO #3488 +#[allow(dead_code)] +pub struct EpochSync { + network_adapter: Arc, + /// Datastructure to keep track of when the last request to each peer was made. + /// Peers do not respond to Epoch Sync requests more frequently than once per a certain time + /// interval, thus there's no point in requesting more frequently. + peer_to_last_request_time: HashMap>, + /// Tracks all the peers who have reported that we are already up to date + peers_reporting_up_to_date: HashSet, + /// The last epoch we are synced to + current_epoch_id: EpochId, + /// The next epoch id we need to sync + next_epoch_id: EpochId, + /// The block producers set to validate the light client block view for the next epoch + next_block_producers: Vec, + /// The last epoch id that we have requested + requested_epoch_id: EpochId, + /// When and to whom was the last request made + last_request_time: DateTime, + last_request_peer_id: Option, + + /// How long to wait for a response before re-requesting the same light client block view + request_timeout: Duration, + /// How frequently to send request to the same peer + peer_timeout: Duration, + + /// True, if all peers agreed that we're at the last Epoch. + /// Only finalization is needed. + have_all_epochs: bool, + /// Whether the Epoch Sync was performed to completion previously. + /// Current state machine allows for only one Epoch Sync. + pub done: bool, + + pub sync_hash: CryptoHash, + + received_epoch: bool, + + is_just_started: bool, +} + +impl EpochSync { + pub fn new( + network_adapter: Arc, + genesis_epoch_id: EpochId, + genesis_next_epoch_id: EpochId, + first_epoch_block_producers: Vec, + request_timeout: TimeDuration, + peer_timeout: TimeDuration, + ) -> Self { + Self { + network_adapter, + peer_to_last_request_time: HashMap::new(), + peers_reporting_up_to_date: HashSet::new(), + current_epoch_id: genesis_epoch_id.clone(), + next_epoch_id: genesis_next_epoch_id, + next_block_producers: first_epoch_block_producers, + requested_epoch_id: genesis_epoch_id, + last_request_time: Clock::utc(), + last_request_peer_id: None, + request_timeout: Duration::from_std(request_timeout).unwrap(), + peer_timeout: Duration::from_std(peer_timeout).unwrap(), + received_epoch: false, + have_all_epochs: false, + done: false, + sync_hash: CryptoHash::default(), + is_just_started: true, + } + } +} diff --git a/chain/client/src/sync/header.rs b/chain/client/src/sync/header.rs new file mode 100644 index 00000000000..e21c145a813 --- /dev/null +++ b/chain/client/src/sync/header.rs @@ -0,0 +1,591 @@ +use std::cmp::min; +use std::sync::Arc; +use std::time::Duration as TimeDuration; + +use chrono::{DateTime, Duration}; +use rand::seq::SliceRandom; +use rand::thread_rng; +use tracing::{debug, warn}; + +use near_chain::Chain; +use near_network::types::{HighestHeightPeerInfo, NetworkRequests, PeerManagerAdapter}; +use near_primitives::block::Tip; +use near_primitives::hash::CryptoHash; +use near_primitives::time::{Clock, Utc}; +use near_primitives::types::BlockHeight; +use near_primitives::utils::to_timestamp; + +use near_client_primitives::types::SyncStatus; +use near_network::types::PeerManagerMessageRequest; +use near_o11y::WithSpanContextExt; + +/// Maximum number of block headers send over the network. +pub const MAX_BLOCK_HEADERS: u64 = 512; + +/// Maximum number of block header hashes to send as part of a locator. +pub const MAX_BLOCK_HEADER_HASHES: usize = 20; + +pub const NS_PER_SECOND: u128 = 1_000_000_000; + +/// Helper to keep track of sync headers. +/// Handles major re-orgs by finding closest header that matches and re-downloading headers from that point. +pub struct HeaderSync { + network_adapter: Arc, + history_locator: Vec<(BlockHeight, CryptoHash)>, + prev_header_sync: (DateTime, BlockHeight, BlockHeight, BlockHeight), + syncing_peer: Option, + stalling_ts: Option>, + + initial_timeout: Duration, + progress_timeout: Duration, + stall_ban_timeout: Duration, + expected_height_per_second: u64, +} + +impl HeaderSync { + pub fn new( + network_adapter: Arc, + initial_timeout: TimeDuration, + progress_timeout: TimeDuration, + stall_ban_timeout: TimeDuration, + expected_height_per_second: u64, + ) -> Self { + HeaderSync { + network_adapter, + history_locator: vec![], + prev_header_sync: (Clock::utc(), 0, 0, 0), + syncing_peer: None, + stalling_ts: None, + initial_timeout: Duration::from_std(initial_timeout).unwrap(), + progress_timeout: Duration::from_std(progress_timeout).unwrap(), + stall_ban_timeout: Duration::from_std(stall_ban_timeout).unwrap(), + expected_height_per_second, + } + } + + pub fn run( + &mut self, + sync_status: &mut SyncStatus, + chain: &Chain, + highest_height: BlockHeight, + highest_height_peers: &[HighestHeightPeerInfo], + ) -> Result<(), near_chain::Error> { + let _span = tracing::debug_span!(target: "sync", "run", sync = "HeaderSync").entered(); + let header_head = chain.header_head()?; + if !self.header_sync_due(sync_status, &header_head, highest_height) { + return Ok(()); + } + + let enable_header_sync = match sync_status { + SyncStatus::HeaderSync { .. } + | SyncStatus::BodySync { .. } + | SyncStatus::StateSyncDone => true, + SyncStatus::NoSync | SyncStatus::AwaitingPeers | SyncStatus::EpochSync { .. } => { + debug!(target: "sync", "Sync: initial transition to Header sync. Header head {} at {}", + header_head.last_block_hash, header_head.height, + ); + self.history_locator.retain(|&x| x.0 == 0); + true + } + SyncStatus::StateSync { .. } => false, + }; + + if enable_header_sync { + let start_height = match sync_status.start_height() { + Some(height) => height, + None => chain.head()?.height, + }; + *sync_status = SyncStatus::HeaderSync { + start_height, + current_height: header_head.height, + highest_height, + }; + self.syncing_peer = None; + if let Some(peer) = highest_height_peers.choose(&mut thread_rng()).cloned() { + if peer.highest_block_height > header_head.height { + self.syncing_peer = self.request_headers(chain, peer); + } + } + } + + Ok(()) + } + + fn compute_expected_height( + &self, + old_height: BlockHeight, + time_delta: Duration, + ) -> BlockHeight { + (old_height as u128 + + (time_delta.num_nanoseconds().unwrap() as u128 + * self.expected_height_per_second as u128 + / NS_PER_SECOND)) as u64 + } + + pub(crate) fn header_sync_due( + &mut self, + sync_status: &SyncStatus, + header_head: &Tip, + highest_height: BlockHeight, + ) -> bool { + let now = Clock::utc(); + let (timeout, old_expected_height, prev_height, prev_highest_height) = + self.prev_header_sync; + + // Received all necessary header, can request more. + let all_headers_received = + header_head.height >= min(prev_height + MAX_BLOCK_HEADERS - 4, prev_highest_height); + + // Did we receive as many headers as we expected from the peer? Request more or ban peer. + let stalling = header_head.height <= old_expected_height && now > timeout; + + // Always enable header sync on initial state transition from NoSync / NoSyncFewBlocksBehind / AwaitingPeers. + let force_sync = match sync_status { + SyncStatus::NoSync | SyncStatus::AwaitingPeers => true, + _ => false, + }; + + if force_sync || all_headers_received || stalling { + self.prev_header_sync = ( + now + self.initial_timeout, + self.compute_expected_height(header_head.height, self.initial_timeout), + header_head.height, + highest_height, + ); + + if stalling { + if self.stalling_ts.is_none() { + self.stalling_ts = Some(now); + } + } else { + self.stalling_ts = None; + } + + if all_headers_received { + self.stalling_ts = None; + } else { + if let Some(ref stalling_ts) = self.stalling_ts { + if let Some(ref peer) = self.syncing_peer { + match sync_status { + SyncStatus::HeaderSync { highest_height, .. } => { + if now > *stalling_ts + self.stall_ban_timeout + && *highest_height == peer.highest_block_height + { + warn!(target: "sync", "Sync: ban a fraudulent peer: {}, claimed height: {}", + peer.peer_info, peer.highest_block_height); + self.network_adapter.do_send( + PeerManagerMessageRequest::NetworkRequests( + NetworkRequests::BanPeer { + peer_id: peer.peer_info.id.clone(), + ban_reason: + near_network::types::ReasonForBan::HeightFraud, + }, + ) + .with_span_context(), + ); + // This peer is fraudulent, let's skip this beat and wait for + // the next one when this peer is not in the list anymore. + self.syncing_peer = None; + return false; + } + } + _ => (), + } + } + } + } + self.syncing_peer = None; + true + } else { + // Resetting the timeout as long as we make progress. + let ns_time_till_timeout = + (to_timestamp(timeout).saturating_sub(to_timestamp(now))) as u128; + let remaining_expected_height = (self.expected_height_per_second as u128 + * ns_time_till_timeout + / NS_PER_SECOND) as u64; + if header_head.height >= old_expected_height.saturating_sub(remaining_expected_height) { + let new_expected_height = + self.compute_expected_height(header_head.height, self.progress_timeout); + self.prev_header_sync = ( + now + self.progress_timeout, + new_expected_height, + prev_height, + prev_highest_height, + ); + } + false + } + } + + /// Request headers from a given peer to advance the chain. + fn request_headers( + &mut self, + chain: &Chain, + peer: HighestHeightPeerInfo, + ) -> Option { + if let Ok(locator) = self.get_locator(chain) { + debug!(target: "sync", "Sync: request headers: asking {} for headers, {:?}", peer.peer_info.id, locator); + self.network_adapter.do_send( + PeerManagerMessageRequest::NetworkRequests(NetworkRequests::BlockHeadersRequest { + hashes: locator, + peer_id: peer.peer_info.id.clone(), + }) + .with_span_context(), + ); + return Some(peer); + } + None + } + + fn get_locator(&mut self, chain: &Chain) -> Result, near_chain::Error> { + let tip = chain.header_head()?; + let genesis_height = chain.genesis().height(); + let heights = get_locator_heights(tip.height - genesis_height) + .into_iter() + .map(|h| h + genesis_height) + .collect::>(); + + // For each height we need, we either check if something is close enough from last locator, or go to the db. + let mut locator: Vec<(u64, CryptoHash)> = vec![(tip.height, tip.last_block_hash)]; + for h in heights { + if let Some(x) = close_enough(&self.history_locator, h) { + locator.push(x); + } else { + // Walk backwards to find last known hash. + let last_loc = *locator.last().unwrap(); + if let Ok(header) = chain.get_block_header_by_height(h) { + if header.height() != last_loc.0 { + locator.push((header.height(), *header.hash())); + } + } + } + } + locator.dedup_by(|a, b| a.0 == b.0); + debug!(target: "sync", "Sync: locator: {:?}", locator); + self.history_locator = locator.clone(); + Ok(locator.iter().map(|x| x.1).collect()) + } +} + +/// Check if there is a close enough value to provided height in the locator. +fn close_enough(locator: &[(u64, CryptoHash)], height: u64) -> Option<(u64, CryptoHash)> { + if locator.len() == 0 { + return None; + } + // Check boundaries, if lower than the last. + if locator.last().unwrap().0 >= height { + return locator.last().map(|x| *x); + } + // Higher than first and first is within acceptable gap. + if locator[0].0 < height && height.saturating_sub(127) < locator[0].0 { + return Some(locator[0]); + } + for h in locator.windows(2) { + if height <= h[0].0 && height > h[1].0 { + if h[0].0 - height < height - h[1].0 { + return Some(h[0]); + } else { + return Some(h[1]); + } + } + } + None +} + +/// Given height stepping back to 0 in powers of 2 steps. +fn get_locator_heights(height: u64) -> Vec { + let mut current = height; + let mut heights = vec![]; + while current > 0 { + heights.push(current); + if heights.len() >= MAX_BLOCK_HEADER_HASHES as usize - 1 { + break; + } + let next = 2u64.pow(heights.len() as u32); + current = if current > next { current - next } else { 0 }; + } + heights.push(0); + heights +} + +#[cfg(test)] +mod test { + use std::sync::Arc; + use std::thread; + + use near_chain::test_utils::{ + process_block_sync, setup, setup_with_validators, ValidatorSchedule, + }; + use near_chain::{BlockProcessingArtifact, Provenance}; + use near_crypto::{KeyType, PublicKey}; + use near_network::test_utils::MockPeerManagerAdapter; + use near_primitives::block::{Approval, Block, GenesisId}; + use near_primitives::network::PeerId; + + use super::*; + use near_network::types::{BlockInfo, FullPeerInfo, PeerInfo}; + use near_primitives::merkle::PartialMerkleTree; + use near_primitives::types::EpochId; + use near_primitives::validator_signer::InMemoryValidatorSigner; + use near_primitives::version::PROTOCOL_VERSION; + use num_rational::Ratio; + + #[test] + fn test_get_locator_heights() { + assert_eq!(get_locator_heights(0), vec![0]); + assert_eq!(get_locator_heights(1), vec![1, 0]); + assert_eq!(get_locator_heights(2), vec![2, 0]); + assert_eq!(get_locator_heights(3), vec![3, 1, 0]); + assert_eq!(get_locator_heights(10), vec![10, 8, 4, 0]); + assert_eq!(get_locator_heights(100), vec![100, 98, 94, 86, 70, 38, 0]); + assert_eq!( + get_locator_heights(1000), + vec![1000, 998, 994, 986, 970, 938, 874, 746, 490, 0] + ); + // Locator is still reasonable size even given large height. + assert_eq!( + get_locator_heights(10000), + vec![10000, 9998, 9994, 9986, 9970, 9938, 9874, 9746, 9490, 8978, 7954, 5906, 1810, 0,] + ); + } + + /// Starts two chains that fork of genesis and checks that they can sync heaaders to the longest. + #[test] + fn test_sync_headers_fork() { + let mock_adapter = Arc::new(MockPeerManagerAdapter::default()); + let mut header_sync = HeaderSync::new( + mock_adapter.clone(), + TimeDuration::from_secs(10), + TimeDuration::from_secs(2), + TimeDuration::from_secs(120), + 1_000_000_000, + ); + let (mut chain, _, signer) = setup(); + for _ in 0..3 { + let prev = chain.get_block(&chain.head().unwrap().last_block_hash).unwrap(); + let block = Block::empty(&prev, &*signer); + process_block_sync( + &mut chain, + &None, + block.into(), + Provenance::PRODUCED, + &mut BlockProcessingArtifact::default(), + ) + .unwrap(); + } + let (mut chain2, _, signer2) = setup(); + for _ in 0..5 { + let prev = chain2.get_block(&chain2.head().unwrap().last_block_hash).unwrap(); + let block = Block::empty(&prev, &*signer2); + process_block_sync( + &mut chain2, + &None, + block.into(), + Provenance::PRODUCED, + &mut BlockProcessingArtifact::default(), + ) + .unwrap(); + } + let mut sync_status = SyncStatus::NoSync; + let peer1 = FullPeerInfo { + peer_info: PeerInfo::random(), + chain_info: near_network::types::PeerChainInfo { + genesis_id: GenesisId { + chain_id: "unittest".to_string(), + hash: *chain.genesis().hash(), + }, + tracked_shards: vec![], + archival: false, + last_block: Some(BlockInfo { + height: chain2.head().unwrap().height, + hash: chain2.head().unwrap().last_block_hash, + }), + }, + }; + let head = chain.head().unwrap(); + assert!(header_sync + .run( + &mut sync_status, + &mut chain, + head.height, + &[>>::into(peer1.clone()).unwrap()] + ) + .is_ok()); + assert!(sync_status.is_syncing()); + // Check that it queried last block, and then stepped down to genesis block to find common block with the peer. + + let item = mock_adapter.pop().unwrap().as_network_requests(); + assert_eq!( + item, + NetworkRequests::BlockHeadersRequest { + hashes: [3, 1, 0] + .iter() + .map(|i| *chain.get_block_by_height(*i).unwrap().hash()) + .collect(), + peer_id: peer1.peer_info.id + } + ); + } + + /// Sets up `HeaderSync` with particular tolerance for slowness, and makes sure that a peer that + /// sends headers below the threshold gets banned, and the peer that sends them faster doesn't get + /// banned. + /// Also makes sure that if `header_sync_due` is checked more frequently than the `progress_timeout` + /// the peer doesn't get banned. (specifically, that the expected height downloaded gets properly + /// adjusted for time passed) + #[test] + fn test_slow_header_sync() { + let network_adapter = Arc::new(MockPeerManagerAdapter::default()); + let highest_height = 1000; + + // Setup header_sync with expectation of 25 headers/second + let mut header_sync = HeaderSync::new( + network_adapter.clone(), + TimeDuration::from_secs(1), + TimeDuration::from_secs(1), + TimeDuration::from_secs(3), + 25, + ); + + let set_syncing_peer = |header_sync: &mut HeaderSync| { + header_sync.syncing_peer = Some(HighestHeightPeerInfo { + peer_info: PeerInfo { + id: PeerId::new(PublicKey::empty(KeyType::ED25519)), + addr: None, + account_id: None, + }, + genesis_id: Default::default(), + highest_block_height: 0, + highest_block_hash: Default::default(), + tracked_shards: vec![], + archival: false, + }); + header_sync.syncing_peer.as_mut().unwrap().highest_block_height = highest_height; + }; + set_syncing_peer(&mut header_sync); + + let vs = ValidatorSchedule::new().block_producers_per_epoch(vec![vec![ + "test0", "test1", "test2", "test3", "test4", + ] + .iter() + .map(|x| x.parse().unwrap()) + .collect()]); + let (chain, _, signers) = setup_with_validators(vs, 1000, 100); + let genesis = chain.get_block(&chain.genesis().hash().clone()).unwrap(); + + let mut last_block = &genesis; + let mut all_blocks = vec![]; + let mut block_merkle_tree = PartialMerkleTree::default(); + for i in 0..61 { + let current_height = 3 + i * 5; + + let approvals = [None, None, Some("test3"), Some("test4")] + .iter() + .map(|account_id| { + account_id.map(|account_id| { + let signer = InMemoryValidatorSigner::from_seed( + account_id.parse().unwrap(), + KeyType::ED25519, + account_id, + ); + Approval::new( + *last_block.hash(), + last_block.header().height(), + current_height, + &signer, + ) + .signature + }) + }) + .collect(); + let (epoch_id, next_epoch_id) = + if last_block.header().prev_hash() == &CryptoHash::default() { + (last_block.header().next_epoch_id().clone(), EpochId(*last_block.hash())) + } else { + ( + last_block.header().epoch_id().clone(), + last_block.header().next_epoch_id().clone(), + ) + }; + let block = Block::produce( + PROTOCOL_VERSION, + PROTOCOL_VERSION, + last_block.header(), + current_height, + last_block.header().block_ordinal() + 1, + last_block.chunks().iter().cloned().collect(), + epoch_id, + next_epoch_id, + None, + approvals, + Ratio::new(0, 1), + 0, + 100, + Some(0), + vec![], + vec![], + &*signers[3], + *last_block.header().next_bp_hash(), + block_merkle_tree.root(), + None, + ); + block_merkle_tree.insert(*block.hash()); + + all_blocks.push(block); + + last_block = &all_blocks[all_blocks.len() - 1]; + } + + let mut last_added_block_ord = 0; + // First send 30 heights every second for a while and make sure it doesn't get + // banned + for _iter in 0..12 { + let block = &all_blocks[last_added_block_ord]; + let current_height = block.header().height(); + set_syncing_peer(&mut header_sync); + header_sync.header_sync_due( + &SyncStatus::HeaderSync { + start_height: current_height, + current_height, + highest_height, + }, + &Tip::from_header(block.header()), + highest_height, + ); + + last_added_block_ord += 3; + + thread::sleep(TimeDuration::from_millis(500)); + } + // 6 blocks / second is fast enough, we should not have banned the peer + assert!(network_adapter.requests.read().unwrap().is_empty()); + + // Now the same, but only 20 heights / sec + for _iter in 0..12 { + let block = &all_blocks[last_added_block_ord]; + let current_height = block.header().height(); + set_syncing_peer(&mut header_sync); + header_sync.header_sync_due( + &SyncStatus::HeaderSync { + start_height: current_height, + current_height, + highest_height, + }, + &Tip::from_header(block.header()), + highest_height, + ); + + last_added_block_ord += 2; + + thread::sleep(TimeDuration::from_millis(500)); + } + // This time the peer should be banned, because 4 blocks/s is not fast enough + let ban_peer = network_adapter.requests.write().unwrap().pop_back().unwrap(); + + if let NetworkRequests::BanPeer { .. } = ban_peer.as_network_requests() { + /* expected */ + } else { + assert!(false); + } + } +} diff --git a/chain/client/src/sync/mod.rs b/chain/client/src/sync/mod.rs new file mode 100644 index 00000000000..689af22095f --- /dev/null +++ b/chain/client/src/sync/mod.rs @@ -0,0 +1,4 @@ +pub mod block; +pub mod epoch; +pub mod header; +pub mod state; diff --git a/chain/client/src/sync/state.rs b/chain/client/src/sync/state.rs new file mode 100644 index 00000000000..31525847173 --- /dev/null +++ b/chain/client/src/sync/state.rs @@ -0,0 +1,759 @@ +use near_chain::{near_chain_primitives, Error}; +use std::collections::HashMap; +use std::ops::Add; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration as TimeDuration; + +use ansi_term::Color::{Purple, Yellow}; +use chrono::{DateTime, Duration}; +use futures::{future, FutureExt}; +use rand::seq::SliceRandom; +use rand::{thread_rng, Rng}; +use tracing::{debug, error, info, warn}; + +use near_chain::{Chain, RuntimeAdapter}; +use near_network::types::{ + HighestHeightPeerInfo, NetworkRequests, NetworkResponses, PeerManagerAdapter, +}; +use near_primitives::hash::CryptoHash; +use near_primitives::syncing::get_num_state_parts; +use near_primitives::time::{Clock, Utc}; +use near_primitives::types::{AccountId, ShardId, StateRoot}; + +use near_chain::chain::{ApplyStatePartsRequest, StateSplitRequest}; +use near_client_primitives::types::{ + DownloadStatus, ShardSyncDownload, ShardSyncStatus, StateSplitApplyingStatus, +}; +use near_network::types::AccountOrPeerIdOrHash; +use near_network::types::PeerManagerMessageRequest; +use near_o11y::WithSpanContextExt; +use near_primitives::shard_layout::ShardUId; + +/// Maximum number of state parts to request per peer on each round when node is trying to download the state. +pub const MAX_STATE_PART_REQUEST: u64 = 16; +/// Number of state parts already requested stored as pending. +/// This number should not exceed MAX_STATE_PART_REQUEST times (number of peers in the network). +pub const MAX_PENDING_PART: u64 = MAX_STATE_PART_REQUEST * 10000; + +pub enum StateSyncResult { + /// No shard has changed its status + Unchanged, + /// At least one shard has changed its status + /// Boolean parameter specifies whether the client needs to start fetching the block + Changed(bool), + /// The state for all shards was downloaded. + Completed, +} + +struct PendingRequestStatus { + missing_parts: usize, + wait_until: DateTime, +} + +impl PendingRequestStatus { + fn new(timeout: Duration) -> Self { + Self { missing_parts: 1, wait_until: Clock::utc().add(timeout) } + } + fn expired(&self) -> bool { + Clock::utc() > self.wait_until + } +} + +/// Private to public API conversion. +fn make_account_or_peer_id_or_hash( + from: near_network::types::AccountOrPeerIdOrHash, +) -> near_client_primitives::types::AccountOrPeerIdOrHash { + type From = near_network::types::AccountOrPeerIdOrHash; + type To = near_client_primitives::types::AccountOrPeerIdOrHash; + match from { + From::AccountId(a) => To::AccountId(a), + From::PeerId(p) => To::PeerId(p), + From::Hash(h) => To::Hash(h), + } +} + +/// Helper to track state sync. +pub struct StateSync { + network_adapter: Arc, + + state_sync_time: HashMap>, + last_time_block_requested: Option>, + + last_part_id_requested: HashMap<(AccountOrPeerIdOrHash, ShardId), PendingRequestStatus>, + /// Map from which part we requested to whom. + requested_target: lru::LruCache<(u64, CryptoHash), AccountOrPeerIdOrHash>, + + timeout: Duration, + + /// Maps shard_id to result of applying downloaded state + state_parts_apply_results: HashMap>, + + /// Maps shard_id to result of splitting state for resharding + split_state_roots: HashMap, Error>>, +} + +impl StateSync { + pub fn new(network_adapter: Arc, timeout: TimeDuration) -> Self { + StateSync { + network_adapter, + state_sync_time: Default::default(), + last_time_block_requested: None, + last_part_id_requested: Default::default(), + requested_target: lru::LruCache::new(MAX_PENDING_PART as usize), + timeout: Duration::from_std(timeout).unwrap(), + state_parts_apply_results: HashMap::new(), + split_state_roots: HashMap::new(), + } + } + + pub fn sync_block_status( + &mut self, + prev_hash: &CryptoHash, + chain: &Chain, + now: DateTime, + ) -> Result<(bool, bool), near_chain::Error> { + let (request_block, have_block) = if !chain.block_exists(prev_hash)? { + match self.last_time_block_requested { + None => (true, false), + Some(last_time) => { + if now - last_time >= self.timeout { + error!(target: "sync", "State sync: block request for {} timed out in {} seconds", prev_hash, self.timeout.num_seconds()); + (true, false) + } else { + (false, false) + } + } + } + } else { + self.last_time_block_requested = None; + (false, true) + }; + if request_block { + self.last_time_block_requested = Some(now); + }; + Ok((request_block, have_block)) + } + + // In the tuple of bools returned by this function, the first one + // indicates whether something has changed in `new_shard_sync`, + // and therefore whether the client needs to update its + // `sync_status`. The second indicates whether state sync is + // finished, in which case the client will transition to block sync + pub fn sync_shards_status( + &mut self, + me: &Option, + sync_hash: CryptoHash, + new_shard_sync: &mut HashMap, + chain: &mut Chain, + runtime_adapter: &Arc, + highest_height_peers: &[HighestHeightPeerInfo], + tracking_shards: Vec, + now: DateTime, + state_parts_task_scheduler: &dyn Fn(ApplyStatePartsRequest), + state_split_scheduler: &dyn Fn(StateSplitRequest), + ) -> Result<(bool, bool), near_chain::Error> { + let mut all_done = true; + let mut update_sync_status = false; + let init_sync_download = ShardSyncDownload { + downloads: vec![ + DownloadStatus { + start_time: now, + prev_update_time: now, + run_me: Arc::new(AtomicBool::new(true)), + error: false, + done: false, + state_requests_count: 0, + last_target: None, + }; + 1 + ], + status: ShardSyncStatus::StateDownloadHeader, + }; + + let prev_hash = *chain.get_block_header(&sync_hash)?.prev_hash(); + let prev_epoch_id = chain.get_block_header(&prev_hash)?.epoch_id().clone(); + let epoch_id = chain.get_block_header(&sync_hash)?.epoch_id().clone(); + if chain.runtime_adapter.get_shard_layout(&prev_epoch_id)? + != chain.runtime_adapter.get_shard_layout(&epoch_id)? + { + error!("cannot sync to the first epoch after sharding upgrade"); + panic!("cannot sync to the first epoch after sharding upgrade. Please wait for the next epoch or find peers that are more up to date"); + } + let split_states = runtime_adapter.will_shard_layout_change_next_epoch(&prev_hash)?; + + for shard_id in tracking_shards { + let mut download_timeout = false; + let mut need_shard = false; + let shard_sync_download = new_shard_sync.entry(shard_id).or_insert_with(|| { + need_shard = true; + update_sync_status = true; + init_sync_download.clone() + }); + let old_status = shard_sync_download.status.clone(); + let mut this_done = false; + match &shard_sync_download.status { + ShardSyncStatus::StateDownloadHeader => { + if shard_sync_download.downloads[0].done { + 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); + *shard_sync_download = ShardSyncDownload { + downloads: vec![ + DownloadStatus { + start_time: now, + prev_update_time: now, + run_me: Arc::new(AtomicBool::new(true)), + error: false, + done: false, + state_requests_count: 0, + last_target: None, + }; + state_num_parts as usize + ], + status: ShardSyncStatus::StateDownloadParts, + }; + need_shard = true; + } else { + let prev = shard_sync_download.downloads[0].prev_update_time; + let error = shard_sync_download.downloads[0].error; + download_timeout = now - prev > self.timeout; + if download_timeout || error { + shard_sync_download.downloads[0].run_me.store(true, Ordering::SeqCst); + shard_sync_download.downloads[0].error = false; + shard_sync_download.downloads[0].prev_update_time = now; + } + if shard_sync_download.downloads[0].run_me.load(Ordering::SeqCst) { + need_shard = true; + } + } + } + ShardSyncStatus::StateDownloadParts => { + let mut parts_done = true; + for part_download in shard_sync_download.downloads.iter_mut() { + if !part_download.done { + parts_done = false; + let prev = part_download.prev_update_time; + let error = part_download.error; + let part_timeout = now - prev > self.timeout; + if part_timeout || error { + download_timeout |= part_timeout; + part_download.run_me.store(true, Ordering::SeqCst); + part_download.error = false; + part_download.prev_update_time = now; + } + if part_download.run_me.load(Ordering::SeqCst) { + need_shard = true; + } + } + } + if parts_done { + *shard_sync_download = ShardSyncDownload { + downloads: vec![], + status: ShardSyncStatus::StateDownloadScheduling, + }; + } + } + ShardSyncStatus::StateDownloadScheduling => { + 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); + match chain.schedule_apply_state_parts( + shard_id, + sync_hash, + state_num_parts, + state_parts_task_scheduler, + ) { + Ok(()) => { + *shard_sync_download = ShardSyncDownload { + downloads: vec![], + status: ShardSyncStatus::StateDownloadApplying, + } + } + Err(e) => { + // Cannot finalize the downloaded state. + // The reasonable behavior here is to start from the very beginning. + error!(target: "sync", "State sync finalizing error, shard = {}, hash = {}: {:?}", shard_id, sync_hash, e); + *shard_sync_download = init_sync_download.clone(); + chain.clear_downloaded_parts(shard_id, sync_hash, state_num_parts)?; + } + } + } + ShardSyncStatus::StateDownloadApplying => { + let result = self.state_parts_apply_results.remove(&shard_id); + if let Some(result) = result { + match chain.set_state_finalize(shard_id, sync_hash, result) { + Ok(()) => { + *shard_sync_download = ShardSyncDownload { + downloads: vec![], + status: ShardSyncStatus::StateDownloadComplete, + } + } + Err(e) => { + // Cannot finalize the downloaded state. + // The reasonable behavior here is to start from the very beginning. + error!(target: "sync", "State sync finalizing error, shard = {}, hash = {}: {:?}", shard_id, sync_hash, e); + *shard_sync_download = init_sync_download.clone(); + 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, + )?; + } + } + } + } + ShardSyncStatus::StateDownloadComplete => { + 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)?; + if split_states { + *shard_sync_download = ShardSyncDownload { + downloads: vec![], + status: ShardSyncStatus::StateSplitScheduling, + } + } else { + *shard_sync_download = ShardSyncDownload { + downloads: vec![], + status: ShardSyncStatus::StateSyncDone, + }; + this_done = true; + } + } + ShardSyncStatus::StateSplitScheduling => { + debug_assert!(split_states); + let status = Arc::new(StateSplitApplyingStatus::new()); + chain.build_state_for_split_shards_preprocessing( + &sync_hash, + shard_id, + state_split_scheduler, + status.clone(), + )?; + debug!(target: "sync", "State sync split scheduled: me {:?}, shard = {}, hash = {}", me, shard_id, sync_hash); + *shard_sync_download = ShardSyncDownload { + downloads: vec![], + status: ShardSyncStatus::StateSplitApplying(status), + }; + } + ShardSyncStatus::StateSplitApplying(_status) => { + debug_assert!(split_states); + let result = self.split_state_roots.remove(&shard_id); + if let Some(state_roots) = result { + chain + .build_state_for_split_shards_postprocessing(&sync_hash, state_roots)?; + *shard_sync_download = ShardSyncDownload { + downloads: vec![], + status: ShardSyncStatus::StateSyncDone, + }; + this_done = true; + } + } + ShardSyncStatus::StateSyncDone => { + this_done = true; + } + } + all_done &= this_done; + + if download_timeout { + warn!(target: "sync", "State sync didn't download the state for shard {} in {} seconds, sending StateRequest again", shard_id, self.timeout.num_seconds()); + info!(target: "sync", "State sync status: me {:?}, sync_hash {}, phase {}", + me, + sync_hash, + match shard_sync_download.status { + ShardSyncStatus::StateDownloadHeader => format!("{} requests sent {}, last target {:?}", + Purple.bold().paint(format!("HEADER")), + shard_sync_download.downloads[0].state_requests_count, + shard_sync_download.downloads[0].last_target), + ShardSyncStatus::StateDownloadParts => { let mut text = "".to_string(); + for (i, download) in shard_sync_download.downloads.iter().enumerate() { + text.push_str(&format!("[{}: {}, {}, {:?}] ", + Yellow.bold().paint(i.to_string()), + download.done, + download.state_requests_count, + download.last_target)); + } + format!("{} [{}: is_done, requests sent, last target] {}", + Purple.bold().paint("PARTS"), + Yellow.bold().paint("part_id"), + text) + } + _ => unreachable!("timeout cannot happen when all state is downloaded"), + }, + ); + } + + // Execute syncing for shard `shard_id` + if need_shard { + update_sync_status = true; + *shard_sync_download = self.request_shard( + me, + shard_id, + chain, + runtime_adapter, + sync_hash, + shard_sync_download.clone(), + highest_height_peers, + )?; + } + update_sync_status |= shard_sync_download.status != old_status; + } + + Ok((update_sync_status, all_done)) + } + + pub fn set_apply_result(&mut self, shard_id: ShardId, apply_result: Result<(), Error>) { + self.state_parts_apply_results.insert(shard_id, apply_result); + } + + pub fn set_split_result( + &mut self, + shard_id: ShardId, + result: Result, Error>, + ) { + self.split_state_roots.insert(shard_id, result); + } + + /// Find the hash of the first block on the same epoch (and chain) of block with hash `sync_hash`. + pub fn get_epoch_start_sync_hash( + chain: &Chain, + sync_hash: &CryptoHash, + ) -> Result { + let mut header = chain.get_block_header(sync_hash)?; + let mut epoch_id = header.epoch_id().clone(); + let mut hash = *header.hash(); + let mut prev_hash = *header.prev_hash(); + loop { + if prev_hash == CryptoHash::default() { + return Ok(hash); + } + header = chain.get_block_header(&prev_hash)?; + if &epoch_id != header.epoch_id() { + return Ok(hash); + } + epoch_id = header.epoch_id().clone(); + hash = *header.hash(); + prev_hash = *header.prev_hash(); + } + } + + fn sent_request_part( + &mut self, + target: AccountOrPeerIdOrHash, + part_id: u64, + shard_id: ShardId, + sync_hash: CryptoHash, + ) { + self.requested_target.put((part_id, sync_hash), target.clone()); + + let timeout = self.timeout; + self.last_part_id_requested + .entry((target, shard_id)) + .and_modify(|pending_request| { + pending_request.missing_parts += 1; + }) + .or_insert_with(|| PendingRequestStatus::new(timeout)); + } + + pub fn received_requested_part( + &mut self, + part_id: u64, + shard_id: ShardId, + sync_hash: CryptoHash, + ) { + let key = (part_id, sync_hash); + if let Some(target) = self.requested_target.get(&key) { + if self.last_part_id_requested.get_mut(&(target.clone(), shard_id)).map_or( + false, + |request| { + request.missing_parts = request.missing_parts.saturating_sub(1); + request.missing_parts == 0 + }, + ) { + self.last_part_id_requested.remove(&(target.clone(), shard_id)); + } + } + } + + /// Find possible targets to download state from. + /// Candidates are validators at current epoch and peers at highest height. + /// Only select candidates that we have no pending request currently ongoing. + fn possible_targets( + &mut self, + me: &Option, + shard_id: ShardId, + chain: &Chain, + runtime_adapter: &Arc, + sync_hash: CryptoHash, + highest_height_peers: &[HighestHeightPeerInfo], + ) -> Result, Error> { + // Remove candidates from pending list if request expired due to timeout + self.last_part_id_requested.retain(|_, request| !request.expired()); + + let prev_block_hash = *chain.get_block_header(&sync_hash)?.prev_hash(); + let epoch_hash = runtime_adapter.get_epoch_id_from_prev_block(&prev_block_hash)?; + + Ok(runtime_adapter + .get_epoch_block_producers_ordered(&epoch_hash, &sync_hash)? + .iter() + .filter_map(|(validator_stake, _slashed)| { + let account_id = validator_stake.account_id(); + if runtime_adapter.cares_about_shard( + Some(account_id), + &prev_block_hash, + shard_id, + false, + ) { + if me.as_ref().map(|me| me != account_id).unwrap_or(true) { + Some(AccountOrPeerIdOrHash::AccountId(account_id.clone())) + } else { + None + } + } else { + None + } + }) + .chain(highest_height_peers.iter().filter_map(|peer| { + if peer.tracked_shards.contains(&shard_id) { + Some(AccountOrPeerIdOrHash::PeerId(peer.peer_info.id.clone())) + } else { + None + } + })) + .filter(|candidate| { + !self.last_part_id_requested.contains_key(&(candidate.clone(), shard_id)) + }) + .collect::>()) + } + + /// Returns new ShardSyncDownload if successful, otherwise returns given shard_sync_download + pub fn request_shard( + &mut self, + me: &Option, + shard_id: ShardId, + chain: &Chain, + runtime_adapter: &Arc, + sync_hash: CryptoHash, + shard_sync_download: ShardSyncDownload, + highest_height_peers: &[HighestHeightPeerInfo], + ) -> Result { + let possible_targets = self.possible_targets( + me, + shard_id, + chain, + runtime_adapter, + sync_hash, + highest_height_peers, + )?; + + if possible_targets.is_empty() { + return Ok(shard_sync_download); + } + + // Downloading strategy starts here + let mut new_shard_sync_download = shard_sync_download.clone(); + + match shard_sync_download.status { + ShardSyncStatus::StateDownloadHeader => { + let target = possible_targets.choose(&mut thread_rng()).cloned().unwrap(); + assert!(new_shard_sync_download.downloads[0].run_me.load(Ordering::SeqCst)); + new_shard_sync_download.downloads[0].run_me.store(false, Ordering::SeqCst); + new_shard_sync_download.downloads[0].state_requests_count += 1; + new_shard_sync_download.downloads[0].last_target = + Some(make_account_or_peer_id_or_hash(target.clone())); + let run_me = new_shard_sync_download.downloads[0].run_me.clone(); + near_performance_metrics::actix::spawn( + std::any::type_name::(), + self.network_adapter + .send( + PeerManagerMessageRequest::NetworkRequests( + NetworkRequests::StateRequestHeader { shard_id, sync_hash, target }, + ) + .with_span_context(), + ) + .then(move |result| { + if let Ok(NetworkResponses::RouteNotFound) = + result.map(|f| f.as_network_response()) + { + // Send a StateRequestHeader on the next iteration + run_me.store(true, Ordering::SeqCst); + } + future::ready(()) + }), + ); + } + ShardSyncStatus::StateDownloadParts => { + let possible_targets_sampler = + SamplerLimited::new(possible_targets, MAX_STATE_PART_REQUEST); + + // Iterate over all parts that needs to be requested (i.e. download.run_me is true). + // Parts are ordered such that its index match its part_id. + // Finally, for every part that needs to be requested it is selected one peer (target) randomly + // to request the part from + for ((part_id, download), target) in new_shard_sync_download + .downloads + .iter_mut() + .enumerate() + .filter(|(_, download)| download.run_me.load(Ordering::SeqCst)) + .zip(possible_targets_sampler) + { + self.sent_request_part(target.clone(), part_id as u64, shard_id, sync_hash); + download.run_me.store(false, Ordering::SeqCst); + download.state_requests_count += 1; + download.last_target = Some(make_account_or_peer_id_or_hash(target.clone())); + let run_me = download.run_me.clone(); + + near_performance_metrics::actix::spawn( + std::any::type_name::(), + self.network_adapter + .send( + PeerManagerMessageRequest::NetworkRequests( + NetworkRequests::StateRequestPart { + shard_id, + sync_hash, + part_id: part_id as u64, + target: target.clone(), + }, + ) + .with_span_context(), + ) + .then(move |result| { + if let Ok(NetworkResponses::RouteNotFound) = + result.map(|f| f.as_network_response()) + { + // Send a StateRequestPart on the next iteration + run_me.store(true, Ordering::SeqCst); + } + future::ready(()) + }), + ); + } + } + _ => {} + } + + Ok(new_shard_sync_download) + } + + pub fn run( + &mut self, + me: &Option, + sync_hash: CryptoHash, + new_shard_sync: &mut HashMap, + chain: &mut Chain, + runtime_adapter: &Arc, + highest_height_peers: &[HighestHeightPeerInfo], + tracking_shards: Vec, + state_parts_task_scheduler: &dyn Fn(ApplyStatePartsRequest), + state_split_scheduler: &dyn Fn(StateSplitRequest), + ) -> Result { + let _span = tracing::debug_span!(target: "sync", "run", sync = "StateSync").entered(); + debug!(target: "sync", %sync_hash, ?tracking_shards, "syncing state"); + let prev_hash = *chain.get_block_header(&sync_hash)?.prev_hash(); + let now = Clock::utc(); + + let (request_block, have_block) = self.sync_block_status(&prev_hash, chain, now)?; + + if tracking_shards.is_empty() { + // This case is possible if a validator cares about the same shards in the new epoch as + // in the previous (or about a subset of them), return success right away + + return if !have_block { + Ok(StateSyncResult::Changed(request_block)) + } else { + Ok(StateSyncResult::Completed) + }; + } + + let (update_sync_status, all_done) = self.sync_shards_status( + me, + sync_hash, + new_shard_sync, + chain, + runtime_adapter, + highest_height_peers, + tracking_shards, + now, + state_parts_task_scheduler, + state_split_scheduler, + )?; + + if have_block && all_done { + self.state_sync_time.clear(); + return Ok(StateSyncResult::Completed); + } + + Ok(if update_sync_status || request_block { + StateSyncResult::Changed(request_block) + } else { + StateSyncResult::Unchanged + }) + } +} + +/// Create an abstract collection of elements to be shuffled. +/// Each element will appear in the shuffled output exactly `limit` times. +/// Use it as an iterator to access the shuffled collection. +/// +/// ```rust,ignore +/// let sampler = SamplerLimited::new(vec![1, 2, 3], 2); +/// +/// let res = sampler.collect::>(); +/// +/// assert!(res.len() == 6); +/// assert!(res.iter().filter(|v| v == 1).count() == 2); +/// assert!(res.iter().filter(|v| v == 2).count() == 2); +/// assert!(res.iter().filter(|v| v == 3).count() == 2); +/// ``` +/// +/// Out of the 90 possible values of `res` in the code above on of them is: +/// +/// ``` +/// vec![1, 2, 1, 3, 3, 2]; +/// ``` +struct SamplerLimited { + data: Vec, + limit: Vec, +} + +impl SamplerLimited { + fn new(data: Vec, limit: u64) -> Self { + if limit == 0 { + Self { data: vec![], limit: vec![] } + } else { + let len = data.len(); + Self { data, limit: vec![limit; len] } + } + } +} + +impl Iterator for SamplerLimited { + type Item = T; + + fn next(&mut self) -> Option { + if self.limit.is_empty() { + None + } else { + let len = self.limit.len(); + let ix = thread_rng().gen_range(0..len); + self.limit[ix] -= 1; + + if self.limit[ix] == 0 { + if ix + 1 != len { + self.limit[ix] = self.limit[len - 1]; + self.data.swap(ix, len - 1); + } + + self.limit.pop(); + self.data.pop() + } else { + Some(self.data[ix].clone()) + } + } + } +} diff --git a/chain/client/src/view_client.rs b/chain/client/src/view_client.rs index 0dcec6c8e91..bcf4e661088 100644 --- a/chain/client/src/view_client.rs +++ b/chain/client/src/view_client.rs @@ -489,7 +489,7 @@ impl ViewClientActor { &mut self, hashes: Vec, ) -> Result, near_chain::Error> { - self.chain.retrieve_headers(hashes, sync::MAX_BLOCK_HEADERS, None) + self.chain.retrieve_headers(hashes, sync::header::MAX_BLOCK_HEADERS, None) } fn check_signature_account_announce( diff --git a/tools/mock-node/src/lib.rs b/tools/mock-node/src/lib.rs index 71f47451d45..e8ffd60b320 100644 --- a/tools/mock-node/src/lib.rs +++ b/tools/mock-node/src/lib.rs @@ -5,7 +5,7 @@ use actix::{Actor, Context, Handler}; use anyhow::{anyhow, Context as AnyhowContext}; use near_chain::{Block, BlockHeader, Chain, ChainStoreAccess, Error}; use near_chain_configs::GenesisConfig; -use near_client::sync; +use near_client::sync::header::MAX_BLOCK_HEADERS; use near_network::time; use near_network::types::{ BlockInfo, ConnectedPeerInfo, FullPeerInfo, NetworkInfo, NetworkRequests, NetworkResponses, @@ -468,7 +468,7 @@ impl ChainHistoryAccess { &mut self, hashes: Vec, ) -> Result, Error> { - self.chain.retrieve_headers(hashes, sync::MAX_BLOCK_HEADERS, Some(self.target_height)) + self.chain.retrieve_headers(hashes, MAX_BLOCK_HEADERS, Some(self.target_height)) } fn retrieve_block_by_height(&mut self, block_height: BlockHeight) -> Result { From 94123798a2f3e5db139b69e26fba92913b902ae8 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Mon, 28 Nov 2022 15:36:56 +0100 Subject: [PATCH 042/188] Made type of ArcMutex::update more strict. (#8126) The semantics of `ArcMutex::update` was misleading in case the result type was `Result<_,_>`, as one could think that returning an error would cancel the update (which was not the case: any mutations done before returning the error were commited). With the new API, ArcMutex::update takes a state monad as an argument (a function which takes old state and returns a new state with an optional result) which makes it obvious that the returned state is actually applied. I've also added a separate ArcMutex::try_update method which allows to cancel the update. --- chain/network/src/accounts_data/mod.rs | 30 +++++++------ chain/network/src/concurrency/arc_mutex.rs | 30 ++++++++++--- chain/network/src/concurrency/tests.rs | 45 +++++++++++++++---- .../src/peer_manager/connection/mod.rs | 22 +++++---- 4 files changed, 90 insertions(+), 37 deletions(-) diff --git a/chain/network/src/accounts_data/mod.rs b/chain/network/src/accounts_data/mod.rs index af5c87738e2..184f6d90d83 100644 --- a/chain/network/src/accounts_data/mod.rs +++ b/chain/network/src/accounts_data/mod.rs @@ -97,17 +97,19 @@ impl Cache { /// The AccountData which is no longer important is dropped. /// Returns true iff the set of accounts actually changed. pub fn set_keys(&self, keys_by_id: Arc) -> bool { - self.0.update(|inner| { - // Skip further processing if the key set didn't change. - // NOTE: if T implements Eq, then Arc short circuits equality for x == x. - if keys_by_id == inner.keys_by_id { - return false; - } - inner.keys_by_id = keys_by_id; - inner.keys = inner.keys_by_id.values().flatten().cloned().collect(); - inner.data.retain(|k, _| inner.keys.contains(k)); - true - }) + self.0 + .try_update(|mut inner| { + // Skip further processing if the key set didn't change. + // NOTE: if T implements Eq, then Arc short circuits equality for x == x. + if keys_by_id == inner.keys_by_id { + return Err(()); + } + inner.keys_by_id = keys_by_id; + inner.keys = inner.keys_by_id.values().flatten().cloned().collect(); + inner.data.retain(|k, _| inner.keys.contains(k)); + Ok(((), inner)) + }) + .is_ok() } /// Selects new data and verifies the signatures. @@ -170,8 +172,10 @@ impl Cache { // Execute verification on the rayon threadpool. let (data, err) = this.verify(data).await; // Insert the successfully verified data, even if an error has been encountered. - let inserted = - self.0.update(|inner| data.into_iter().filter_map(|d| inner.try_insert(d)).collect()); + let inserted = self.0.update(|mut inner| { + let inserted = data.into_iter().filter_map(|d| inner.try_insert(d)).collect(); + (inserted, inner) + }); // Return the inserted data. (inserted, err) } diff --git a/chain/network/src/concurrency/arc_mutex.rs b/chain/network/src/concurrency/arc_mutex.rs index 362496d1138..54e96d59d25 100644 --- a/chain/network/src/concurrency/arc_mutex.rs +++ b/chain/network/src/concurrency/arc_mutex.rs @@ -12,16 +12,34 @@ impl ArcMutex { pub fn new(v: T) -> Self { Self { value: ArcSwap::new(Arc::new(v)), mutex: Mutex::new(()) } } - // non-blocking + + /// Loads the last value stored. Non-blocking. pub fn load(&self) -> Arc { self.value.load_full() } - // blocking - pub fn update(&self, f: impl FnOnce(&mut T) -> R) -> R { + + /// Atomic update of the value. Blocking. + /// Note that `T -> (R,T)` is a state monad. + /// State monad is a function which takes the old state and + /// returns the new state + additional result value. + pub fn update(&self, f: impl FnOnce(T) -> (R, T)) -> R { let _guard = self.mutex.lock().unwrap(); - let mut value = self.value.load().as_ref().clone(); - let res = f(&mut value); - self.value.store(Arc::new(value)); + let (res, val) = f(self.value.load().as_ref().clone()); + self.value.store(Arc::new(val)); res } + + /// Atomic update of the value. Value is not modified if an error is returned. Blocking. + /// Note that `T -> Result<(R,T),E>` is a state monad transformer applied to the exception + /// monad. + pub fn try_update(&self, f: impl FnOnce(T) -> Result<(R, T), E>) -> Result { + let _guard = self.mutex.lock().unwrap(); + match f(self.value.load().as_ref().clone()) { + Ok((res, val)) => { + self.value.store(Arc::new(val)); + Ok(res) + } + Err(e) => Err(e), + } + } } diff --git a/chain/network/src/concurrency/tests.rs b/chain/network/src/concurrency/tests.rs index f4ebb33241e..7dc0f5f0de6 100644 --- a/chain/network/src/concurrency/tests.rs +++ b/chain/network/src/concurrency/tests.rs @@ -67,20 +67,47 @@ fn demux_runtime_dropped_during_call() { } #[test] -fn test_arc_mutex() { +fn arc_mutex_update() { let v = 5; let v2 = 10; + let v3 = 15; let m = ArcMutex::new(v); // Loaded value should be the same as constructor argument. assert_eq!(v, *m.load()); - m.update(|x| { - // Initial content of x should be the same as before update. - assert_eq!(v, *x); - // Concurrent load() should be possible and should return the value from before the update. - assert_eq!(v, *m.load()); - *x = v2; - assert_eq!(v, *m.load()); - }); + + // The extra result should be passed forward. + assert_eq!( + 19, + m.update(|x| { + // Initial content of x should be the same as before update. + assert_eq!(v, x); + // Concurrent load() should be possible and should return the value from before the update. + assert_eq!(v, *m.load()); + (19, v2) + }) + ); // After update, load() should return the new value. assert_eq!(v2, *m.load()); + + // try_update returning an Error. + assert_eq!( + Err::<(), i32>(35), + m.try_update(|x| { + assert_eq!(v2, x); + assert_eq!(v2, *m.load()); + Err(35) + }) + ); + assert_eq!(v2, *m.load()); + + // try_update returning Ok. + assert_eq!( + Ok::(21), + m.try_update(|x| { + assert_eq!(v2, x); + assert_eq!(v2, *m.load()); + Ok((21, v3)) + }) + ); + assert_eq!(v3, *m.load()); } diff --git a/chain/network/src/peer_manager/connection/mod.rs b/chain/network/src/peer_manager/connection/mod.rs index 00f068ac157..0fa13d61766 100644 --- a/chain/network/src/peer_manager/connection/mod.rs +++ b/chain/network/src/peer_manager/connection/mod.rs @@ -208,8 +208,9 @@ impl fmt::Debug for OutboundHandshakePermit { impl Drop for OutboundHandshakePermit { fn drop(&mut self) { if let Some(pool) = self.1.upgrade() { - pool.update(|pool| { + pool.update(|mut pool| { pool.outbound_handshakes.remove(&self.0); + ((), pool) }); } } @@ -242,7 +243,7 @@ impl Pool { } pub fn insert_ready(&self, peer: Arc) -> Result<(), PoolError> { - self.0.update(move |pool| { + self.0.try_update(move |mut pool| { let id = &peer.peer_info.id; if id == &pool.me { return Err(PoolError::LoopConnection); @@ -270,12 +271,12 @@ impl Pool { } } pool.ready.insert(id.clone(), peer); - Ok(()) + Ok(((),pool)) }) } pub fn start_outbound(&self, peer_id: PeerId) -> Result { - self.0.update(move |pool| { + self.0.try_update(move |mut pool| { if peer_id == pool.me { return Err(PoolError::LoopConnection); } @@ -286,13 +287,14 @@ impl Pool { return Err(PoolError::AlreadyStartedConnecting); } pool.outbound_handshakes.insert(peer_id.clone()); - Ok(OutboundHandshakePermit(peer_id, Arc::downgrade(&self.0))) + Ok((OutboundHandshakePermit(peer_id, Arc::downgrade(&self.0)), pool)) }) } pub fn remove(&self, peer_id: &PeerId) { - self.0.update(|pool| { + self.0.update(|mut pool| { pool.ready.remove(peer_id); + ((), pool) }); } /// Update the edge in the pool (if it is newer). @@ -300,10 +302,12 @@ impl Pool { let pool = self.load(); let Some(other) = new_edge.other(&pool.me) else { return }; let Some(conn) = pool.ready.get(other) else { return }; - conn.edge.update(|e| { - if e.nonce() < new_edge.nonce() { - *e = new_edge.clone(); + // Returns an error if the current edge is not older than new_edge. + let _ = conn.edge.try_update(|e| { + if e.nonce() >= new_edge.nonce() { + return Err(()); } + Ok(((), new_edge.clone())) }); } From 915a700a0a2a01599099b53779dfcb8de5a3edb4 Mon Sep 17 00:00:00 2001 From: anthony-near <113615218+anthony-near@users.noreply.github.com> Date: Mon, 28 Nov 2022 10:29:03 -0600 Subject: [PATCH 043/188] Tx status query not waiting for refunds (#7928) * only include non-refund receipts in tx results for get_recursive_transaction_results * filtering out refund receipts in runtime get_recursive_transaction_results too * moved the receipts reference outside of for loop * unwrap() self.store.get_recipt instead of ok_or_else(Error...) * revert changes to runtime_user get_recursive_transaction_results * only process receipts that are not None * re-added refund receipt filtering in runtime_user test get_recursive_transaction_results fn * revert changes to runtime_user * recursing on refund receipts as well, but not adding them to results * if let receipt and continue if is_refund * adding test in rpc, not finished * cleaned up unit test * updated test to poll instead of sleep, better test condition * break from polling if geetting a response in tx_status in test * assert we get a non-empty response at the end * comments added to get_recursive_transaction_result fn --- chain/chain/src/chain.rs | 8 ++++ .../jsonrpc-tests/tests/rpc_transactions.rs | 46 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/chain/chain/src/chain.rs b/chain/chain/src/chain.rs index 8598a7f2938..dc5cb605b78 100644 --- a/chain/chain/src/chain.rs +++ b/chain/chain/src/chain.rs @@ -3368,6 +3368,7 @@ impl Chain { Ok(self.store.get_outcomes_by_id(id)?.into_iter().map(Into::into).collect()) } + /// Returns all tx results given a tx hash, excluding refund receipts fn get_recursive_transaction_results( &self, id: &CryptoHash, @@ -3376,6 +3377,13 @@ impl Chain { let receipt_ids = outcome.outcome.receipt_ids.clone(); let mut results = vec![outcome]; for receipt_id in &receipt_ids { + // don't include refund receipts to speed up tx status query + if let Some(receipt) = self.store.get_receipt(&receipt_id)? { + let is_refund = receipt.predecessor_id.is_system(); + if is_refund { + continue; + } + } results.extend(self.get_recursive_transaction_results(receipt_id)?); } Ok(results) diff --git a/chain/jsonrpc/jsonrpc-tests/tests/rpc_transactions.rs b/chain/jsonrpc/jsonrpc-tests/tests/rpc_transactions.rs index 20415f8cb9f..b6f65b7c39e 100644 --- a/chain/jsonrpc/jsonrpc-tests/tests/rpc_transactions.rs +++ b/chain/jsonrpc/jsonrpc-tests/tests/rpc_transactions.rs @@ -1,8 +1,10 @@ use std::sync::{Arc, Mutex}; +use std::{thread, time}; use actix::{Actor, System}; use borsh::BorshSerialize; use futures::{future, FutureExt, TryFutureExt}; +use serde_json::json; use near_actix_test_utils::run_actix; use near_crypto::{InMemorySigner, KeyType}; @@ -97,6 +99,50 @@ fn test_send_tx_commit() { }); } +/// Test get_recursive_transaction_results (called by get_final_transaction_result) +/// only returns non-refund receipts +#[test] +fn test_refunds_not_in_receipts() { + test_with_client!(test_utils::NodeType::Validator, client, async move { + let block_hash = client.block(BlockReference::latest()).await.unwrap().header.hash; + let signer = InMemorySigner::from_seed("test1".parse().unwrap(), KeyType::ED25519, "test1"); + let tx = SignedTransaction::send_money( + 1, + "test1".parse().unwrap(), + "test2".parse().unwrap(), + &signer, + 100, + block_hash, + ); + let bytes = tx.try_to_vec().unwrap(); + client.broadcast_tx_commit(to_base64(&bytes)).await.unwrap(); + let mut tx_status = json!(client.EXPERIMENTAL_tx_status(to_base64(&bytes)).await.unwrap()); + for _ in 1..10 { + // poll every 10 milliseconds for updated tx status + thread::sleep(time::Duration::from_millis(10)); + tx_status = json!(client.EXPERIMENTAL_tx_status(to_base64(&bytes)).await.unwrap()); + let receipts = tx_status.get("receipts"); + if receipts.is_some() { + if !receipts.unwrap().as_array().unwrap().is_empty() { + break; + } + } + } + for receipt in tx_status.get("receipts") { + if !receipt.as_array().unwrap().is_empty() { + let receipt_predecessor_id = receipt.get("predecessor_id"); + if receipt_predecessor_id.is_some() { + let is_refund = receipt["predecessor_id"].get("is_system").unwrap().as_bool(); + if is_refund.is_some() { + assert!(!is_refund.unwrap()); + } + } + } + } + assert!(tx_status.get("receipts").unwrap().as_array().is_some()); + }); +} + /// Test that expired transaction should be rejected #[test] fn test_expired_tx() { From 54290ad84baa4b92218839bba5efe2711a4e327c Mon Sep 17 00:00:00 2001 From: Marcelo Diop-Gonzalez Date: Mon, 28 Nov 2022 12:09:47 -0500 Subject: [PATCH 044/188] fix(mirror): initialize global logging (#8129) otherwise logs from other threads (the ones started by the near node) will not show up --- tools/mirror/src/cli.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/mirror/src/cli.rs b/tools/mirror/src/cli.rs index f7de18054ff..f6e24ae6c6b 100644 --- a/tools/mirror/src/cli.rs +++ b/tools/mirror/src/cli.rs @@ -70,6 +70,11 @@ impl RunCmd { let system = new_actix_system(runtime); system .block_on(async move { + let _subscriber_guard = near_o11y::default_subscriber( + near_o11y::EnvFilterBuilder::from_env().finish().unwrap(), + &near_o11y::Options::default(), + ) + .global(); actix::spawn(crate::run( self.source_home, self.target_home, From f9950fda4f849fb2017aee8a36426d90bfa8607d Mon Sep 17 00:00:00 2001 From: Saketh Are Date: Mon, 28 Nov 2022 14:09:53 -0500 Subject: [PATCH 045/188] Migrate boot node and blacklisting tests to network crate (#8109) These tests verify the behavior for connections formed after starting a network with boot nodes, as well as boot nodes with blacklisted peers. This PR migrates them from integration-tests into the chain/network crate. The goals for this migration are to: - utilize the internal APIs of the near-network crate - make the tests await the changes rather than poll - to decrease the public API surface of the near-network crate --- chain/network/src/peer/peer_actor.rs | 2 +- .../network/src/peer_manager/tests/routing.rs | 248 ++++++++++++++++++ .../src/tests/network/routing.rs | 64 ----- integration-tests/src/tests/network/runner.rs | 15 -- 4 files changed, 249 insertions(+), 80 deletions(-) diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index 4ecfcf301c0..7a99b7c8fb5 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -627,7 +627,7 @@ impl PeerActor { } })); - // Refresh connection nonces but only if we're outbound. For inbound connection, the other party should + // Refresh connection nonces but only if we're outbound. For inbound connection, the other party should // take care of nonce refresh. if act.peer_type == PeerType::Outbound { ctx.spawn(wrap_future({ diff --git a/chain/network/src/peer_manager/tests/routing.rs b/chain/network/src/peer_manager/tests/routing.rs index 5ca07fc1fa7..d89e355cafd 100644 --- a/chain/network/src/peer_manager/tests/routing.rs +++ b/chain/network/src/peer_manager/tests/routing.rs @@ -1,21 +1,27 @@ +use crate::blacklist; use crate::broadcast; +use crate::config::NetworkConfig; use crate::network_protocol::testonly as data; use crate::network_protocol::{Edge, Encoding, Ping, Pong, RoutedMessageBody, RoutingTableUpdate}; use crate::peer; +use crate::peer::peer_actor::{ClosingReason, ConnectionClosedEvent}; use crate::peer_manager; use crate::peer_manager::peer_manager_actor::Event as PME; use crate::peer_manager::testonly::start as start_pm; use crate::peer_manager::testonly::Event; +use crate::private_actix::RegisterPeerError; use crate::store; use crate::tcp; use crate::testonly::{make_rng, Rng}; use crate::time; +use crate::types::PeerInfo; use crate::types::PeerMessage; use near_o11y::testonly::init_test_logger; use near_store::db::TestDB; use pretty_assertions::assert_eq; use rand::Rng as _; use std::collections::HashSet; +use std::net::Ipv4Addr; use std::sync::Arc; // test routing in a two-node network before and after connecting the nodes @@ -588,6 +594,248 @@ async fn test_dropping_duplicate_messages() { wait_for_pong(&mut pm0_ev, Pong { nonce: 1, source: id2.clone() }).await; } +// Awaits until a ConnectionClosed event with the expected reason is seen in the event stream. +pub(crate) async fn wait_for_connection_closed( + events: &mut broadcast::Receiver, + want_reason: ClosingReason, +) { + events + .recv_until(|ev| match ev { + Event::PeerManager(PME::ConnectionClosed(ConnectionClosedEvent { + stream_id: _, + reason, + })) => { + if reason == want_reason { + Some(()) + } else { + None + } + } + _ => None, + }) + .await; +} + +// Constructs NetworkConfigs for num_nodes nodes, the first num_boot_nodes of which +// are configured as boot nodes for all nodes. +fn make_configs( + chain: &data::Chain, + rng: &mut Rng, + num_nodes: usize, + num_boot_nodes: usize, + enable_outbound: bool, +) -> Vec { + let mut cfgs: Vec<_> = (0..num_nodes).map(|_i| chain.make_config(rng)).collect(); + let boot_nodes: Vec<_> = cfgs[0..num_boot_nodes] + .iter() + .map(|c| PeerInfo { id: c.node_id(), addr: c.node_addr, account_id: None }) + .collect(); + for config in cfgs.iter_mut() { + config.outbound_disabled = !enable_outbound; + config.peer_store.boot_nodes = boot_nodes.clone(); + } + cfgs +} + +// test bootstrapping a two-node network with one boot node +#[tokio::test] +async fn from_boot_nodes() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + tracing::info!(target:"test", "start two nodes"); + let cfgs = make_configs(&chain, rng, 2, 1, true); + let pm0 = start_pm(clock.clock(), TestDB::new(), cfgs[0].clone(), chain.clone()).await; + let pm1 = start_pm(clock.clock(), TestDB::new(), cfgs[1].clone(), chain.clone()).await; + + let id0 = pm0.cfg.node_id(); + let id1 = pm1.cfg.node_id(); + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[(id1.clone(), vec![id1.clone()])]).await; + tracing::info!(target:"test", "wait for {id1} routing table"); + pm1.wait_for_routing_table(&[(id0.clone(), vec![id0.clone()])]).await; +} + +// test node 0 blacklisting node 1 +#[tokio::test] +async fn blacklist_01() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + tracing::info!(target:"test", "start two nodes with 0 blacklisting 1"); + let mut cfgs = make_configs(&chain, rng, 2, 2, true); + cfgs[0].peer_store.blacklist = + [blacklist::Entry::from_addr(cfgs[1].node_addr.unwrap())].into_iter().collect(); + + let pm0 = start_pm(clock.clock(), TestDB::new(), cfgs[0].clone(), chain.clone()).await; + let pm1 = start_pm(clock.clock(), TestDB::new(), cfgs[1].clone(), chain.clone()).await; + + let id0 = pm0.cfg.node_id(); + let id1 = pm1.cfg.node_id(); + + tracing::info!(target:"test", "wait for the connection to be attempted and rejected"); + wait_for_connection_closed( + &mut pm0.events.clone(), + ClosingReason::RejectedByPeerManager(RegisterPeerError::Blacklisted), + ) + .await; + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[]).await; + tracing::info!(target:"test", "wait for {id1} routing table"); + pm1.wait_for_routing_table(&[]).await; +} + +// test node 1 blacklisting node 0 +#[tokio::test] +async fn blacklist_10() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + tracing::info!(target:"test", "start two nodes with 1 blacklisting 0"); + let mut cfgs = make_configs(&chain, rng, 2, 2, true); + cfgs[1].peer_store.blacklist = + [blacklist::Entry::from_addr(cfgs[0].node_addr.unwrap())].into_iter().collect(); + + let pm0 = start_pm(clock.clock(), TestDB::new(), cfgs[0].clone(), chain.clone()).await; + let pm1 = start_pm(clock.clock(), TestDB::new(), cfgs[1].clone(), chain.clone()).await; + + let id0 = pm0.cfg.node_id(); + let id1 = pm1.cfg.node_id(); + + tracing::info!(target:"test", "wait for the connection to be attempted and rejected"); + wait_for_connection_closed( + &mut pm1.events.clone(), + ClosingReason::RejectedByPeerManager(RegisterPeerError::Blacklisted), + ) + .await; + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[]).await; + tracing::info!(target:"test", "wait for {id1} routing table"); + pm1.wait_for_routing_table(&[]).await; +} + +// test node 0 blacklisting all nodes +#[tokio::test] +async fn blacklist_all() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + tracing::info!(target:"test", "start two nodes with 0 blacklisting everything"); + let mut cfgs = make_configs(&chain, rng, 2, 2, true); + cfgs[0].peer_store.blacklist = + [blacklist::Entry::from_ip(Ipv4Addr::LOCALHOST.into())].into_iter().collect(); + + let pm0 = start_pm(clock.clock(), TestDB::new(), cfgs[0].clone(), chain.clone()).await; + let pm1 = start_pm(clock.clock(), TestDB::new(), cfgs[1].clone(), chain.clone()).await; + + let id0 = pm0.cfg.node_id(); + let id1 = pm1.cfg.node_id(); + + tracing::info!(target:"test", "wait for the connection to be attempted and rejected"); + wait_for_connection_closed( + &mut pm0.events.clone(), + ClosingReason::RejectedByPeerManager(RegisterPeerError::Blacklisted), + ) + .await; + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[]).await; + tracing::info!(target:"test", "wait for {id1} routing table"); + pm1.wait_for_routing_table(&[]).await; +} + +// Spawn 3 nodes with max peers configured to 2, then allow them to connect to each other in a triangle. +// Spawn a fourth node and see it fail to connect since the first three are at max capacity. +#[tokio::test] +async fn max_num_peers_limit() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + 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); + for config in cfgs.iter_mut() { + config.max_num_peers = 2; + config.ideal_connections_lo = 2; + config.ideal_connections_hi = 2; + } + + let pm0 = start_pm(clock.clock(), TestDB::new(), cfgs[0].clone(), chain.clone()).await; + let pm1 = start_pm(clock.clock(), TestDB::new(), cfgs[1].clone(), chain.clone()).await; + let pm2 = start_pm(clock.clock(), TestDB::new(), cfgs[2].clone(), chain.clone()).await; + + let id0 = pm0.cfg.node_id(); + let id1 = pm1.cfg.node_id(); + let id2 = pm2.cfg.node_id(); + + tracing::info!(target:"test", "wait for {id0} routing table"); + pm0.wait_for_routing_table(&[ + (id1.clone(), vec![id1.clone()]), + (id2.clone(), vec![id2.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id1} routing table"); + pm1.wait_for_routing_table(&[ + (id0.clone(), vec![id0.clone()]), + (id2.clone(), vec![id2.clone()]), + ]) + .await; + tracing::info!(target:"test", "wait for {id2} routing table"); + pm2.wait_for_routing_table(&[ + (id0.clone(), vec![id0.clone()]), + (id1.clone(), vec![id1.clone()]), + ]) + .await; + + let mut pm0_ev = pm0.events.from_now(); + let mut pm1_ev = pm1.events.from_now(); + let mut pm2_ev = pm2.events.from_now(); + + tracing::info!(target:"test", "start a fourth node"); + let pm3 = start_pm(clock.clock(), TestDB::new(), cfgs[3].clone(), chain.clone()).await; + + let id3 = pm3.cfg.node_id(); + + tracing::info!(target:"test", "wait for {id0} to reject attempted connection"); + wait_for_connection_closed( + &mut pm0_ev, + ClosingReason::RejectedByPeerManager(RegisterPeerError::ConnectionLimitExceeded), + ) + .await; + tracing::info!(target:"test", "wait for {id1} to reject attempted connection"); + wait_for_connection_closed( + &mut pm1_ev, + ClosingReason::RejectedByPeerManager(RegisterPeerError::ConnectionLimitExceeded), + ) + .await; + tracing::info!(target:"test", "wait for {id2} to reject attempted connection"); + wait_for_connection_closed( + &mut pm2_ev, + ClosingReason::RejectedByPeerManager(RegisterPeerError::ConnectionLimitExceeded), + ) + .await; + + tracing::info!(target:"test", "wait for {id3} routing table"); + pm3.wait_for_routing_table(&[]).await; +} + // test that TTL is handled property. #[tokio::test] async fn ttl() { diff --git a/integration-tests/src/tests/network/routing.rs b/integration-tests/src/tests/network/routing.rs index 5fe590adc1a..094d654bbe1 100644 --- a/integration-tests/src/tests/network/routing.rs +++ b/integration-tests/src/tests/network/routing.rs @@ -1,16 +1,6 @@ use crate::tests::network::runner::*; use near_network::time; -#[test] -fn from_boot_nodes() -> anyhow::Result<()> { - let mut runner = Runner::new(2, 1).use_boot_nodes(vec![0]).enable_outbound(); - - runner.push(Action::CheckRoutingTable(0, vec![(1, vec![1])])); - runner.push(Action::CheckRoutingTable(1, vec![(0, vec![0])])); - - start_test(runner) -} - #[test] fn account_propagation() -> anyhow::Result<()> { let mut runner = Runner::new(3, 2); @@ -23,60 +13,6 @@ fn account_propagation() -> anyhow::Result<()> { start_test(runner) } -#[test] -fn blacklist_01() -> anyhow::Result<()> { - let mut runner = Runner::new(2, 2).add_to_blacklist(0, Some(1)).use_boot_nodes(vec![0]); - - runner.push(Action::Wait(time::Duration::milliseconds(100))); - runner.push(Action::CheckRoutingTable(1, vec![])); - runner.push(Action::CheckRoutingTable(0, vec![])); - - start_test(runner) -} - -#[test] -fn blacklist_10() -> anyhow::Result<()> { - let mut runner = Runner::new(2, 2).add_to_blacklist(1, Some(0)).use_boot_nodes(vec![0]); - - runner.push(Action::Wait(time::Duration::milliseconds(100))); - runner.push(Action::CheckRoutingTable(1, vec![])); - runner.push(Action::CheckRoutingTable(0, vec![])); - - start_test(runner) -} - -#[test] -fn blacklist_all() -> anyhow::Result<()> { - let mut runner = Runner::new(2, 2).add_to_blacklist(0, None).use_boot_nodes(vec![0]); - - runner.push(Action::Wait(time::Duration::milliseconds(100))); - runner.push(Action::CheckRoutingTable(1, vec![])); - runner.push(Action::CheckRoutingTable(0, vec![])); - - start_test(runner) -} - -/// Spawn 4 nodes with max peers required equal 2. Connect first three peers in a triangle. -/// Try to connect peer3 to peer0 and see it fail since first three peer are at max capacity. -#[test] -fn max_num_peers_limit() -> anyhow::Result<()> { - let mut runner = Runner::new(4, 4).max_num_peers(2).ideal_connections(2, 2).enable_outbound(); - - runner.push(Action::AddEdge { from: 0, to: 1, force: true }); - runner.push(Action::AddEdge { from: 1, to: 2, force: true }); - runner.push(Action::CheckRoutingTable(0, vec![(1, vec![1]), (2, vec![2])])); - runner.push(Action::CheckRoutingTable(1, vec![(0, vec![0]), (2, vec![2])])); - runner.push(Action::CheckRoutingTable(2, vec![(1, vec![1]), (0, vec![0])])); - runner.push(Action::AddEdge { from: 3, to: 0, force: false }); - runner.push(Action::Wait(time::Duration::milliseconds(100))); - runner.push(Action::CheckRoutingTable(0, vec![(1, vec![1]), (2, vec![2])])); - runner.push(Action::CheckRoutingTable(1, vec![(0, vec![0]), (2, vec![2])])); - runner.push(Action::CheckRoutingTable(2, vec![(1, vec![1]), (0, vec![0])])); - runner.push(Action::CheckRoutingTable(3, vec![])); - - start_test(runner) -} - /// Check that two archival nodes keep connected after network rebalance. Nodes 0 and 1 are archival nodes, others aren't. /// Initially connect 2, 3, 4 to 0. Then connect 1 to 0, this connection should persist, even after other nodes tries /// to connect to node 0 again. diff --git a/integration-tests/src/tests/network/runner.rs b/integration-tests/src/tests/network/runner.rs index 0fbf13ab8f6..72d16c3a2a4 100644 --- a/integration-tests/src/tests/network/runner.rs +++ b/integration-tests/src/tests/network/runner.rs @@ -343,21 +343,6 @@ impl Runner { } } - /// Add node `v` to the blacklist of node `u`. - /// If passed `Some(v)` it is created a blacklist entry like: - /// - /// 127.0.0.1:PORT_OF_NODE_V - /// - /// Use None (instead of Some(v)) if you want to add all other nodes to the blacklist. - /// If passed None it is created a blacklist entry like: - /// - /// 127.0.0.1 - /// - pub fn add_to_blacklist(mut self, u: usize, v: Option) -> Self { - self.test_config[u].blacklist.insert(v); - self - } - /// Add node `v` to the whitelist of node `u`. /// If passed `v` an entry of the following form is added to the whitelist: /// PEER_ID_OF_NODE_V@127.0.0.1:PORT_OF_NODE_V From 24e25d3379ef2547e120cfcb10f825536353b286 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Tue, 29 Nov 2022 12:40:33 +0100 Subject: [PATCH 046/188] fuzzing: simplify Function::arbitrary (#8097) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some small refactoring to Function::arbitrary but most notably simplify ExtSha256 and WriteKeyValue cases. For ExtSha256, all that we want is just bunch of bytes to be hashed. There’s no reason to think about them in terms of haw many 64-bit integers they are or whether they are little or big endian. With that observation, also change the case so that the argument is arbitrary length (from 0 up to 100 bytes) rather than hard-coding it to 160 bytes. For WriteKeyValue, simplify how arguments are constructed by using u64::to_le_bytes and <[T]>::concat. --- Cargo.lock | 1 - test-utils/runtime-tester/Cargo.toml | 1 - test-utils/runtime-tester/src/fuzzing.rs | 73 +++++++++++------------- 3 files changed, 33 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e870de2cdb..f9d7a27f5de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4959,7 +4959,6 @@ dependencies = [ name = "runtime-tester" version = "0.0.0" dependencies = [ - "byteorder", "cpu-time", "libfuzzer-sys", "near-chain", diff --git a/test-utils/runtime-tester/Cargo.toml b/test-utils/runtime-tester/Cargo.toml index 1cfa0f81091..adfc2df9162 100644 --- a/test-utils/runtime-tester/Cargo.toml +++ b/test-utils/runtime-tester/Cargo.toml @@ -6,7 +6,6 @@ publish = false edition.workspace = true [dependencies] -byteorder.workspace = true cpu-time.workspace = true libfuzzer-sys.workspace = true serde.workspace = true diff --git a/test-utils/runtime-tester/src/fuzzing.rs b/test-utils/runtime-tester/src/fuzzing.rs index b321fbc8350..02b4a723fcb 100644 --- a/test-utils/runtime-tester/src/fuzzing.rs +++ b/test-utils/runtime-tester/src/fuzzing.rs @@ -10,11 +10,9 @@ use near_primitives::{ }; use nearcore::config::{NEAR_BASE, TESTING_INIT_BALANCE}; -use byteorder::{ByteOrder, LittleEndian}; use libfuzzer_sys::arbitrary::{Arbitrary, Result, Unstructured}; use std::collections::{HashMap, HashSet}; -use std::mem::size_of; use std::str::FromStr; pub type ContractId = usize; @@ -674,88 +672,83 @@ impl Account { impl Function { pub fn arbitrary(&self, u: &mut Unstructured) -> Result { - let mut res = - FunctionCallAction { method_name: String::new(), args: vec![], gas: GAS_1, deposit: 0 }; + let method_name; + let mut args = Vec::new(); match self { // ################# // # Test contract # // ################# Function::StorageUsage => { - res.method_name = "ext_storage_usage".to_string(); + method_name = "ext_storage_usage"; } Function::BlockIndex => { - res.method_name = "ext_block_index".to_string(); + method_name = "ext_block_index"; } Function::BlockTimestamp => { - res.method_name = "ext_block_timestamp".to_string(); + method_name = "ext_block_timestamp"; } Function::PrepaidGas => { - res.method_name = "ext_prepaid_gas".to_string(); + method_name = "ext_prepaid_gas"; } Function::RandomSeed => { - res.method_name = "ext_random_seed".to_string(); + method_name = "ext_random_seed"; } Function::PredecessorAccountId => { - res.method_name = "ext_predecessor_account_id".to_string(); + method_name = "ext_predecessor_account_id"; } Function::SignerAccountPk => { - res.method_name = "ext_signer_account_pk".to_string(); + method_name = "ext_signer_account_pk"; } Function::SignerAccountId => { - res.method_name = "ext_signer_account_id".to_string(); + method_name = "ext_signer_account_id"; } Function::CurrentAccountId => { - res.method_name = "ext_current_account_id".to_string(); + method_name = "ext_current_account_id"; } Function::AccountBalance => { - res.method_name = "ext_account_balance".to_string(); + method_name = "ext_account_balance"; } Function::AttachedDeposit => { - res.method_name = "ext_attached_deposit".to_string(); + method_name = "ext_attached_deposit"; } Function::ValidatorTotalStake => { - res.method_name = "ext_validators_total_stake".to_string(); + method_name = "ext_validators_total_stake"; } Function::ExtSha256 => { - const VALUES_LEN: usize = 20; - let mut args = [0u8; VALUES_LEN * size_of::()]; - let mut values = vec![]; - for _ in 0..VALUES_LEN { - values.push(u.arbitrary::()?); - } - LittleEndian::write_u64_into(&values, &mut args); - res.method_name = "ext_sha256".to_string(); - res.args = args.to_vec(); + let len = u.int_in_range(0..=100)?; + method_name = "ext_sha256"; + args = u.bytes(len)?.to_vec(); } Function::UsedGas => { - res.method_name = "ext_used_gas".to_string(); + method_name = "ext_used_gas"; } Function::WriteKeyValue => { - let key = u.int_in_range::(0..=1_000)?; - let value = u.int_in_range::(0..=1_000)?; - let mut args = [0u8; 2 * size_of::()]; - LittleEndian::write_u64_into(&[key, value], &mut args); - res.method_name = "write_key_value".to_string(); - res.args = args.to_vec(); + let key = u.int_in_range::(0..=1_000)?.to_le_bytes(); + let value = u.int_in_range::(0..=1_000)?.to_le_bytes(); + method_name = "write_key_value"; + args = [&key[..], &value[..]].concat(); } Function::WriteBlockHeight => { - res.method_name = "write_block_height".to_string(); + method_name = "write_block_height"; } // ######################## // # Contract for fuzzing # // ######################## Function::SumOfNumbers => { - let args = u.int_in_range::(1..=10)?.to_le_bytes(); - res.method_name = "sum_of_numbers".to_string(); - res.args = args.to_vec(); + method_name = "sum_of_numbers"; + args = u.int_in_range::(1..=10)?.to_le_bytes().to_vec(); } Function::DataReceipt => { - let args = (*u.choose(&[10, 100, 1000, 10000, 100000])? as u64).to_le_bytes(); - res.method_name = "data_receipt_with_size".to_string(); - res.args = args.to_vec(); + method_name = "data_receipt_with_size"; + args = u.choose(&[10u64, 100, 1000, 10000, 100000])?.to_le_bytes().to_vec(); } }; - Ok(res) + Ok(FunctionCallAction { + method_name: method_name.to_string(), + args: args, + gas: GAS_1, + deposit: 0, + }) } fn size_hint(_depth: usize) -> (usize, Option) { From 08fa5dedf79615a39600e018e991a836f07d0f02 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Tue, 29 Nov 2022 14:48:53 +0100 Subject: [PATCH 047/188] Added AccountKey to Handshake message. (#8106) * Added a proof of AccountKey -> PeerId mapping to the Handshake message. It is needed to convince the inbound end of connection that the connecting peer actually belongs to TIER1 network. * Separated TIER2 Handshake from TIER1 Handshake message, to make it impossible for the peers to confuse which network the given connection belongs to. In particular the binary from the previous release won't be able to accept TIER1 connections. --- .../src/network_protocol/borsh_conv.rs | 1 + chain/network/src/network_protocol/mod.rs | 50 +++++++++ .../src/network_protocol/network.proto | 23 ++++ .../proto_conv/account_key.rs | 90 ++++++++++++++- .../network_protocol/proto_conv/handshake.rs | 5 + .../src/network_protocol/proto_conv/util.rs | 4 + .../network/src/network_protocol/testonly.rs | 1 + chain/network/src/peer/peer_actor.rs | 57 ++++++++-- chain/network/src/peer/tests/communication.rs | 1 + .../src/peer_manager/connection/mod.rs | 78 +++++++++++-- .../src/peer_manager/network_state/mod.rs | 2 +- .../src/peer_manager/peer_manager_actor.rs | 4 +- .../src/peer_manager/tests/connection_pool.rs | 104 +++++++++++++++++- chain/network/src/peer_manager/tests/nonce.rs | 1 + .../network/src/peer_manager/tests/routing.rs | 4 + chain/network/src/private_actix.rs | 2 +- chain/network/src/raw/connection.rs | 1 + chain/network/src/stats/metrics.rs | 8 ++ 18 files changed, 409 insertions(+), 27 deletions(-) diff --git a/chain/network/src/network_protocol/borsh_conv.rs b/chain/network/src/network_protocol/borsh_conv.rs index 23f4ad441af..9acd32937f0 100644 --- a/chain/network/src/network_protocol/borsh_conv.rs +++ b/chain/network/src/network_protocol/borsh_conv.rs @@ -14,6 +14,7 @@ impl From<&net::Handshake> for mem::Handshake { sender_listen_port: x.sender_listen_port, sender_chain_info: x.sender_chain_info.clone(), partial_edge_info: x.partial_edge_info.clone(), + owned_account: None, } } } diff --git a/chain/network/src/network_protocol/mod.rs b/chain/network/src/network_protocol/mod.rs index 7959f51588e..26ba0aff8a3 100644 --- a/chain/network/src/network_protocol/mod.rs +++ b/chain/network/src/network_protocol/mod.rs @@ -180,6 +180,54 @@ impl SignedAccountData { } } +/// Proof that a given peer owns the account key. +/// Included in every handshake sent by a validator node. +#[derive(Clone, PartialEq, Eq, Debug, Hash)] +pub struct OwnedAccount { + pub(crate) account_key: PublicKey, + pub(crate) peer_id: PeerId, + pub(crate) timestamp: time::Utc, +} + +impl OwnedAccount { + /// Serializes OwnedAccount to proto and signs it using `signer`. + /// Panics if OwnedAccount.account_key doesn't match signer.public_key(), + /// as this would likely be a bug. + pub fn sign(self, signer: &dyn ValidatorSigner) -> SignedOwnedAccount { + assert_eq!( + self.account_key, + signer.public_key(), + "OwnedAccount.account_key doesn't match the signer's account_key" + ); + let payload = proto::AccountKeyPayload::from(&self).write_to_bytes().unwrap(); + let signature = signer.sign_account_key_payload(&payload); + SignedOwnedAccount { + owned_account: self, + payload: AccountKeySignedPayload { payload, signature }, + } + } +} + +#[derive(Clone, PartialEq, Eq, Debug, Hash)] +pub struct SignedOwnedAccount { + owned_account: OwnedAccount, + // Serialized and signed OwnedAccount. + payload: AccountKeySignedPayload, +} + +impl std::ops::Deref for SignedOwnedAccount { + type Target = OwnedAccount; + fn deref(&self) -> &Self::Target { + &self.owned_account + } +} + +impl SignedOwnedAccount { + pub fn payload(&self) -> &AccountKeySignedPayload { + &self.payload + } +} + #[derive(PartialEq, Eq, Clone, Debug, Default)] pub struct RoutingTableUpdate { pub edges: Vec, @@ -216,6 +264,8 @@ pub struct Handshake { pub(crate) sender_chain_info: PeerChainInfoV2, /// Represents new `edge`. Contains only `none` and `Signature` from the sender. pub(crate) partial_edge_info: PartialEdgeInfo, + /// Account owned by the sender. + pub(crate) owned_account: Option, } #[derive(PartialEq, Eq, Clone, Debug, strum::IntoStaticStr)] diff --git a/chain/network/src/network_protocol/network.proto b/chain/network/src/network_protocol/network.proto index 840da94faf0..4997cd1865f 100644 --- a/chain/network/src/network_protocol/network.proto +++ b/chain/network/src/network_protocol/network.proto @@ -5,6 +5,22 @@ package network; import "google/protobuf/timestamp.proto"; +// Proof that a given peer owns the account key. +// Included in every handshake sent by a validator node. +// Note: sign AccountKeyPayload, rather than OwnedAccount directly. +message OwnedAccount { + PublicKey account_key = 1; // required + // PeerId of the node owning the account_key. + PublicKey peer_id = 2; // required + // Timestamp indicates the date of signing - we do not assume the + // nodes' clocks to be synchronized, but for security if the timestamp + // deviation is too large, the handshake will be rejected. + // TODO(gprusak): an alternative would be a 3-way handshake with a + // random challenge to sign, or even better: just use some standard + // asymetric entryption. + google.protobuf.Timestamp timestamp = 3; // required +} + // A payload that can be signed with account keys. // Since account keys are used to sign things in independent contexts, // we need this common enum to prevent message replay attacks, like this one: @@ -19,6 +35,7 @@ message AccountKeyPayload { reserved 1; oneof payload_type { AccountData account_data = 2; + OwnedAccount owned_account = 3; } } @@ -144,7 +161,13 @@ message Handshake { // connection has been established. // In case receiver accepts the Handshake, it sends back back a Handshake // containing his signature in this field. + // WARNING: this field contains a signature of (sender_peer_id,target_peer_id,nonce) tuple, + // which currently the only thing that we have as a substitute for a real authentication. + // TODO(gprusak): for TIER1 authentication is way more important than for TIER2, so this + // thing should be replaced with sth better. PartialEdgeInfo partial_edge_info = 7; + // See description of OwnedAccount. + AccountKeySignedPayload owned_account = 8; // optional } // Response to Handshake, in case the Handshake was rejected. diff --git a/chain/network/src/network_protocol/proto_conv/account_key.rs b/chain/network/src/network_protocol/proto_conv/account_key.rs index 77787795c98..2c2c38bb038 100644 --- a/chain/network/src/network_protocol/proto_conv/account_key.rs +++ b/chain/network/src/network_protocol/proto_conv/account_key.rs @@ -3,7 +3,9 @@ use super::*; use crate::network_protocol::proto; use crate::network_protocol::proto::account_key_payload::Payload_type as ProtoPT; -use crate::network_protocol::{AccountData, AccountKeySignedPayload, SignedAccountData}; +use crate::network_protocol::{ + AccountData, AccountKeySignedPayload, OwnedAccount, SignedAccountData, SignedOwnedAccount, +}; use protobuf::{Message as _, MessageField as MF}; #[derive(thiserror::Error, Debug)] @@ -20,9 +22,7 @@ pub enum ParseAccountDataError { Timestamp(ParseRequiredError), } -// TODO: currently a direct conversion Validator <-> proto::AccountKeyPayload is implemented. -// When more variants are available, consider whether to introduce an intermediate -// AccountKeyPayload enum. +// TODO: consider whether to introduce an intermediate AccountKeyPayload enum. impl From<&AccountData> for proto::AccountKeyPayload { fn from(x: &AccountData) -> Self { Self { @@ -44,7 +44,6 @@ impl TryFrom<&proto::AccountKeyPayload> for AccountData { fn try_from(x: &proto::AccountKeyPayload) -> Result { let x = match x.payload_type.as_ref().ok_or(Self::Error::BadPayloadType)? { ProtoPT::AccountData(a) => a, - #[allow(unreachable_patterns)] _ => return Err(Self::Error::BadPayloadType), }; Ok(Self { @@ -94,3 +93,84 @@ impl TryFrom<&proto::AccountKeySignedPayload> for SignedAccountData { }) } } + +////////////////////////////////////////// + +#[derive(thiserror::Error, Debug)] +pub enum ParseOwnedAccountError { + #[error("bad payload type")] + BadPayloadType, + #[error("peer_id: {0}")] + PeerId(ParseRequiredError), + #[error("account_key: {0}")] + AccountKey(ParseRequiredError), + #[error("timestamp: {0}")] + Timestamp(ParseRequiredError), +} + +impl From<&OwnedAccount> for proto::AccountKeyPayload { + fn from(x: &OwnedAccount) -> Self { + Self { + payload_type: Some(ProtoPT::OwnedAccount(proto::OwnedAccount { + account_key: MF::some((&x.account_key).into()), + peer_id: MF::some((&x.peer_id).into()), + timestamp: MF::some(utc_to_proto(&x.timestamp)), + ..Default::default() + })), + ..Self::default() + } + } +} + +impl TryFrom<&proto::AccountKeyPayload> for OwnedAccount { + type Error = ParseOwnedAccountError; + fn try_from(x: &proto::AccountKeyPayload) -> Result { + let x = match x.payload_type.as_ref().ok_or(Self::Error::BadPayloadType)? { + ProtoPT::OwnedAccount(a) => a, + _ => return Err(Self::Error::BadPayloadType), + }; + Ok(Self { + account_key: try_from_required(&x.account_key).map_err(Self::Error::AccountKey)?, + peer_id: try_from_required(&x.peer_id).map_err(Self::Error::PeerId)?, + timestamp: map_from_required(&x.timestamp, utc_from_proto) + .map_err(Self::Error::Timestamp)?, + }) + } +} + +////////////////////////////////////////// + +#[derive(thiserror::Error, Debug)] +pub enum ParseSignedOwnedAccountError { + #[error("decode: {0}")] + Decode(protobuf::Error), + #[error("owned_account: {0}")] + OwnedAccount(ParseOwnedAccountError), + #[error("signature: {0}")] + Signature(ParseRequiredError), +} + +impl From<&SignedOwnedAccount> for proto::AccountKeySignedPayload { + fn from(x: &SignedOwnedAccount) -> Self { + Self { + payload: (&x.payload.payload).clone(), + signature: MF::some((&x.payload.signature).into()), + ..Self::default() + } + } +} + +impl TryFrom<&proto::AccountKeySignedPayload> for SignedOwnedAccount { + type Error = ParseSignedOwnedAccountError; + fn try_from(x: &proto::AccountKeySignedPayload) -> Result { + let owned_account = + proto::AccountKeyPayload::parse_from_bytes(&x.payload).map_err(Self::Error::Decode)?; + Ok(Self { + owned_account: (&owned_account).try_into().map_err(Self::Error::OwnedAccount)?, + payload: AccountKeySignedPayload { + payload: x.payload.clone(), + signature: try_from_required(&x.signature).map_err(Self::Error::Signature)?, + }, + }) + } +} diff --git a/chain/network/src/network_protocol/proto_conv/handshake.rs b/chain/network/src/network_protocol/proto_conv/handshake.rs index 95b1bd83427..11003597f14 100644 --- a/chain/network/src/network_protocol/proto_conv/handshake.rs +++ b/chain/network/src/network_protocol/proto_conv/handshake.rs @@ -75,6 +75,8 @@ pub enum ParseHandshakeError { SenderChainInfo(ParseRequiredError), #[error("partial_edge_info {0}")] PartialEdgeInfo(ParseRequiredError), + #[error("owned_account {0}")] + OwnedAccount(ParseSignedOwnedAccountError), } impl From<&Handshake> for proto::Handshake { @@ -87,6 +89,7 @@ impl From<&Handshake> for proto::Handshake { sender_listen_port: x.sender_listen_port.unwrap_or(0).into(), sender_chain_info: MF::some((&x.sender_chain_info).into()), partial_edge_info: MF::some((&x.partial_edge_info).into()), + owned_account: x.owned_account.as_ref().map(Into::into).into(), ..Self::default() } } @@ -115,6 +118,8 @@ impl TryFrom<&proto::Handshake> for Handshake { .map_err(Self::Error::SenderChainInfo)?, partial_edge_info: try_from_required(&p.partial_edge_info) .map_err(Self::Error::PartialEdgeInfo)?, + owned_account: try_from_optional(&p.owned_account) + .map_err(Self::Error::OwnedAccount)?, }) } } diff --git a/chain/network/src/network_protocol/proto_conv/util.rs b/chain/network/src/network_protocol/proto_conv/util.rs index 515065a4882..186db2292d8 100644 --- a/chain/network/src/network_protocol/proto_conv/util.rs +++ b/chain/network/src/network_protocol/proto_conv/util.rs @@ -27,6 +27,10 @@ pub enum ParseRequiredError { Other(E), } +pub fn try_from_optional<'a, X, Y: TryFrom<&'a X>>(x: &'a MF) -> Result, Y::Error> { + x.as_ref().map(|x| x.try_into()).transpose() +} + pub fn try_from_required<'a, X, Y: TryFrom<&'a X>>( x: &'a MF, ) -> Result> { diff --git a/chain/network/src/network_protocol/testonly.rs b/chain/network/src/network_protocol/testonly.rs index 7f1518d75e2..63033b118af 100644 --- a/chain/network/src/network_protocol/testonly.rs +++ b/chain/network/src/network_protocol/testonly.rs @@ -340,6 +340,7 @@ pub fn make_handshake(rng: &mut R, chain: &Chain) -> Handshake { sender_listen_port: Some(rng.gen()), sender_chain_info: chain.get_peer_chain_info(), partial_edge_info: make_partial_edge(rng), + owned_account: None, } } diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index 7a99b7c8fb5..cfbaac4663c 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -3,8 +3,9 @@ use crate::concurrency::arc_mutex::ArcMutex; use crate::concurrency::atomic_cell::AtomicCell; use crate::concurrency::demux; use crate::network_protocol::{ - Edge, EdgeState, Encoding, ParsePeerMessageError, PartialEdgeInfo, PeerChainInfoV2, PeerInfo, - RawRoutedMessage, RoutedMessageBody, RoutingTableUpdate, SyncAccountsData, + Edge, EdgeState, Encoding, OwnedAccount, ParsePeerMessageError, PartialEdgeInfo, + PeerChainInfoV2, PeerIdOrHash, PeerInfo, RawRoutedMessage, RoutedMessageBody, + RoutingTableUpdate, SyncAccountsData, }; use crate::peer::stream; use crate::peer::tracker::Tracker; @@ -17,7 +18,7 @@ use crate::stats::metrics; use crate::tcp; use crate::time; use crate::types::{ - BlockInfo, Handshake, HandshakeFailureReason, PeerIdOrHash, PeerMessage, PeerType, ReasonForBan, + BlockInfo, Handshake, HandshakeFailureReason, PeerMessage, PeerType, ReasonForBan, }; use actix::fut::future::wrap_future; use actix::{Actor as _, ActorContext as _, ActorFutureExt as _, AsyncContext as _}; @@ -46,6 +47,9 @@ const MAX_PEER_MSG_PER_MIN: usize = usize::MAX; /// How often to request peers from active peers. const REQUEST_PEERS_INTERVAL: time::Duration = time::Duration::seconds(60); +/// Maximal allowed UTC clock skew between this node and the peer. +const MAX_CLOCK_SKEW: time::Duration = time::Duration::minutes(30); + /// Maximum number of transaction messages we will accept between block messages. /// The purpose of this constant is to ensure we do not spend too much time deserializing and /// dispatching transactions when we should be focusing on consensus-related messages. @@ -93,6 +97,10 @@ pub(crate) enum ClosingReason { PeerManager, #[error("Received DisconnectMessage from peer")] DisconnectMessage, + #[error("Peer clock skew exceeded {MAX_CLOCK_SKEW}")] + TooLargeClockSkew, + #[error("owned_account.peer_id doesn't match handshake.sender_peer_id")] + OwnedAccountMismatch, #[error("PeerActor stopped NOT via PeerActor::stop()")] Unknown, } @@ -334,6 +342,14 @@ impl PeerActor { archival: self.network_state.config.archive, }, partial_edge_info: spec.partial_edge_info, + owned_account: self.network_state.config.validator.as_ref().map(|vc| { + OwnedAccount { + account_key: vc.signer.public_key().clone(), + peer_id: self.network_state.config.node_id(), + timestamp: self.clock.now_utc(), + } + .sign(vc.signer.as_ref()) + }), }; let msg = PeerMessage::Tier2Handshake(handshake); self.send_message_or_log(&msg); @@ -453,6 +469,22 @@ impl PeerActor { } } + // Verify that handshake.owned_account is valid. + if let Some(owned_account) = &handshake.owned_account { + if let Err(_) = owned_account.payload().verify(&owned_account.account_key) { + self.stop(ctx, ClosingReason::Ban(ReasonForBan::InvalidSignature)); + return; + } + if owned_account.peer_id != handshake.sender_peer_id { + self.stop(ctx, ClosingReason::OwnedAccountMismatch); + return; + } + if (owned_account.timestamp - self.clock.now_utc()).abs() >= MAX_CLOCK_SKEW { + self.stop(ctx, ClosingReason::TooLargeClockSkew); + return; + } + } + // Verify that the received partial edge is valid. // WARNING: signature is verified against the 2nd argument. if !Edge::partial_verify( @@ -502,6 +534,7 @@ impl PeerActor { addr: ctx.address(), peer_info: peer_info.clone(), edge: ArcMutex::new(edge), + owned_account: handshake.owned_account.clone(), genesis_id: handshake.sender_chain_info.genesis_id.clone(), tracked_shards: handshake.sender_chain_info.tracked_shards.clone(), archival: handshake.sender_chain_info.archival, @@ -514,7 +547,7 @@ impl PeerActor { }), last_time_peer_requested: AtomicCell::new(None), last_time_received_message: AtomicCell::new(now), - connection_established_time: now, + established_time: now, send_accounts_data_demux: demux::Demux::new( self.network_state.config.accounts_data_broadcast_rate_limit, ), @@ -1180,11 +1213,15 @@ impl actix::Actor for PeerActor { // closing_reason may be None in case the whole actix system is stopped. // It happens a lot in tests. metrics::PEER_CONNECTIONS_TOTAL.dec(); - tracing::debug!(target: "network", "{:?}: [status = {:?}] Peer {} disconnected.", self.my_node_info.id, self.peer_status, self.peer_info); - if self.closing_reason.is_none() { - // Due to Actix semantics, sometimes closing reason may be not set. - // But it is only expected to happen in tests. - tracing::error!(target:"network", "closing reason not set. This should happen only in tests."); + match &self.closing_reason { + None => { + // Due to Actix semantics, sometimes closing reason may be not set. + // But it is only expected to happen in tests. + tracing::error!(target:"network", "closing reason not set. This should happen only in tests."); + } + Some(reason) => { + tracing::info!(target: "network", "{:?}: Peer {} disconnected, reason: {reason}", self.my_node_info.id, self.peer_info); + } } match &self.peer_status { // If PeerActor is in Connecting state, then @@ -1331,7 +1368,7 @@ impl actix::Handler for PeerActor { // case when our peer doesn't use that logic yet. if let Some(skip_tombstones) = self.network_state.config.skip_tombstones { if let PeerMessage::SyncRoutingTable(routing_table) = &mut peer_msg { - if conn.connection_established_time + skip_tombstones > self.clock.now() { + if conn.established_time + skip_tombstones > self.clock.now() { routing_table .edges .retain(|edge| edge.edge_type() == EdgeState::Active); diff --git a/chain/network/src/peer/tests/communication.rs b/chain/network/src/peer/tests/communication.rs index cd644cfda12..7a3963c6346 100644 --- a/chain/network/src/peer/tests/communication.rs +++ b/chain/network/src/peer/tests/communication.rs @@ -204,6 +204,7 @@ async fn test_handshake(outbound_encoding: Option, inbound_encoding: O sender_listen_port: Some(outbound_port), sender_chain_info: outbound_cfg.chain.get_peer_chain_info(), partial_edge_info: outbound_cfg.partial_edge_info(&inbound.cfg.id(), 1), + owned_account: None, }; // We will also introduce chain_id mismatch, but ProtocolVersionMismatch is expected to take priority. handshake.sender_chain_info.genesis_id.chain_id = "unknown_chain".to_string(); diff --git a/chain/network/src/peer_manager/connection/mod.rs b/chain/network/src/peer_manager/connection/mod.rs index 0fa13d61766..c200fcacdd5 100644 --- a/chain/network/src/peer_manager/connection/mod.rs +++ b/chain/network/src/peer_manager/connection/mod.rs @@ -1,7 +1,9 @@ use crate::concurrency::arc_mutex::ArcMutex; use crate::concurrency::atomic_cell::AtomicCell; use crate::concurrency::demux; -use crate::network_protocol::{Edge, PeerInfo, PeerMessage, SignedAccountData, SyncAccountsData}; +use crate::network_protocol::{ + Edge, PeerInfo, PeerMessage, SignedAccountData, SignedOwnedAccount, SyncAccountsData, +}; use crate::peer::peer_actor; use crate::peer::peer_actor::PeerActor; use crate::private_actix::SendMessage; @@ -9,6 +11,7 @@ use crate::stats::metrics; use crate::time; use crate::types::{BlockInfo, FullPeerInfo, PeerChainInfo, PeerType, ReasonForBan}; use arc_swap::ArcSwap; +use near_crypto::PublicKey; use near_o11y::WithSpanContextExt; use near_primitives::block::GenesisId; use near_primitives::network::PeerId; @@ -46,6 +49,8 @@ pub(crate) struct Connection { pub addr: actix::Addr, pub peer_info: PeerInfo, + /// AccountKey ownership proof. + pub owned_account: Option, pub edge: ArcMutex, /// Chain Id and hash of genesis block. pub genesis_id: GenesisId, @@ -58,7 +63,7 @@ pub(crate) struct Connection { /// Who started connection. Inbound (other) or Outbound (us). pub peer_type: PeerType, /// Time where the connection was established. - pub connection_established_time: time::Instant, + pub established_time: time::Instant, /// Last time requested peers. pub last_time_peer_requested: AtomicCell>, @@ -79,7 +84,7 @@ impl fmt::Debug for Connection { .field("peer_info", &self.peer_info) .field("edge", &self.edge.load()) .field("peer_type", &self.peer_type) - .field("connection_established_time", &self.connection_established_time) + .field("established_time", &self.established_time) .finish() } } @@ -158,6 +163,10 @@ pub(crate) struct PoolSnapshot { /// Connections which have completed the handshake and are ready /// for transmitting messages. pub ready: im::HashMap>, + /// Index on `ready` by Connection.owned_account.account_key. + /// We allow only 1 connection to a peer with the given account_key, + /// as it is an invalid setup to have 2 nodes acting as the same validator. + pub ready_by_account_key: im::HashMap>, /// Set of started outbound connections, which are not ready yet. /// We need to keep those to prevent a deadlock when 2 peers try /// to connect to each other at the same time. @@ -219,10 +228,12 @@ impl Drop for OutboundHandshakePermit { #[derive(Clone)] pub(crate) struct Pool(Arc>); -#[derive(thiserror::Error, Clone, Copy, Debug, PartialEq, Eq)] +#[derive(thiserror::Error, Clone, Debug, PartialEq, Eq)] pub(crate) enum PoolError { #[error("already connected to this peer")] AlreadyConnected, + #[error("already connected to peer {peer_id} with the same account key {account_key}")] + AlreadyConnectedAccount { peer_id: PeerId, account_key: PublicKey }, #[error("already started another outbound connection to this peer")] AlreadyStartedConnecting, #[error("loop connections are not allowed")] @@ -234,6 +245,7 @@ impl Pool { Self(Arc::new(ArcMutex::new(PoolSnapshot { me, ready: im::HashMap::new(), + ready_by_account_key: im::HashMap::new(), outbound_handshakes: im::HashSet::new(), }))) } @@ -270,7 +282,46 @@ impl Pool { } } } - pool.ready.insert(id.clone(), peer); + if pool.ready.insert(id.clone(), peer.clone()).is_some() { + return Err(PoolError::AlreadyConnected); + } + if let Some(owned_account) = &peer.owned_account { + // Only 1 connection per account key is allowed. + // Having 2 peers use the same account key is an invalid setup, + // which violates the BFT consensus anyway. + // TODO(gprusak): an incorrectly closed TCP connection may remain in ESTABLISHED + // state up to minutes/hours afterwards. This may cause problems in + // case a validator is restarting a node after crash and the new node has the same + // peer_id/account_key/IP:port as the old node. What is the desired behavior is + // such a case? Linux TCP implementation supports: + // TCP_USER_TIMEOUT - timeout for ACKing the sent data + // TCP_KEEPIDLE - idle connection time after which a KEEPALIVE is sent + // TCP_KEEPINTVL - interval between subsequent KEEPALIVE probes + // TCP_KEEPCNT - number of KEEPALIVE probes before closing the connection. + // If it ever becomes a problem, we can eiter: + // 1. replace TCP with sth else, like QUIC. + // 2. use some lower level API than tokio::net to be able to set the linux flags. + // 3. implement KEEPALIVE equivalent manually on top of TCP to resolve conflicts. + // 4. allow overriding old connections by new connections, but that will require + // a deeper thought to make sure that the connections will be eventually stable + // and that incorrect setups will be detectable. + if let Some(conn) = pool.ready_by_account_key.insert(owned_account.account_key.clone(), peer.clone()) { + // Unwrap is safe, because pool.ready_by_account_key is an index on connections + // with owned_account present. + let err = PoolError::AlreadyConnectedAccount{ + peer_id: conn.peer_info.id.clone(), + account_key: conn.owned_account.as_ref().unwrap().account_key.clone(), + }; + // We duplicate the error logging here, because returning an error + // from insert_ready is expected (pool may regularly reject connections), + // however conflicting connections with the same account key indicate an + // incorrect validator setup, so we log it here as a warn!, rather than just + // info!. + tracing::warn!(target:"network", "Pool::register({id}): {err}"); + metrics::ALREADY_CONNECTED_ACCOUNT.inc(); + return Err(err); + } + } Ok(((),pool)) }) } @@ -291,9 +342,22 @@ impl Pool { }) } - pub fn remove(&self, peer_id: &PeerId) { + pub fn remove(&self, conn: &Arc) { self.0.update(|mut pool| { - pool.ready.remove(peer_id); + match pool.ready.entry(conn.peer_info.id.clone()) { + im::hashmap::Entry::Occupied(e) if Arc::ptr_eq(e.get(), conn) => { + e.remove_entry(); + } + _ => {} + } + if let Some(owned_account) = &conn.owned_account { + match pool.ready_by_account_key.entry(owned_account.account_key.clone()) { + im::hashmap::Entry::Occupied(e) if Arc::ptr_eq(e.get(), conn) => { + e.remove_entry(); + } + _ => {} + } + } ((), pool) }); } diff --git a/chain/network/src/peer_manager/network_state/mod.rs b/chain/network/src/peer_manager/network_state/mod.rs index d68fb821ab8..03d5fcb5795 100644 --- a/chain/network/src/peer_manager/network_state/mod.rs +++ b/chain/network/src/peer_manager/network_state/mod.rs @@ -286,7 +286,7 @@ impl NetworkState { let conn = conn.clone(); self.spawn(async move { let peer_id = conn.peer_info.id.clone(); - this.tier2.remove(&peer_id); + this.tier2.remove(&conn); // If the last edge we have with this peer represent a connection addition, create the edge // update that represents the connection removal. diff --git a/chain/network/src/peer_manager/peer_manager_actor.rs b/chain/network/src/peer_manager/peer_manager_actor.rs index 6806658f6d3..c38123af59b 100644 --- a/chain/network/src/peer_manager/peer_manager_actor.rs +++ b/chain/network/src/peer_manager/peer_manager_actor.rs @@ -434,7 +434,7 @@ impl PeerManagerActor { .collect(); // Sort by established time. - active_peers.sort_by_key(|p| p.connection_established_time); + active_peers.sort_by_key(|p| p.established_time); // Saturate safe set with recently active peers. let set_limit = self.state.config.safe_set_size as usize; for p in active_peers { @@ -582,7 +582,7 @@ impl PeerManagerActor { .load() .unwrap_or(self.clock.now()), last_time_received_message: cp.last_time_received_message.load(), - connection_established_time: cp.connection_established_time, + connection_established_time: cp.established_time, peer_type: cp.peer_type, nonce: cp.edge.load().nonce(), }) diff --git a/chain/network/src/peer_manager/tests/connection_pool.rs b/chain/network/src/peer_manager/tests/connection_pool.rs index 9fb72343ffb..0ed2e8d1a79 100644 --- a/chain/network/src/peer_manager/tests/connection_pool.rs +++ b/chain/network/src/peer_manager/tests/connection_pool.rs @@ -1,6 +1,6 @@ use crate::network_protocol::testonly as data; use crate::network_protocol::PeerMessage; -use crate::network_protocol::{Encoding, Handshake, PartialEdgeInfo}; +use crate::network_protocol::{Encoding, Handshake, OwnedAccount, PartialEdgeInfo}; use crate::peer::peer_actor::ClosingReason; use crate::peer_manager; use crate::peer_manager::connection; @@ -99,6 +99,7 @@ async fn loop_connection() { 1, &pm.cfg.node_key, ), + owned_account: None, })) .await; let reason = events @@ -119,3 +120,104 @@ async fn loop_connection() { reason ); } + +#[tokio::test] +async fn owned_account_mismatch() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + let pm = peer_manager::testonly::start( + clock.clock(), + near_store::db::TestDB::new(), + chain.make_config(rng), + chain.clone(), + ) + .await; + + // An inbound connection pretending to be a loop should be rejected. + let stream = tcp::Stream::connect(&pm.peer_info()).await.unwrap(); + let stream_id = stream.id(); + let port = stream.local_addr.port(); + let mut events = pm.events.from_now(); + let mut stream = Stream::new(Some(Encoding::Proto), stream); + let cfg = chain.make_config(rng); + let vc = cfg.validator.clone().unwrap(); + stream + .write(&PeerMessage::Tier2Handshake(Handshake { + protocol_version: PROTOCOL_VERSION, + oldest_supported_version: PROTOCOL_VERSION, + sender_peer_id: cfg.node_id(), + target_peer_id: pm.cfg.node_id(), + sender_listen_port: Some(port), + sender_chain_info: chain.get_peer_chain_info(), + partial_edge_info: PartialEdgeInfo::new( + &cfg.node_id(), + &pm.cfg.node_id(), + 1, + &cfg.node_key, + ), + owned_account: Some( + OwnedAccount { + account_key: vc.signer.public_key().clone(), + // random peer_id, different than the expected cfg.node_id(). + peer_id: data::make_peer_id(rng), + timestamp: clock.now_utc(), + } + .sign(vc.signer.as_ref()), + ), + })) + .await; + let reason = events + .recv_until(|ev| match ev { + Event::PeerManager(PME::ConnectionClosed(ev)) if ev.stream_id == stream_id => { + Some(ev.reason) + } + Event::PeerManager(PME::HandshakeCompleted(ev)) if ev.stream_id == stream_id => { + panic!("PeerManager accepted the handshake") + } + _ => None, + }) + .await; + assert_eq!(ClosingReason::OwnedAccountMismatch, reason); +} + +#[tokio::test] +async fn owned_account_conflict() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + let pm = peer_manager::testonly::start( + clock.clock(), + near_store::db::TestDB::new(), + chain.make_config(rng), + chain.clone(), + ) + .await; + + let cfg1 = chain.make_config(rng); + let mut cfg2 = chain.make_config(rng); + cfg2.validator = cfg1.validator.clone(); + + // Start 2 connections with the same account_key. + // The second should be rejected. + let conn1 = pm.start_inbound(chain.clone(), cfg1.clone()).await.handshake(&clock.clock()).await; + let reason = + pm.start_inbound(chain.clone(), cfg2).await.manager_fail_handshake(&clock.clock()).await; + assert_eq!( + reason, + ClosingReason::RejectedByPeerManager(RegisterPeerError::PoolError( + connection::PoolError::AlreadyConnectedAccount { + peer_id: cfg1.node_id(), + account_key: cfg1.validator.as_ref().unwrap().signer.public_key(), + } + )) + ); + drop(conn1); + drop(pm); +} diff --git a/chain/network/src/peer_manager/tests/nonce.rs b/chain/network/src/peer_manager/tests/nonce.rs index e310e9dea70..fe094cb10e1 100644 --- a/chain/network/src/peer_manager/tests/nonce.rs +++ b/chain/network/src/peer_manager/tests/nonce.rs @@ -74,6 +74,7 @@ async fn test_nonces() { sender_listen_port: Some(24567), sender_chain_info: chain.get_peer_chain_info(), partial_edge_info: PartialEdgeInfo::new(&peer_id, &pm.cfg.node_id(), test.0, &peer_key), + owned_account: None, }); stream.write(&handshake).await; if test.1 { diff --git a/chain/network/src/peer_manager/tests/routing.rs b/chain/network/src/peer_manager/tests/routing.rs index d89e355cafd..08121f474e0 100644 --- a/chain/network/src/peer_manager/tests/routing.rs +++ b/chain/network/src/peer_manager/tests/routing.rs @@ -224,6 +224,10 @@ async fn join_components() { (id2.clone(), vec![id2.clone()]), ]) .await; + drop(pm0); + drop(pm1); + drop(pm2); + drop(pm3); } // test routing for three nodes in a line, then test dropping the middle node diff --git a/chain/network/src/private_actix.rs b/chain/network/src/private_actix.rs index 85185df385e..89eccca7478 100644 --- a/chain/network/src/private_actix.rs +++ b/chain/network/src/private_actix.rs @@ -5,7 +5,7 @@ use crate::peer_manager::connection; use std::fmt::Debug; use std::sync::Arc; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum RegisterPeerError { Blacklisted, Banned, diff --git a/chain/network/src/raw/connection.rs b/chain/network/src/raw/connection.rs index d943c81efa8..39fcd0271b2 100644 --- a/chain/network/src/raw/connection.rs +++ b/chain/network/src/raw/connection.rs @@ -149,6 +149,7 @@ impl Connection { 1, &self.secret_key, ), + owned_account: None, }); self.write_message(&handshake).await.map_err(ConnectError::IO)?; diff --git a/chain/network/src/stats/metrics.rs b/chain/network/src/stats/metrics.rs index e6c1244b3bf..edb6cabb4e6 100644 --- a/chain/network/src/stats/metrics.rs +++ b/chain/network/src/stats/metrics.rs @@ -340,6 +340,14 @@ pub(crate) static CONNECTED_TO_MYSELF: Lazy = Lazy::new(|| { .unwrap() }); +pub(crate) static ALREADY_CONNECTED_ACCOUNT: Lazy = Lazy::new(|| { + try_create_int_counter( + "near_already_connected_account", + "A second peer with the same validator key is trying to connect to our node. This means that the validator peer has invalid setup." + ) + .unwrap() +}); + pub(crate) fn record_routed_msg_metrics(clock: &time::Clock, msg: &RoutedMessageV2) { record_routed_msg_latency(clock, msg); record_routed_msg_hops(msg); From 76d3982804eb3d10cbda86405c1ee6de8863aaa0 Mon Sep 17 00:00:00 2001 From: Marcelo Diop-Gonzalez Date: Tue, 29 Nov 2022 10:08:12 -0500 Subject: [PATCH 048/188] feat(mirror): send extra AddKey txs for function calls that add keys (#8130) right now the only keys that get mapped are the ones that come from actual AddKey transactions. This misses any keys that get added from contract calls, which is actually pretty common. So to fix this, we will search through the txs and receipts of each block looking for function calls that generated AddKey transactions. When we see one, we'll just send an extra AddKey transaction to map the key the way we do for normal AddKey transactions. This change doesn't handle the case of CreateAccount actions generated by function calls, since the logic there is different, so we'll take care of that in another PR. --- Cargo.lock | 1 + pytest/tools/mirror/contract/Cargo.lock | 1521 +++++++++++++++++++++++ pytest/tools/mirror/contract/Cargo.toml | 25 + pytest/tools/mirror/contract/src/lib.rs | 27 + pytest/tools/mirror/mirror_utils.py | 134 +- pytest/tools/mirror/online_test.py | 1 - tools/mirror/Cargo.toml | 1 + tools/mirror/src/chain_tracker.rs | 25 +- tools/mirror/src/lib.rs | 665 ++++++++-- tools/mirror/src/offline.rs | 101 +- tools/mirror/src/online.rs | 115 +- 11 files changed, 2422 insertions(+), 194 deletions(-) create mode 100644 pytest/tools/mirror/contract/Cargo.lock create mode 100644 pytest/tools/mirror/contract/Cargo.toml create mode 100644 pytest/tools/mirror/contract/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index f9d7a27f5de..afbdddf616d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3246,6 +3246,7 @@ dependencies = [ "near-client", "near-client-primitives", "near-crypto", + "near-epoch-manager", "near-indexer", "near-indexer-primitives", "near-network", diff --git a/pytest/tools/mirror/contract/Cargo.lock b/pytest/tools/mirror/contract/Cargo.lock new file mode 100644 index 00000000000..eb1df2d3edf --- /dev/null +++ b/pytest/tools/mirror/contract/Cargo.lock @@ -0,0 +1,1521 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + +[[package]] +name = "addkey-contract" +version = "0.1.0" +dependencies = [ + "near-sdk", +] + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.8", + "once_cell", + "version_check", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "bitvec" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174" +dependencies = [ + "crypto-mac", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "borsh" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +dependencies = [ + "borsh-derive", + "hashbrown", +] + +[[package]] +name = "borsh-derive" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "byte-slice-cast" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytesize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70" + +[[package]] +name = "c2-chacha" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27dae93fe7b1e0424dc57179ac396908c26b035a87234809f5c4dfd1b47dc80" +dependencies = [ + "cipher", + "ppv-lite86", +] + +[[package]] +name = "cc" +version = "1.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "serde", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "cxx" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer 0.10.3", + "crypto-common", +] + +[[package]] +name = "dyn-clone" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2" + +[[package]] +name = "easy-ext" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53aff6fdc1b181225acdcb5b14c47106726fd8e486707315b1b138baed68ee31" + +[[package]] +name = "ed25519" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "fixed-hash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" +dependencies = [ + "byteorder", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "impl-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161ebdfec3c8e3b52bf61c4f3550a1eea4f9579d10dc1b936f3171ebdcd6c443" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itoa" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" + +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" + +[[package]] +name = "link-cplusplus" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +dependencies = [ + "cc", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + +[[package]] +name = "near-abi" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "885db39b08518fa700b73fa2214e8adbbfba316ba82dd510f50519173eadaf73" +dependencies = [ + "borsh", + "schemars", + "semver", + "serde", +] + +[[package]] +name = "near-account-id" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d258582a1878e6db67400b0504a5099db85718d22c2e07f747fe1706ae7150" +dependencies = [ + "borsh", + "serde", +] + +[[package]] +name = "near-crypto" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e75673d69fd7365508f3d32483669fe45b03bfb34e4d9363e90adae9dfb416c" +dependencies = [ + "arrayref", + "blake2", + "borsh", + "bs58", + "c2-chacha", + "curve25519-dalek", + "derive_more", + "ed25519-dalek", + "near-account-id", + "once_cell", + "parity-secp256k1", + "primitive-types", + "rand 0.7.3", + "rand_core 0.5.1", + "serde", + "serde_json", + "subtle", + "thiserror", +] + +[[package]] +name = "near-primitives" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ad1a9a1640539c81f065425c31bffcfbf6b31ef1aeaade59ce905f5df6ac860" +dependencies = [ + "borsh", + "byteorder", + "bytesize", + "chrono", + "derive_more", + "easy-ext", + "hex", + "near-crypto", + "near-primitives-core", + "near-rpc-error-macro", + "near-vm-errors", + "num-rational", + "once_cell", + "primitive-types", + "rand 0.7.3", + "reed-solomon-erasure", + "serde", + "serde_json", + "smart-default", + "strum", + "thiserror", +] + +[[package]] +name = "near-primitives-core" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d508f0fc340f6461e4e256417685720d3c4c00bb5a939b105160e49137caba" +dependencies = [ + "base64 0.11.0", + "borsh", + "bs58", + "derive_more", + "near-account-id", + "num-rational", + "serde", + "sha2 0.10.6", + "strum", +] + +[[package]] +name = "near-rpc-error-core" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ee0b41c75ef859c193a8ff1dadfa0c8207bc0ac447cc22259721ad769a1408" +dependencies = [ + "quote", + "serde", + "syn", +] + +[[package]] +name = "near-rpc-error-macro" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e837bd4bacd807073ec5ceb85708da7f721b46a4c2a978de86027fb0034ce31" +dependencies = [ + "near-rpc-error-core", + "serde", + "syn", +] + +[[package]] +name = "near-sdk" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15eb3de2defe3626260cc209a6cdb985c6b27b0bd4619fad97dcfae002c3c5bd" +dependencies = [ + "base64 0.13.1", + "borsh", + "bs58", + "near-abi", + "near-crypto", + "near-primitives", + "near-primitives-core", + "near-sdk-macros", + "near-sys", + "near-vm-logic", + "once_cell", + "schemars", + "serde", + "serde_json", + "wee_alloc", +] + +[[package]] +name = "near-sdk-macros" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4907affc9f5ed559456509188ff0024f1f2099c0830e6bdb66eb61d5b75912c0" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "near-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e307313276eaeced2ca95740b5639e1f3125b7c97f0a1151809d105f1aa8c6d3" + +[[package]] +name = "near-vm-errors" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0da466a30f0446639cbd788c30865086fac3e8dcb07a79e51d2b0775ed4261e" +dependencies = [ + "borsh", + "near-account-id", + "near-rpc-error-macro", + "serde", +] + +[[package]] +name = "near-vm-logic" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81b534828419bacbf1f7b11ef7b00420f248c548c485d3f0cfda8bb6931152f2" +dependencies = [ + "base64 0.13.1", + "borsh", + "bs58", + "byteorder", + "near-account-id", + "near-crypto", + "near-primitives", + "near-primitives-core", + "near-vm-errors", + "ripemd", + "serde", + "sha2 0.9.9", + "sha3", + "zeropool-bn", +] + +[[package]] +name = "num-bigint" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "parity-scale-codec" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909" +dependencies = [ + "arrayvec 0.7.2", + "bitvec", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1557010476e0595c9b568d16dcfb81b93cdeb157612726f5170d31aa707bed27" +dependencies = [ + "proc-macro-crate 1.2.1", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "parity-secp256k1" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fca4f82fccae37e8bbdaeb949a4a218a1bbc485d11598f193d2a908042e5fc1" +dependencies = [ + "arrayvec 0.5.2", + "cc", + "cfg-if 0.1.10", + "rand 0.7.3", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "primitive-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro-crate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +dependencies = [ + "once_cell", + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro2" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.8", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "reed-solomon-erasure" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a415a013dd7c5d4221382329a5a3482566da675737494935cbbbcdec04662f9d" +dependencies = [ + "smallvec", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.6", +] + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "schemars" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a5fb6c61f29e723026dc8e923d94c694313212abbecbbe5f55a7748eec5b307" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f188d036977451159430f3b8dc82ec76364a42b7e289c2b18a9a18f4470058e9" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[package]] +name = "scratch" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" + +[[package]] +name = "semver" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" + +[[package]] +name = "serde" +version = "1.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8b3801309262e8184d9687fb697586833e939767aea0dda89f5a8e650e8bd7" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.6", +] + +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "keccak", + "opaque-debug", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "smart-default" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "uint" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45526d29728d135c2900b0d30573fe3ee79fceb12ef534c7bb30e810a91b601" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + +[[package]] +name = "zeroize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeropool-bn" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e61de68ede9ffdd69c01664f65a178c5188b73f78faa21f0936016a888ff7c" +dependencies = [ + "borsh", + "byteorder", + "crunchy", + "lazy_static", + "rand 0.8.5", + "rustc-hex", +] diff --git a/pytest/tools/mirror/contract/Cargo.toml b/pytest/tools/mirror/contract/Cargo.toml new file mode 100644 index 00000000000..a67790e8f20 --- /dev/null +++ b/pytest/tools/mirror/contract/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "addkey-contract" +version = "0.1.0" +authors = ["Near Inc "] +edition = "2018" + +[workspace] +members = [] + + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +near-sdk = "4.1.1" + +[profile.release] +codegen-units = 1 +# Tell `rustc` to optimize for small code size. +opt-level = "z" +lto = true +debug = false +panic = "abort" +# Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 +overflow-checks = true diff --git a/pytest/tools/mirror/contract/src/lib.rs b/pytest/tools/mirror/contract/src/lib.rs new file mode 100644 index 00000000000..db6555687d4 --- /dev/null +++ b/pytest/tools/mirror/contract/src/lib.rs @@ -0,0 +1,27 @@ +//! Contract that adds keys + +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::{env, near_bindgen, Promise, PublicKey}; +use std::str::FromStr; + +#[near_bindgen] +#[derive(Default, BorshDeserialize, BorshSerialize)] +pub struct KeyAdder {} + +#[near_bindgen] +impl KeyAdder { + pub fn add_key(&mut self, public_key: String) -> Promise { + let signer_id = env::signer_account_id(); + if signer_id == env::current_account_id() { + let public_key = PublicKey::from_str(&public_key).unwrap(); + Promise::new(signer_id).add_full_access_key(public_key) + } else { + // here we call the contract again on the signer, assuming that + // it has been deployed. This lets us test the two cases, one where + // we have a local receipt generated by a tx with the same signer and receiver, + // and one where test1 calls test0, and what we want to look for on chain is + // the receipt generated by this cross-contract call here + Self::ext(signer_id).add_key(public_key) + } + } +} diff --git a/pytest/tools/mirror/mirror_utils.py b/pytest/tools/mirror/mirror_utils.py index 00f7f906fb8..e3c36b9926a 100644 --- a/pytest/tools/mirror/mirror_utils.py +++ b/pytest/tools/mirror/mirror_utils.py @@ -281,7 +281,9 @@ def send_add_access_key(node, key, target_key, nonce, block_hash): key.account_id, key.decoded_pk(), key.decoded_sk()) - node.send_tx(tx) + res = node.send_tx(tx) + logger.info( + f'sent add key tx for {target_key.account_id} {target_key.pk}: {res}') def send_delete_access_key(node, key, target_key, nonce, block_hash): @@ -292,7 +294,10 @@ def send_delete_access_key(node, key, target_key, nonce, block_hash): target_key.account_id, key.decoded_pk(), key.decoded_sk()) - node.send_tx(tx) + res = node.send_tx(tx) + logger.info( + f'sent delete key tx for {target_key.account_id} {target_key.pk}: {res}' + ) def create_subaccount(node, signer_key, nonce, block_hash): @@ -311,10 +316,36 @@ def create_subaccount(node, signer_key, nonce, block_hash): signer_key.account_id, signer_key.decoded_pk(), signer_key.decoded_sk()) - node.send_tx(tx) + res = node.send_tx(tx) + logger.info(f'sent create account tx for {k.account_id} {k.pk}: {res}') return k +def deploy_addkey_contract(node, signer_key, contract_path, nonce, block_hash): + code = utils.load_binary_file(contract_path) + tx = transaction.sign_deploy_contract_tx(signer_key, code, nonce, + block_hash) + node.send_tx(tx) + + +def call_addkey(node, signer_key, new_key, nonce, block_hash, extra_actions=[]): + args = bytearray(json.dumps({'public_key': new_key.pk}), encoding='utf-8') + + # add a transfer action and the extra_actions to exercise some more code paths + actions = [ + transaction.create_function_call_action('add_key', args, 10**14, 0), + transaction.create_payment_action(123) + ] + actions.extend(extra_actions) + tx = transaction.sign_and_serialize_transaction('test0', nonce, actions, + block_hash, + signer_key.account_id, + signer_key.decoded_pk(), + signer_key.decoded_sk()) + res = node.send_tx(tx) + logger.info(f'called add_key for {new_key.account_id} {new_key.pk}: {res}') + + # a key that we added with an AddKey tx or implicit account transfer. # just for nonce handling convenience class AddedKey: @@ -325,7 +356,9 @@ def __init__(self, key): def send_if_inited(self, node, transfers, block_hash): if self.nonce is None: - self.nonce = node.get_nonce_for_pk(self.key.account_id, self.key.pk) + self.nonce = node.get_nonce_for_pk(self.key.account_id, + self.key.pk, + finality='final') if self.nonce is not None: logger.info( f'added key {self.key.account_id} {self.key.pk} inited @ {self.nonce}' @@ -338,6 +371,9 @@ def send_if_inited(self, node, transfers, block_hash): self.nonce, block_hash) node.send_tx(tx) + def account_id(self): + return self.key.account_id + def inited(self): return self.nonce is not None @@ -348,7 +384,7 @@ def __init__(self): self.key = AddedKey(key.Key.implicit_account()) def account_id(self): - return self.key.key.account_id + return self.key.account_id() def transfer(self, node, sender_key, amount, block_hash, nonce): tx = transaction.sign_payment_tx(sender_key, self.account_id(), amount, @@ -406,9 +442,11 @@ def check_num_txs(source_node, target_node): total_source_txs = count_total_txs(source_node, min_height=genesis_height) total_target_txs = count_total_txs(target_node) - assert total_source_txs == total_target_txs, (total_source_txs, + assert total_source_txs <= total_target_txs, (total_source_txs, total_target_txs) - logger.info(f'all {total_source_txs} transactions mirrored') + logger.info( + f'passed. num source txs: {total_source_txs} num target txs: {total_target_txs}' + ) # keeps info initialized during start_source_chain() for use in send_traffic() @@ -432,12 +470,30 @@ def send_transfers(self, nodes, block_hash, skip_senders=None): self.nonces[sender] += 1 +def added_keys_send_transfers(nodes, added_keys, receivers, amount, block_hash): + node_idx = 0 + for key in added_keys: + key.send_if_inited(nodes[node_idx], + [(receiver, amount) for receiver in receivers], + block_hash) + node_idx += 1 + node_idx %= len(nodes) + + def start_source_chain(config, num_source_validators=3, target_validators=['foo0', 'foo1', 'foo2']): # for now we need at least 2 because we're sending traffic for source_nodes[1].signer_key # Could fix that but for now this assert is fine assert num_source_validators >= 2 + + contract_path = pathlib.Path(__file__).resolve().parents[ + 0] / 'contract/target/wasm32-unknown-unknown/release/addkey_contract.wasm' + if not os.path.exists(contract_path): + sys.exit( + 'please build the addkey contract by running cargo build --target wasm32-unknown-unknown --release from the ./contract/ dir' + ) + config_changes = {} for i in range(num_source_validators + 1): config_changes[i] = {"tracked_shards": [0, 1, 2, 3], "archive": True} @@ -470,10 +526,21 @@ def start_source_chain(config, traffic_data.implicit_account = ImplicitAccount() for height, block_hash in utils.poll_blocks(source_nodes[0], timeout=TIMEOUT): - traffic_data.implicit_account.transfer( - source_nodes[0], source_nodes[0].signer_key, 10**24, - base58.b58decode(block_hash.encode('utf8')), traffic_data.nonces[0]) + block_hash_bytes = base58.b58decode(block_hash.encode('utf8')) + traffic_data.implicit_account.transfer(source_nodes[0], + source_nodes[0].signer_key, + 10**24, block_hash_bytes, + traffic_data.nonces[0]) traffic_data.nonces[0] += 1 + + deploy_addkey_contract(source_nodes[0], source_nodes[0].signer_key, + contract_path, traffic_data.nonces[0], + block_hash_bytes) + traffic_data.nonces[0] += 1 + deploy_addkey_contract(source_nodes[0], source_nodes[1].signer_key, + contract_path, traffic_data.nonces[1], + block_hash_bytes) + traffic_data.nonces[1] += 1 break for height, block_hash in utils.poll_blocks(source_nodes[0], @@ -514,6 +581,33 @@ def send_traffic(near_root, source_nodes, traffic_data, callback): traffic_data.nonces[0], block_hash_bytes) traffic_data.nonces[0] += 1 + test0_contract_key = key.Key.from_random('test0') + test0_contract_extra_key = key.Key.from_random('test0') + + # here we are assuming that the deployed contract has landed since we called start_source_chain() + # we will add an extra AddKey action to hit some more code paths + call_addkey(source_nodes[1], + source_nodes[0].signer_key, + test0_contract_key, + traffic_data.nonces[0], + block_hash_bytes, + extra_actions=[ + transaction.create_full_access_key_action( + test0_contract_extra_key.decoded_pk()) + ]) + traffic_data.nonces[0] += 1 + + test0_contract_key = AddedKey(test0_contract_key) + test0_contract_extra_key = AddedKey(test0_contract_extra_key) + + test1_contract_key = key.Key.from_random('test1') + + call_addkey(source_nodes[1], source_nodes[1].signer_key, test1_contract_key, + traffic_data.nonces[1], block_hash_bytes) + traffic_data.nonces[1] += 1 + + test1_contract_key = AddedKey(test1_contract_key) + test0_deleted_height = None test0_readded_key = None implicit_added = None @@ -555,15 +649,17 @@ def send_traffic(near_root, source_nodes, traffic_data, callback): [('test2', height), ('test0', height), (traffic_data.implicit_account.account_id(), height)], block_hash_bytes) - new_key.send_if_inited( - source_nodes[1], - [('test1', height), ('test2', height), - (traffic_data.implicit_account.account_id(), height), - (implicit_account2.account_id(), height)], block_hash_bytes) - subaccount_key.send_if_inited( - source_nodes[1], [('test3', height), - (implicit_account2.account_id(), height)], - block_hash_bytes) + keys = [ + new_key, + subaccount_key, + test0_contract_key, + test0_contract_extra_key, + test1_contract_key, + ] + added_keys_send_transfers(source_nodes, keys, [ + traffic_data.implicit_account.account_id(), + implicit_account2.account_id(), 'test2', 'test3' + ], height, block_hash_bytes) if implicit_added is None: # wait for 15 blocks after we started to get some "normal" traffic diff --git a/pytest/tools/mirror/online_test.py b/pytest/tools/mirror/online_test.py index dd8541893fa..66eff9387cd 100755 --- a/pytest/tools/mirror/online_test.py +++ b/pytest/tools/mirror/online_test.py @@ -32,7 +32,6 @@ def main(): mirror_utils.dot_near() / f'{mirror_utils.MIRROR_DIR}/source', online_source=True) - mirror_utils.send_traffic(near_root, source_nodes, traffic_data, mirror.restart_once) diff --git a/tools/mirror/Cargo.toml b/tools/mirror/Cargo.toml index 97d72680791..2fa9da3aa24 100644 --- a/tools/mirror/Cargo.toml +++ b/tools/mirror/Cargo.toml @@ -34,6 +34,7 @@ near-chain = { path = "../../chain/chain" } near-chain-primitives = { path = "../../chain/chain-primitives" } near-client = { path = "../../chain/client" } near-client-primitives = { path = "../../chain/client-primitives" } +near-epoch-manager = { path = "../../chain/epoch-manager" } near-indexer-primitives = { path = "../../chain/indexer-primitives" } near-indexer = { path = "../../chain/indexer" } near-network = { path = "../../chain/network" } diff --git a/tools/mirror/src/chain_tracker.rs b/tools/mirror/src/chain_tracker.rs index 551bf2679db..0504e643d82 100644 --- a/tools/mirror/src/chain_tracker.rs +++ b/tools/mirror/src/chain_tracker.rs @@ -1,6 +1,6 @@ use crate::{ - ChainObjectId, LatestTargetNonce, MappedBlock, MappedTx, NonceUpdater, ReceiptInfo, - TargetChainTx, TargetNonce, TxInfo, TxOutcome, TxRef, + ChainObjectId, LatestTargetNonce, MappedBlock, MappedTx, MappedTxProvenance, NonceUpdater, + ReceiptInfo, TargetChainTx, TargetNonce, TxInfo, TxOutcome, TxRef, }; use actix::Addr; use near_client::ViewClientActor; @@ -27,7 +27,7 @@ use std::time::{Duration, Instant}; struct TxSendInfo { sent_at: Instant, source_height: BlockHeight, - source_tx_index: usize, + provenance: MappedTxProvenance, source_shard_id: ShardId, source_signer_id: AccountId, source_receiver_id: AccountId, @@ -59,7 +59,7 @@ impl TxSendInfo { Self { source_height, source_shard_id: source_shard_id, - source_tx_index: tx.source_tx_index, + provenance: tx.provenance, source_signer_id: tx.source_signer_id.clone(), source_receiver_id: tx.source_receiver_id.clone(), target_signer_id, @@ -400,7 +400,7 @@ impl TxTracker { TargetChainTx::Ready(t) => { tracing::debug!( target: "mirror", "Prepared {} for ({}, {:?}) with nonce {} even though there are still pending outcomes that may affect the access key", - &tx_ref, &t.target_tx.transaction.signer_id, &t.target_tx.transaction.public_key, t.target_tx.transaction.nonce + &t.provenance, &t.target_tx.transaction.signer_id, &t.target_tx.transaction.public_key, t.target_tx.transaction.nonce ); self.nonces .get_mut(&( @@ -414,7 +414,7 @@ impl TxTracker { TargetChainTx::AwaitingNonce(t) => { tracing::warn!( target: "mirror", "Could not prepare {} for ({}, {:?}). Nonce unknown", - &tx_ref, &t.target_tx.signer_id, &t.target_tx.public_key, + &t.provenance, &t.target_tx.signer_id, &t.target_tx.public_key, ); } }; @@ -508,14 +508,14 @@ impl TxTracker { if let Some(info) = self.sent_txs.get(&tx.transaction.hash) { write!( log_message, - "source #{}{} tx #{} signer: \"{}\"{} receiver: \"{}\"{} actions: <{}> sent {:?} ago @ target #{}\n", + "source #{}{} {} signer: \"{}\"{} receiver: \"{}\"{} actions: <{}> sent {:?} ago @ target #{}\n", info.source_height, if s.shard_id == info.source_shard_id { String::new() } else { format!(" (source shard {})", info.source_shard_id) }, - info.source_tx_index, + info.provenance, info.source_signer_id, info.target_signer_id.as_ref().map_or(String::new(), |s| format!(" (mapped to \"{}\")", s)), info.source_receiver_id, @@ -751,6 +751,15 @@ impl TxTracker { return Ok(()); } + if matches!( + &tx.provenance, + MappedTxProvenance::ReceiptAddKey(_) | MappedTxProvenance::TxAddKey(_) + ) { + tracing::debug!( + target: "mirror", "Successfully sent transaction {} for {} @ source #{} to add extra keys: {:?}", + &hash, &tx.target_tx.transaction.receiver_id, tx_ref.source_height, &tx.target_tx.transaction.actions, + ); + } let access_key = ( tx.target_tx.transaction.signer_id.clone(), tx.target_tx.transaction.public_key.clone(), diff --git a/tools/mirror/src/lib.rs b/tools/mirror/src/lib.rs index aa14806bcaf..327c87efbbd 100644 --- a/tools/mirror/src/lib.rs +++ b/tools/mirror/src/lib.rs @@ -3,11 +3,12 @@ use anyhow::Context; use async_trait::async_trait; use borsh::{BorshDeserialize, BorshSerialize}; use near_chain_configs::GenesisValidationMode; +use near_chain_primitives::error::QueryError as RuntimeQueryError; use near_client::{ClientActor, ViewClientActor}; use near_client::{ProcessTxRequest, ProcessTxResponse}; use near_client_primitives::types::{ GetBlockError, GetChunkError, GetExecutionOutcome, GetExecutionOutcomeError, - GetExecutionOutcomeResponse, Query, QueryError, + GetExecutionOutcomeResponse, GetReceiptError, Query, QueryError, }; use near_crypto::{PublicKey, SecretKey}; use near_indexer::{Indexer, StreamerMessage}; @@ -20,7 +21,8 @@ use near_primitives::types::{ AccountId, BlockHeight, BlockReference, Finality, TransactionOrReceiptId, }; use near_primitives::views::{ - ExecutionStatusView, QueryRequest, QueryResponseKind, SignedTransactionView, + AccessKeyView, ActionView, ExecutionOutcomeWithIdView, ExecutionStatusView, QueryRequest, + QueryResponseKind, ReceiptEnumView, ReceiptView, SignedTransactionView, }; use near_primitives_core::types::{Nonce, ShardId}; use nearcore::config::NearConfig; @@ -224,15 +226,16 @@ fn set_next_source_height(db: &DB, height: BlockHeight) -> anyhow::Result<()> { Ok(()) } -struct ChunkTxs { +struct SourceBlock { shard_id: ShardId, transactions: Vec, + receipts: Vec, } #[derive(thiserror::Error, Debug)] enum ChainError { - #[error("block unknown")] - UnknownBlock, + #[error("unknown")] + Unknown, #[error(transparent)] Other(#[from] anyhow::Error), } @@ -246,7 +249,7 @@ impl ChainError { impl From for ChainError { fn from(err: GetBlockError) -> Self { match err { - GetBlockError::UnknownBlock { .. } => Self::UnknownBlock, + GetBlockError::UnknownBlock { .. } => Self::Unknown, _ => Self::other(err), } } @@ -255,7 +258,7 @@ impl From for ChainError { impl From for ChainError { fn from(err: GetChunkError) -> Self { match err { - GetChunkError::UnknownBlock { .. } => Self::UnknownBlock, + GetChunkError::UnknownBlock { .. } => Self::Unknown, _ => Self::other(err), } } @@ -264,7 +267,52 @@ impl From for ChainError { impl From for ChainError { fn from(err: near_chain_primitives::Error) -> Self { match err { - near_chain_primitives::Error::DBNotFoundErr(_) => Self::UnknownBlock, + near_chain_primitives::Error::DBNotFoundErr(_) => Self::Unknown, + _ => Self::other(err), + } + } +} + +impl From for ChainError { + fn from(err: GetExecutionOutcomeError) -> Self { + match err { + GetExecutionOutcomeError::UnknownBlock { .. } + | GetExecutionOutcomeError::UnknownTransactionOrReceipt { .. } + | GetExecutionOutcomeError::NotConfirmed { .. } => Self::Unknown, + _ => Self::other(err), + } + } +} + +impl From for ChainError { + fn from(err: GetReceiptError) -> Self { + match err { + GetReceiptError::UnknownReceipt(_) => Self::Unknown, + _ => Self::other(err), + } + } +} + +impl From for ChainError { + fn from(err: QueryError) -> Self { + match err { + QueryError::UnavailableShard { .. } + | QueryError::UnknownAccount { .. } + | QueryError::NoContractCode { .. } + | QueryError::UnknownAccessKey { .. } + | QueryError::GarbageCollectedBlock { .. } + | QueryError::UnknownBlock { .. } => Self::Unknown, + _ => Self::other(err), + } + } +} + +impl From for ChainError { + fn from(err: RuntimeQueryError) -> Self { + match err { + RuntimeQueryError::UnknownAccount { .. } + | RuntimeQueryError::NoContractCode { .. } + | RuntimeQueryError::UnknownAccessKey { .. } => Self::Unknown, _ => Self::other(err), } } @@ -276,13 +324,59 @@ trait ChainAccess { Ok(()) } - async fn head_height(&self) -> anyhow::Result; + async fn head_height(&self) -> Result; async fn get_txs( &self, height: BlockHeight, shards: &[ShardId], - ) -> Result, ChainError>; + ) -> Result, ChainError>; + + async fn get_outcome( + &self, + id: TransactionOrReceiptId, + ) -> Result; + + // for transactions with the same signer and receiver, we + // want the receipt that results from applying it, if successful, + // since that receipt doesn't show up if you just look through + // the receipts in the next block. + // If the tx wasn't successful, just returns None + async fn get_tx_receipt_id( + &self, + tx_hash: &CryptoHash, + signer_id: &AccountId, + ) -> Result, ChainError> { + match self + .get_outcome(TransactionOrReceiptId::Transaction { + transaction_hash: tx_hash.clone(), + sender_id: signer_id.clone(), + }) + .await? + .outcome + .status + { + ExecutionStatusView::SuccessReceiptId(id) => Ok(Some(id)), + ExecutionStatusView::Failure(_) | ExecutionStatusView::Unknown => Ok(None), + ExecutionStatusView::SuccessValue(_) => unreachable!(), + } + } + + async fn get_receipt(&self, id: &CryptoHash) -> Result; + + // returns all public keys with full permissions for the given account + async fn get_full_access_keys( + &self, + account_id: &AccountId, + block_hash: &CryptoHash, + ) -> Result, ChainError>; +} + +fn execution_status_good(status: &ExecutionStatusView) -> bool { + matches!( + status, + ExecutionStatusView::SuccessReceiptId(_) | ExecutionStatusView::SuccessValue(_) + ) } struct TxMirror { @@ -315,6 +409,23 @@ fn open_db>(home: P, config: &NearConfig) -> anyhow::Result { Ok(DB::open_cf_descriptors(&options, db_path, cf_descriptors)?) } +#[derive(Clone, Copy, Debug)] +enum MappedTxProvenance { + SourceTxIndex(usize), + TxAddKey(usize), + ReceiptAddKey(usize), +} + +impl std::fmt::Display for MappedTxProvenance { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::SourceTxIndex(idx) => write!(f, "tx #{}", idx), + Self::TxAddKey(idx) => write!(f, "extra AddKey for tx #{}", idx), + Self::ReceiptAddKey(idx) => write!(f, "extra AddKey for receipt #{}", idx), + } + } +} + // a transaction that's almost prepared, except that we don't yet know // what nonce to use because the public key was added in an AddKey // action that we haven't seen on chain yet. The target_tx field is complete @@ -323,8 +434,8 @@ fn open_db>(home: P, config: &NearConfig) -> anyhow::Result { struct TxAwaitingNonce { source_signer_id: AccountId, source_receiver_id: AccountId, - source_tx_index: usize, - target_private: SecretKey, + provenance: MappedTxProvenance, + target_secret_key: SecretKey, target_tx: Transaction, nonce_updates: HashSet<(AccountId, PublicKey)>, target_nonce: TargetNonce, @@ -332,30 +443,31 @@ struct TxAwaitingNonce { impl TxAwaitingNonce { fn new( - source_tx: &SignedTransactionView, - source_tx_index: usize, - actions: Vec, - target_nonce: &TargetNonce, - ref_hash: &CryptoHash, + source_signer_id: AccountId, + source_receiver_id: AccountId, target_signer_id: AccountId, target_receiver_id: AccountId, - target_private: SecretKey, + target_secret_key: SecretKey, target_public_key: PublicKey, + actions: Vec, + target_nonce: &TargetNonce, + ref_hash: &CryptoHash, + provenance: MappedTxProvenance, nonce_updates: HashSet<(AccountId, PublicKey)>, ) -> Self { let mut target_tx = Transaction::new( target_signer_id, target_public_key, target_receiver_id, - source_tx.nonce, + 0, ref_hash.clone(), ); target_tx.actions = actions; Self { - source_signer_id: source_tx.signer_id.clone(), - source_receiver_id: source_tx.receiver_id.clone(), - source_tx_index, - target_private, + source_signer_id, + source_receiver_id, + provenance, + target_secret_key, target_tx, nonce_updates, target_nonce: target_nonce.clone(), @@ -370,7 +482,7 @@ impl TxAwaitingNonce { struct MappedTx { source_signer_id: AccountId, source_receiver_id: AccountId, - source_tx_index: usize, + provenance: MappedTxProvenance, target_tx: SignedTransaction, nonce_updates: HashSet<(AccountId, PublicKey)>, sent_successfully: bool, @@ -378,15 +490,16 @@ struct MappedTx { impl MappedTx { fn new( - source_tx: &SignedTransactionView, - source_tx_index: usize, - actions: Vec, - nonce: Nonce, - ref_hash: &CryptoHash, + source_signer_id: AccountId, + source_receiver_id: AccountId, target_signer_id: AccountId, target_receiver_id: AccountId, target_secret_key: &SecretKey, target_public_key: PublicKey, + actions: Vec, + nonce: Nonce, + ref_hash: &CryptoHash, + provenance: MappedTxProvenance, nonce_updates: HashSet<(AccountId, PublicKey)>, ) -> Self { let mut target_tx = Transaction::new( @@ -402,9 +515,9 @@ impl MappedTx { target_tx, ); Self { - source_signer_id: source_tx.signer_id.clone(), - source_receiver_id: source_tx.receiver_id.clone(), - source_tx_index, + source_signer_id, + source_receiver_id, + provenance, target_tx, nonce_updates, sent_successfully: false, @@ -424,13 +537,13 @@ impl TargetChainTx { Self::AwaitingNonce(t) => { t.target_tx.nonce = nonce; let target_tx = SignedTransaction::new( - t.target_private.sign(&t.target_tx.get_hash_and_size().0.as_ref()), + t.target_secret_key.sign(&t.target_tx.get_hash_and_size().0.as_ref()), t.target_tx.clone(), ); *self = Self::Ready(MappedTx { source_signer_id: t.source_signer_id.clone(), source_receiver_id: t.source_receiver_id.clone(), - source_tx_index: t.source_tx_index, + provenance: t.provenance, target_tx, nonce_updates: t.nonce_updates.clone(), sent_successfully: false, @@ -454,53 +567,57 @@ impl TargetChainTx { } fn new_ready( - source_tx: &SignedTransactionView, - source_tx_index: usize, - actions: Vec, - nonce: Nonce, - ref_hash: &CryptoHash, + source_signer_id: AccountId, + source_receiver_id: AccountId, target_signer_id: AccountId, target_receiver_id: AccountId, target_secret_key: &SecretKey, target_public_key: PublicKey, + actions: Vec, + nonce: Nonce, + ref_hash: &CryptoHash, + provenance: MappedTxProvenance, nonce_updates: HashSet<(AccountId, PublicKey)>, ) -> Self { Self::Ready(MappedTx::new( - source_tx, - source_tx_index, - actions, - nonce, - &ref_hash, + source_signer_id, + source_receiver_id, target_signer_id, target_receiver_id, target_secret_key, target_public_key, + actions, + nonce, + ref_hash, + provenance, nonce_updates, )) } fn new_awaiting_nonce( - source_tx: &SignedTransactionView, - source_tx_index: usize, - actions: Vec, - target_nonce: &TargetNonce, - ref_hash: &CryptoHash, + source_signer_id: AccountId, + source_receiver_id: AccountId, target_signer_id: AccountId, target_receiver_id: AccountId, - target_private: SecretKey, + target_secret_key: &SecretKey, target_public_key: PublicKey, + actions: Vec, + target_nonce: &TargetNonce, + ref_hash: &CryptoHash, + provenance: MappedTxProvenance, nonce_updates: HashSet<(AccountId, PublicKey)>, ) -> Self { Self::AwaitingNonce(TxAwaitingNonce::new( - source_tx, - source_tx_index, - actions, - target_nonce, - &ref_hash, + source_signer_id, + source_receiver_id, target_signer_id, target_receiver_id, - target_private, + target_secret_key.clone(), target_public_key, + actions, + target_nonce, + &ref_hash, + provenance, nonce_updates, )) } @@ -745,8 +862,8 @@ impl TxMirror { // that some other instance of this code ran and made progress already. For now we can assume // only once instance of this code will run, but this is the place to detect if that's not the case. tracing::error!( - target: "mirror", "Tried to send an invalid tx for ({}, {:?}) from source #{} shard {} tx {}: {:?}", - &tx.target_tx.transaction.signer_id, &tx.target_tx.transaction.public_key, block.source_height, chunk.shard_id, tx.source_tx_index, e + target: "mirror", "Tried to send an invalid tx for ({}, {:?}) from source #{} shard {} {}: {:?}", + &tx.target_tx.transaction.signer_id, &tx.target_tx.transaction.public_key, block.source_height, chunk.shard_id, &tx.provenance, e ); crate::metrics::TRANSACTIONS_SENT .with_label_values(&["invalid"]) @@ -833,6 +950,323 @@ impl TxMirror { Ok((actions, nonce_updates)) } + async fn prepare_tx( + &self, + tracker: &mut crate::chain_tracker::TxTracker, + source_signer_id: AccountId, + source_receiver_id: AccountId, + target_signer_id: AccountId, + target_receiver_id: AccountId, + target_secret_key: &SecretKey, + actions: Vec, + ref_hash: &CryptoHash, + source_height: BlockHeight, + provenance: MappedTxProvenance, + nonce_updates: HashSet<(AccountId, PublicKey)>, + ) -> anyhow::Result { + let target_public_key = target_secret_key.public_key(); + let target_nonce = tracker + .next_nonce( + &self.target_view_client, + &self.db, + &target_signer_id, + &target_public_key, + source_height, + ) + .await?; + if target_nonce.pending_outcomes.is_empty() && target_nonce.nonce.is_some() { + Ok(TargetChainTx::new_ready( + source_signer_id, + source_receiver_id, + target_signer_id, + target_receiver_id, + &target_secret_key, + target_public_key, + actions, + target_nonce.nonce.unwrap(), + &ref_hash, + provenance, + nonce_updates, + )) + } else { + Ok(TargetChainTx::new_awaiting_nonce( + source_signer_id, + source_receiver_id, + target_signer_id, + target_receiver_id, + target_secret_key, + target_public_key, + actions, + target_nonce, + &ref_hash, + provenance, + nonce_updates, + )) + } + } + + // add extra AddKey transactions that come from function call. If we don't do this, + // then the only keys we will have mapped are the ones added by regular AddKey transactions. + async fn push_extra_tx( + &self, + tracker: &mut crate::chain_tracker::TxTracker, + block_hash: CryptoHash, + txs: &mut Vec, + predecessor_id: AccountId, + receiver_id: AccountId, + access_keys: Vec<(PublicKey, AccessKeyView)>, + ref_hash: &CryptoHash, + provenance: MappedTxProvenance, + source_height: BlockHeight, + ) -> anyhow::Result<()> { + let target_signer_id = + crate::key_mapping::map_account(&predecessor_id, self.secret.as_ref()); + + let target_secret_key = match self + .source_chain_access + .get_full_access_keys(&predecessor_id, &block_hash) + .await + { + Ok(keys) => { + let mut key = None; + let mut first_key = None; + for k in keys.iter() { + let target_secret_key = crate::key_mapping::map_key(k, self.secret.as_ref()); + if fetch_access_key_nonce( + &self.target_view_client, + &target_signer_id, + &target_secret_key.public_key(), + ) + .await? + .is_some() + { + key = Some(target_secret_key); + break; + } + if first_key.is_none() { + first_key = Some(target_secret_key); + } + } + // here none of them have equivalents in the target chain. Just use the first one and hope that + // it will become available. shouldn't happen much in practice + if key.is_none() { + if let Some(k) = first_key { + tracing::warn!( + target: "mirror", "preparing a transaction for {} for signer {} with key {} even though it is not yet known in the target chain", + &provenance, &target_signer_id, &k.public_key(), + ); + key = Some(k); + } + } + match key { + Some(key) => key, + None => { + tracing::warn!( + target: "mirror", "not preparing a transaction for {} because no full access key for {} in the source chain is known at block {}", + &provenance, &target_signer_id, &block_hash, + ); + return Ok(()); + } + } + } + Err(ChainError::Unknown) => { + tracing::warn!( + target: "mirror", "not preparing a transaction for {} because no full access key for {} in the source chain is known at block {}", + &provenance, &predecessor_id, &block_hash, + ); + return Ok(()); + } + Err(ChainError::Other(e)) => { + return Err(e) + .with_context(|| format!("failed fetching access key for {}", &predecessor_id)) + } + }; + + let target_receiver_id = + crate::key_mapping::map_account(&receiver_id, self.secret.as_ref()); + + let mut nonce_updates = HashSet::new(); + let mut actions = Vec::new(); + + for (public_key, access_key) in access_keys { + let target_public_key = + crate::key_mapping::map_key(&public_key, self.secret.as_ref()).public_key(); + + nonce_updates.insert((target_receiver_id.clone(), target_public_key.clone())); + actions.push(Action::AddKey(AddKeyAction { + public_key: target_public_key, + access_key: access_key.into(), + })); + } + + let target_tx = self + .prepare_tx( + tracker, + predecessor_id, + receiver_id, + target_signer_id, + target_receiver_id, + &target_secret_key, + actions, + ref_hash, + source_height, + provenance, + nonce_updates, + ) + .await?; + txs.push(target_tx); + Ok(()) + } + + async fn add_function_call_keys( + &self, + tracker: &mut crate::chain_tracker::TxTracker, + txs: &mut Vec, + receipt_id: &CryptoHash, + receiver_id: &AccountId, + ref_hash: &CryptoHash, + provenance: MappedTxProvenance, + source_height: BlockHeight, + ) -> anyhow::Result<()> { + let outcome = self + .source_chain_access + .get_outcome(TransactionOrReceiptId::Receipt { + receipt_id: receipt_id.clone(), + receiver_id: receiver_id.clone(), + }) + .await + .with_context(|| format!("failed fetching outcome for receipt {}", receipt_id))?; + if !execution_status_good(&outcome.outcome.status) { + return Ok(()); + } + for id in outcome.outcome.receipt_ids.iter() { + let receipt = match self.source_chain_access.get_receipt(id).await { + Ok(r) => r, + Err(ChainError::Unknown) => { + tracing::warn!( + target: "mirror", "receipt {} appears in the list output by receipt {}, but can't find it in the source chain", + id, receipt_id, + ); + continue; + } + Err(ChainError::Other(e)) => return Err(e), + }; + if let ReceiptEnumView::Action { actions, .. } = &receipt.receipt { + // TODO: if predecessor and receiver are different, then any AddKeys are probably along with a + // CreateAccount. That is a different case we will handler later + if receipt.predecessor_id != receipt.receiver_id { + continue; + } + let mut keys = Vec::new(); + for a in actions.iter() { + if let ActionView::AddKey { public_key, access_key } = a { + keys.push((public_key.clone(), access_key.clone())); + } + } + + if keys.is_empty() { + continue; + } + let outcome = self + .source_chain_access + .get_outcome(TransactionOrReceiptId::Receipt { + receipt_id: receipt.receipt_id.clone(), + receiver_id: receipt.receiver_id.clone(), + }) + .await + .with_context(|| { + format!("failed fetching outcome for receipt {}", receipt.receipt_id) + })?; + if !execution_status_good(&outcome.outcome.status) { + continue; + } + + self.push_extra_tx( + tracker, + outcome.block_hash, + txs, + receipt.predecessor_id, + receipt.receiver_id, + keys, + ref_hash, + provenance, + source_height, + ) + .await?; + } + } + + Ok(()) + } + + async fn add_tx_function_call_keys( + &self, + tx: &SignedTransactionView, + tx_idx: usize, + source_height: BlockHeight, + ref_hash: &CryptoHash, + tracker: &mut crate::chain_tracker::TxTracker, + txs: &mut Vec, + ) -> anyhow::Result<()> { + // if signer and receiver are the same then the resulting local receipt + // is only logically included, and we won't see it in the receipts in any chunk, + // so handle that case here + if tx.signer_id == tx.receiver_id + && tx.actions.iter().any(|a| matches!(a, ActionView::FunctionCall { .. })) + { + if let Some(receipt_id) = self + .source_chain_access + .get_tx_receipt_id(&tx.hash, &tx.signer_id) + .await + .with_context(|| format!("failed fetching local receipt ID for tx {}", &tx.hash))? + { + self.add_function_call_keys( + tracker, + txs, + &receipt_id, + &tx.receiver_id, + ref_hash, + MappedTxProvenance::TxAddKey(tx_idx), + source_height, + ) + .await + } else { + Ok(()) + } + } else { + Ok(()) + } + } + + async fn add_receipt_function_call_keys( + &self, + receipt: &ReceiptView, + receipt_idx: usize, + source_height: BlockHeight, + ref_hash: &CryptoHash, + tracker: &mut crate::chain_tracker::TxTracker, + txs: &mut Vec, + ) -> anyhow::Result<()> { + if let ReceiptEnumView::Action { actions, .. } = &receipt.receipt { + if actions.iter().any(|a| matches!(a, ActionView::FunctionCall { .. })) { + self.add_function_call_keys( + tracker, + txs, + &receipt.receipt_id, + &receipt.receiver_id, + ref_hash, + MappedTxProvenance::ReceiptAddKey(receipt_idx), + source_height, + ) + .await + } else { + Ok(()) + } + } else { + Ok(()) + } + } + // fetch the source chain block at `source_height`, and prepare a // set of transactions that should be valid in the target chain // from it. @@ -846,103 +1280,70 @@ impl TxMirror { match self.source_chain_access.get_txs(source_height, &self.tracked_shards).await { Ok(x) => x, Err(e) => match e { - ChainError::UnknownBlock => return Ok(None), + ChainError::Unknown => return Ok(None), ChainError::Other(e) => return Err(e), }, }; - let mut num_not_ready = 0; let mut chunks = Vec::new(); for ch in source_chunks { let mut txs = Vec::new(); for (idx, source_tx) in ch.transactions.into_iter().enumerate() { - let (actions, tx_nonce_updates) = self.map_actions(&source_tx).await?; + let (actions, nonce_updates) = self.map_actions(&source_tx).await?; if actions.is_empty() { // If this is a tx containing only stake actions, skip it. continue; } - let mapped_key = + let target_private_key = crate::key_mapping::map_key(&source_tx.public_key, self.secret.as_ref()); - let public_key = mapped_key.public_key(); let target_signer_id = crate::key_mapping::map_account(&source_tx.signer_id, self.secret.as_ref()); let target_receiver_id = crate::key_mapping::map_account(&source_tx.receiver_id, self.secret.as_ref()); - let target_nonce = tracker - .next_nonce( - &self.target_view_client, - &self.db, - &target_signer_id, - &public_key, + let target_tx = self + .prepare_tx( + tracker, + source_tx.signer_id.clone(), + source_tx.receiver_id.clone(), + target_signer_id, + target_receiver_id, + &target_private_key, + actions, + &ref_hash, source_height, + MappedTxProvenance::SourceTxIndex(idx), + nonce_updates, ) .await?; - if target_nonce.pending_outcomes.is_empty() { - match target_nonce.nonce { - Some(nonce) => { - let target_tx = TargetChainTx::new_ready( - &source_tx, - idx, - actions, - nonce, - &ref_hash, - target_signer_id, - target_receiver_id, - &mapped_key, - public_key, - tx_nonce_updates, - ); - txs.push(target_tx); - } - None => { - num_not_ready += 1; - let target_tx = TargetChainTx::new_awaiting_nonce( - &source_tx, - idx, - actions, - target_nonce, - &ref_hash, - target_signer_id, - target_receiver_id, - mapped_key, - public_key, - tx_nonce_updates, - ); - txs.push(target_tx); - } - } - } else { - num_not_ready += 1; - let target_tx = TargetChainTx::new_awaiting_nonce( - &source_tx, - idx, - actions, - target_nonce, - &ref_hash, - target_signer_id, - target_receiver_id, - mapped_key, - public_key, - tx_nonce_updates, - ); - txs.push(target_tx); - } + txs.push(target_tx); + self.add_tx_function_call_keys( + &source_tx, + idx, + source_height, + &ref_hash, + tracker, + &mut txs, + ) + .await?; } - if num_not_ready == 0 { - tracing::debug!( - target: "mirror", "prepared {} transacations for source chain #{} shard {}", - txs.len(), source_height, ch.shard_id - ); - } else { - tracing::debug!( - target: "mirror", "prepared {} transacations for source chain #{} shard {} with {} \ - still waiting for the corresponding access keys to make it on chain", - txs.len(), source_height, ch.shard_id, num_not_ready, - ); + for (idx, r) in ch.receipts.iter().enumerate() { + self.add_receipt_function_call_keys( + r, + idx, + source_height, + &ref_hash, + tracker, + &mut txs, + ) + .await?; } + tracing::debug!( + target: "mirror", "prepared {} transacations for source chain #{} shard {}", + txs.len(), source_height, ch.shard_id + ); chunks.push(MappedChunk { txs, shard_id: ch.shard_id }); } Ok(Some(MappedBlock { source_height, chunks })) @@ -1059,7 +1460,9 @@ async fn run>( ) -> anyhow::Result<()> { if !online_source { let source_chain_access = crate::offline::ChainAccess::new(source_home)?; - let stop_height = stop_height.unwrap_or(source_chain_access.head_height().await?); + let stop_height = stop_height.unwrap_or( + source_chain_access.head_height().await.context("could not fetch source chain head")?, + ); TxMirror::new(source_chain_access, target_home, secret)?.run(Some(stop_height)).await } else { TxMirror::new(crate::online::ChainAccess::new(source_home)?, target_home, secret)? diff --git a/tools/mirror/src/offline.rs b/tools/mirror/src/offline.rs index dba74e5692e..847b27ab291 100644 --- a/tools/mirror/src/offline.rs +++ b/tools/mirror/src/offline.rs @@ -1,14 +1,33 @@ -use crate::{ChainError, ChunkTxs}; +use crate::{ChainError, SourceBlock}; use anyhow::Context; use async_trait::async_trait; -use near_chain::{ChainStore, ChainStoreAccess}; +use near_chain::{ChainStore, ChainStoreAccess, RuntimeAdapter}; use near_chain_configs::GenesisValidationMode; -use near_primitives::types::BlockHeight; +use near_crypto::PublicKey; +use near_epoch_manager::EpochManagerAdapter; +use near_primitives::block::BlockHeader; +use near_primitives::hash::CryptoHash; +use near_primitives::receipt::Receipt; +use near_primitives::types::{AccountId, BlockHeight, TransactionOrReceiptId}; +use near_primitives::views::{ + AccessKeyPermissionView, ExecutionOutcomeWithIdView, QueryRequest, QueryResponseKind, + ReceiptView, +}; use near_primitives_core::types::ShardId; +use nearcore::NightshadeRuntime; use std::path::Path; +fn is_on_current_chain( + chain: &ChainStore, + header: &BlockHeader, +) -> Result { + let chain_header = chain.get_block_header_by_height(header.height())?; + Ok(chain_header.hash() == header.hash()) +} + pub(crate) struct ChainAccess { chain: ChainStore, + runtime: NightshadeRuntime, } impl ChainAccess { @@ -24,25 +43,26 @@ impl ChainAccess { .with_context(|| format!("Error opening store in {:?}", home.as_ref()))? .get_store(near_store::Temperature::Hot); let chain = ChainStore::new( - store, + store.clone(), config.genesis.config.genesis_height, !config.client_config.archive, ); - Ok(Self { chain }) + let runtime = NightshadeRuntime::from_config(home.as_ref(), store, &config); + Ok(Self { chain, runtime }) } } #[async_trait(?Send)] impl crate::ChainAccess for ChainAccess { - async fn head_height(&self) -> anyhow::Result { - Ok(self.chain.head().context("Could not fetch chain head")?.height) + async fn head_height(&self) -> Result { + Ok(self.chain.head()?.height) } async fn get_txs( &self, height: BlockHeight, shards: &[ShardId], - ) -> Result, ChainError> { + ) -> Result, ChainError> { let block_hash = self.chain.get_block_hash_by_height(height)?; let block = self .chain @@ -67,11 +87,74 @@ impl crate::ChainAccess for ChainAccess { continue; } }; - chunks.push(ChunkTxs { + chunks.push(SourceBlock { shard_id: chunk.shard_id(), transactions: chunk.transactions().iter().map(|t| t.clone().into()).collect(), + receipts: chunk.receipts().iter().map(|t| t.clone().into()).collect(), }) } Ok(chunks) } + + async fn get_outcome( + &self, + id: TransactionOrReceiptId, + ) -> Result { + let id = match id { + TransactionOrReceiptId::Receipt { receipt_id, .. } => receipt_id, + TransactionOrReceiptId::Transaction { transaction_hash, .. } => transaction_hash, + }; + let outcomes = self.chain.get_outcomes_by_id(&id)?; + // this implements the same logic as in Chain::get_execution_outcome(). We will rewrite + // that here because it makes more sense for us to have just the ChainStore and not the Chain, + // since we're just reading data, not doing any protocol related stuff + outcomes + .into_iter() + .find(|outcome| match self.chain.get_block_header(&outcome.block_hash) { + Ok(header) => is_on_current_chain(&self.chain, &header).unwrap_or(false), + Err(_) => false, + }) + .map(Into::into) + .ok_or(ChainError::Unknown) + } + + async fn get_receipt(&self, id: &CryptoHash) -> Result { + self.chain.get_receipt(id)?.map(|r| Receipt::clone(&r).into()).ok_or(ChainError::Unknown) + } + + async fn get_full_access_keys( + &self, + account_id: &AccountId, + block_hash: &CryptoHash, + ) -> Result, ChainError> { + let mut ret = Vec::new(); + let header = self.chain.get_block_header(block_hash)?; + let shard_id = self.runtime.account_id_to_shard_id(account_id, header.epoch_id())?; + let shard_uid = self.runtime.shard_id_to_uid(shard_id, header.epoch_id())?; + let chunk_extra = self.chain.get_chunk_extra(header.hash(), &shard_uid)?; + match self + .runtime + .query( + shard_uid, + chunk_extra.state_root(), + header.height(), + header.raw_timestamp(), + header.prev_hash(), + header.hash(), + header.epoch_id(), + &QueryRequest::ViewAccessKeyList { account_id: account_id.clone() }, + )? + .kind + { + QueryResponseKind::AccessKeyList(l) => { + for k in l.keys { + if k.access_key.permission == AccessKeyPermissionView::FullAccess { + ret.push(k.public_key); + } + } + } + _ => unreachable!(), + } + Ok(ret) + } } diff --git a/tools/mirror/src/online.rs b/tools/mirror/src/online.rs index 748c5f3e2c6..ed7aebe5c9b 100644 --- a/tools/mirror/src/online.rs +++ b/tools/mirror/src/online.rs @@ -1,19 +1,28 @@ -use crate::{ChainError, ChunkTxs}; +use crate::{ChainError, SourceBlock}; use actix::Addr; use anyhow::Context; use async_trait::async_trait; use near_chain_configs::GenesisValidationMode; -use near_client::{ClientActor, ViewClientActor}; -use near_client_primitives::types::{GetChunk, GetChunkError}; +use near_client::ViewClientActor; +use near_client_primitives::types::{ + GetBlock, GetChunk, GetChunkError, GetExecutionOutcome, GetReceipt, Query, +}; +use near_crypto::PublicKey; use near_o11y::WithSpanContextExt; -use near_primitives::types::BlockHeight; +use near_primitives::hash::CryptoHash; +use near_primitives::types::{ + AccountId, BlockHeight, BlockId, BlockReference, Finality, TransactionOrReceiptId, +}; +use near_primitives::views::{ + AccessKeyPermissionView, ExecutionOutcomeWithIdView, QueryRequest, QueryResponseKind, + ReceiptView, +}; use near_primitives_core::types::ShardId; use std::path::Path; use std::time::Duration; pub(crate) struct ChainAccess { view_client: Addr, - client: Addr, } impl ChainAccess { @@ -24,7 +33,7 @@ impl ChainAccess { let node = nearcore::start_with_config(home.as_ref(), config.clone()) .context("failed to start NEAR node")?; - Ok(Self { view_client: node.view_client, client: node.client }) + Ok(Self { view_client: node.view_client }) } } @@ -34,38 +43,40 @@ impl crate::ChainAccess for ChainAccess { async fn init(&self) -> anyhow::Result<()> { let mut first_height = None; loop { - let head = self.head_height().await?; - match first_height { - Some(h) => { - if h != head { - return Ok(()); + match self.head_height().await { + Ok(head) => match first_height { + Some(h) => { + if h != head { + return Ok(()); + } } - } - None => { - first_height = Some(head); - } - } + None => { + first_height = Some(head); + } + }, + Err(ChainError::Unknown) => {} + Err(ChainError::Other(e)) => return Err(e), + }; tokio::time::sleep(Duration::from_millis(500)).await; } } - async fn head_height(&self) -> anyhow::Result { - self.client - .send( - near_client::Status { is_health_check: false, detailed: false }.with_span_context(), - ) + async fn head_height(&self) -> Result { + Ok(self + .view_client + .send(GetBlock(BlockReference::Finality(Finality::Final)).with_span_context()) .await - .unwrap() - .map(|s| s.sync_info.latest_block_height) - .map_err(Into::into) + .unwrap()? + .header + .height) } async fn get_txs( &self, height: BlockHeight, shards: &[ShardId], - ) -> Result, ChainError> { + ) -> Result, ChainError> { let mut chunks = Vec::new(); for shard_id in shards.iter() { let chunk = match self @@ -87,13 +98,65 @@ impl crate::ChainAccess for ChainAccess { }, }; if chunk.header.height_included == height { - chunks.push(ChunkTxs { + chunks.push(SourceBlock { shard_id: *shard_id, transactions: chunk.transactions.clone(), + receipts: chunk.receipts.clone(), }) } } Ok(chunks) } + + async fn get_outcome( + &self, + id: TransactionOrReceiptId, + ) -> Result { + Ok(self + .view_client + .send(GetExecutionOutcome { id }.with_span_context()) + .await + .unwrap()? + .outcome_proof) + } + + async fn get_receipt(&self, id: &CryptoHash) -> Result { + self.view_client + .send(GetReceipt { receipt_id: id.clone() }.with_span_context()) + .await + .unwrap()? + .ok_or(ChainError::Unknown) + } + + async fn get_full_access_keys( + &self, + account_id: &AccountId, + block_hash: &CryptoHash, + ) -> Result, ChainError> { + let mut ret = Vec::new(); + match self + .view_client + .send( + Query { + block_reference: BlockReference::BlockId(BlockId::Hash(block_hash.clone())), + request: QueryRequest::ViewAccessKeyList { account_id: account_id.clone() }, + } + .with_span_context(), + ) + .await + .unwrap()? + .kind + { + QueryResponseKind::AccessKeyList(l) => { + for k in l.keys { + if k.access_key.permission == AccessKeyPermissionView::FullAccess { + ret.push(k.public_key); + } + } + } + _ => unreachable!(), + }; + Ok(ret) + } } From 37bb44b913fe7e4a84698011b6dbb7657451de9c Mon Sep 17 00:00:00 2001 From: mm-near <91919554+mm-near@users.noreply.github.com> Date: Tue, 29 Nov 2022 18:14:11 +0100 Subject: [PATCH 049/188] Adding documentation to State Sync code (#8134) Adding documentation to StateSync code and removed state_sync_time that was not used. --- chain/client/src/sync/state.rs | 75 +++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/chain/client/src/sync/state.rs b/chain/client/src/sync/state.rs index 31525847173..ad33b5391b4 100644 --- a/chain/client/src/sync/state.rs +++ b/chain/client/src/sync/state.rs @@ -1,3 +1,25 @@ +//! State sync is trying to fetch the 'full state' from the peers (which can be multiple GB). +//! It happens after HeaderSync and before Body Sync (but only if the node sees that it is 'too much behind'). +//! See https://near.github.io/nearcore/architecture/how/sync.html for more detailed information. +//! Such state can be downloaded only at special heights (currently - at the beginning of the current and previous +//! epochs). +//! +//! You can do the state sync for each shard independently. +//! It starts by fetching a 'header' - that contains basic information about the state (for example its size, how +//! many parts it consists of, hash of the root etc). +//! Then it tries downloading the rest of the data in 'parts' (usually the part is around 1MB in size). +//! +//! For downloading - the code is picking the potential target nodes (all direct peers that are tracking the shard +//! (and are high enough) + validators from that epoch that were tracking the shard) +//! Then for each part that we're missing, we're 'randomly' picking a target from whom we'll request it - but we make +//! sure to not request more than MAX_STATE_PART_REQUESTS from each. +//! +//! WARNING: with the current design, we're putting quite a load on the validators - as we request a lot of data from +//! them (if you assume that we have 100 validators and 30 peers - we send 100/130 of requests to validators). +//! Currently validators defend against it, by having a rate limiters - but we should improve the algorithm +//! here to depend more on local peers instead. +//! + use near_chain::{near_chain_primitives, Error}; use std::collections::HashMap; use std::ops::Add; @@ -47,6 +69,7 @@ pub enum StateSyncResult { } struct PendingRequestStatus { + /// Number of parts that are in progress (we requested them from a given peer but didn't get the answer yet). missing_parts: usize, wait_until: DateTime, } @@ -77,13 +100,14 @@ fn make_account_or_peer_id_or_hash( pub struct StateSync { network_adapter: Arc, - state_sync_time: HashMap>, last_time_block_requested: Option>, last_part_id_requested: HashMap<(AccountOrPeerIdOrHash, ShardId), PendingRequestStatus>, /// Map from which part we requested to whom. requested_target: lru::LruCache<(u64, CryptoHash), AccountOrPeerIdOrHash>, + /// Timeout (set in config - by default to 60 seconds) is used to figure out how long we should wait + /// for the answer from the other node before giving up. timeout: Duration, /// Maps shard_id to result of applying downloaded state @@ -97,7 +121,6 @@ impl StateSync { pub fn new(network_adapter: Arc, timeout: TimeDuration) -> Self { StateSync { network_adapter, - state_sync_time: Default::default(), last_time_block_requested: None, last_part_id_requested: Default::default(), requested_target: lru::LruCache::new(MAX_PENDING_PART as usize), @@ -107,7 +130,7 @@ impl StateSync { } } - pub fn sync_block_status( + fn sync_block_status( &mut self, prev_hash: &CryptoHash, chain: &Chain, @@ -140,7 +163,7 @@ impl StateSync { // and therefore whether the client needs to update its // `sync_status`. The second indicates whether state sync is // finished, in which case the client will transition to block sync - pub fn sync_shards_status( + fn sync_shards_status( &mut self, me: &Option, sync_hash: CryptoHash, @@ -194,10 +217,13 @@ impl StateSync { let mut this_done = false; match &shard_sync_download.status { ShardSyncStatus::StateDownloadHeader => { + // StateDownloadHeader is the first step. We want to fetch the basic information about the state (its size, hash etc). if shard_sync_download.downloads[0].done { 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); + // If the header was downloaded succesfully - move to phase 2 (downloading parts). + // Create the vector with entry for each part. *shard_sync_download = ShardSyncDownload { downloads: vec![ DownloadStatus { @@ -218,6 +244,7 @@ impl StateSync { let prev = shard_sync_download.downloads[0].prev_update_time; let error = shard_sync_download.downloads[0].error; download_timeout = now - prev > self.timeout; + // Retry in case of timeout or failure. if download_timeout || error { shard_sync_download.downloads[0].run_me.store(true, Ordering::SeqCst); shard_sync_download.downloads[0].error = false; @@ -229,6 +256,7 @@ impl StateSync { } } ShardSyncStatus::StateDownloadParts => { + // Step 2 - download all the parts (each part is usually around 1MB). let mut parts_done = true; for part_download in shard_sync_download.downloads.iter_mut() { if !part_download.done { @@ -236,6 +264,7 @@ impl StateSync { let prev = part_download.prev_update_time; let error = part_download.error; let part_timeout = now - prev > self.timeout; + // Retry parts that failed. if part_timeout || error { download_timeout |= part_timeout; part_download.run_me.store(true, Ordering::SeqCst); @@ -247,6 +276,7 @@ impl StateSync { } } } + // If all parts are done - we can move towards scheduling. if parts_done { *shard_sync_download = ShardSyncDownload { downloads: vec![], @@ -258,6 +288,9 @@ impl StateSync { 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); + // Now apply all the parts to the chain / runtime. + // TODO: not sure why this has to happen only after all the parts were downloaded - + // as we could have done this in parallel after getting each part. match chain.schedule_apply_state_parts( shard_id, sync_hash, @@ -280,6 +313,8 @@ impl StateSync { } } ShardSyncStatus::StateDownloadApplying => { + // 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 { match chain.set_state_finalize(shard_id, sync_hash, result) { @@ -313,12 +348,14 @@ impl StateSync { 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)?; + // 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, } } else { + // If there is no layout change - we're done. *shard_sync_download = ShardSyncDownload { downloads: vec![], status: ShardSyncStatus::StateSyncDone, @@ -407,10 +444,12 @@ impl StateSync { Ok((update_sync_status, all_done)) } + // Called by the client actor, when it finished applying all the downloaded parts. pub fn set_apply_result(&mut self, shard_id: ShardId, apply_result: Result<(), Error>) { self.state_parts_apply_results.insert(shard_id, apply_result); } + // Called by the client actor, when it finished splitting the state. pub fn set_split_result( &mut self, shard_id: ShardId, @@ -449,6 +488,7 @@ impl StateSync { shard_id: ShardId, sync_hash: CryptoHash, ) { + // FIXME: something is wrong - the index should have a shard_id too. self.requested_target.put((part_id, sync_hash), target.clone()); let timeout = self.timeout; @@ -460,6 +500,7 @@ impl StateSync { .or_insert_with(|| PendingRequestStatus::new(timeout)); } + // Function called when our node receives the network response with a part. pub fn received_requested_part( &mut self, part_id: u64, @@ -467,6 +508,7 @@ impl StateSync { sync_hash: CryptoHash, ) { let key = (part_id, sync_hash); + // Check that it came from the target that we requested it from. if let Some(target) = self.requested_target.get(&key) { if self.last_part_id_requested.get_mut(&(target.clone(), shard_id)).map_or( false, @@ -509,16 +551,21 @@ impl StateSync { shard_id, false, ) { + // If we are one of the validators (me is not None) - then make sure that we don't try to send request to ourselves. if me.as_ref().map(|me| me != account_id).unwrap_or(true) { Some(AccountOrPeerIdOrHash::AccountId(account_id.clone())) } else { None } } else { + // This validator doesn't track the shard - ignore. None } }) .chain(highest_height_peers.iter().filter_map(|peer| { + // Select peers that are high enough (if they are syncing themselves, they might not have the data that we want) + // and that are tracking the shard. + // TODO: possible optimization - simply select peers that have height greater than the epoch start that we're asking for. if peer.tracked_shards.contains(&shard_id) { Some(AccountOrPeerIdOrHash::PeerId(peer.peer_info.id.clone())) } else { @@ -526,6 +573,7 @@ impl StateSync { } })) .filter(|candidate| { + // If we still have a pending request from this node - don't add another one. !self.last_part_id_requested.contains_key(&(candidate.clone(), shard_id)) }) .collect::>()) @@ -552,6 +600,7 @@ impl StateSync { )?; if possible_targets.is_empty() { + // In most cases it means that all the targets are currently busy (that we have a pending request with them). return Ok(shard_sync_download); } @@ -588,13 +637,17 @@ impl StateSync { ); } ShardSyncStatus::StateDownloadParts => { + // We'll select all the 'highest' peers + validators as candidates (exluding those that gave us timeout in the past). + // And for each one of them, we'll ask for up to 16 (MAX_STATE_PART_REQUEST) parts. let possible_targets_sampler = SamplerLimited::new(possible_targets, MAX_STATE_PART_REQUEST); // Iterate over all parts that needs to be requested (i.e. download.run_me is true). // Parts are ordered such that its index match its part_id. // Finally, for every part that needs to be requested it is selected one peer (target) randomly - // to request the part from + // to request the part from. + // IMPORTANT: here we use 'zip' with possible_target_sampler - which is limited. So at any moment we'll not request more than + // possible_targets.len() * MAX_STATE_PART_REQUEST parts. for ((part_id, download), target) in new_shard_sync_download .downloads .iter_mut() @@ -623,6 +676,9 @@ impl StateSync { .with_span_context(), ) .then(move |result| { + // TODO: possible optimization - in the current code, even if one of the targets it not present in the network graph + // (so we keep getting RouteNotFound) - we'll still keep trying to assign parts to it. + // Fortunately only once every 60 seconds (timeout value). if let Ok(NetworkResponses::RouteNotFound) = result.map(|f| f.as_network_response()) { @@ -640,6 +696,10 @@ impl StateSync { Ok(new_shard_sync_download) } + /// The main 'step' function that should be called periodically to check and update the sync process. + /// The current state/progress information is mostly kept within 'new_shard_sync' object. + /// + /// Returns the state of the sync. pub fn run( &mut self, me: &Option, @@ -648,6 +708,7 @@ impl StateSync { chain: &mut Chain, runtime_adapter: &Arc, highest_height_peers: &[HighestHeightPeerInfo], + // Shards to sync. tracking_shards: Vec, state_parts_task_scheduler: &dyn Fn(ApplyStatePartsRequest), state_split_scheduler: &dyn Fn(StateSplitRequest), @@ -657,6 +718,9 @@ impl StateSync { let prev_hash = *chain.get_block_header(&sync_hash)?.prev_hash(); let now = Clock::utc(); + // FIXME: it checks if the block exists.. but I have no idea why.. + // seems that we don't really use this block in case of catchup - we use it only for state sync. + // Seems it is related to some bug with block getting orphaned after state sync? but not sure. let (request_block, have_block) = self.sync_block_status(&prev_hash, chain, now)?; if tracking_shards.is_empty() { @@ -684,7 +748,6 @@ impl StateSync { )?; if have_block && all_done { - self.state_sync_time.clear(); return Ok(StateSyncResult::Completed); } From 82b5ad4795b635c4b0559d20220c87694196ffba Mon Sep 17 00:00:00 2001 From: nikurt <86772482+nikurt@users.noreply.github.com> Date: Tue, 29 Nov 2022 18:26:27 +0100 Subject: [PATCH 050/188] Tool for generating State Parts requests (#8128) This tool lets us generate controlled state parts requests, to check how long it takes to obtain the corresponding state parts from a running node. --- Cargo.lock | 18 +++ Cargo.toml | 3 +- chain/network/src/raw/connection.rs | 29 +++- chain/network/src/raw/tests.rs | 63 +++++++++ chain/network/src/testonly/fake_client.rs | 27 ++-- neard/Cargo.toml | 9 +- neard/src/cli.rs | 7 + tools/ping/Cargo.toml | 1 - tools/ping/src/cli.rs | 9 +- tools/ping/src/lib.rs | 1 + tools/state-parts/Cargo.toml | 20 +++ tools/state-parts/src/cli.rs | 121 ++++++++++++++++ tools/state-parts/src/lib.rs | 164 ++++++++++++++++++++++ 13 files changed, 452 insertions(+), 20 deletions(-) create mode 100644 tools/state-parts/Cargo.toml create mode 100644 tools/state-parts/src/cli.rs create mode 100644 tools/state-parts/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index afbdddf616d..347992844a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3523,6 +3523,23 @@ dependencies = [ name = "near-stable-hasher" version = "0.0.0" +[[package]] +name = "near-state-parts" +version = "0.0.0" +dependencies = [ + "anyhow", + "chrono", + "clap 3.1.18", + "near-jsonrpc", + "near-network", + "near-o11y", + "near-ping", + "near-primitives", + "once_cell", + "tokio", + "tracing", +] + [[package]] name = "near-stdx" version = "0.0.0" @@ -3767,6 +3784,7 @@ dependencies = [ "near-ping", "near-primitives", "near-rust-allocator-proxy", + "near-state-parts", "near-store", "nearcore", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index a8ae607527f..d12adb67eca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,8 +52,9 @@ members = [ "tools/restaked", "tools/rpctypegen/core", "tools/rpctypegen/macro", - "tools/state-viewer", "tools/speedy_sync", + "tools/state-parts", + "tools/state-viewer", "tools/storage-usage-delta-calculator", "tools/themis", "utils/mainnet-res", diff --git a/chain/network/src/raw/connection.rs b/chain/network/src/raw/connection.rs index 39fcd0271b2..7754eb5813d 100644 --- a/chain/network/src/raw/connection.rs +++ b/chain/network/src/raw/connection.rs @@ -3,13 +3,14 @@ use crate::network_protocol::{ PeerMessage, Ping, RawRoutedMessage, RoutedMessageBody, }; use crate::time::{Duration, Instant, Utc}; +use crate::types::StateResponseInfo; use bytes::buf::{Buf, BufMut}; use bytes::BytesMut; use near_crypto::{KeyType, SecretKey}; use near_primitives::block::GenesisId; use near_primitives::hash::CryptoHash; use near_primitives::network::PeerId; -use near_primitives::types::{AccountId, BlockHeight, EpochId}; +use near_primitives::types::{AccountId, BlockHeight, EpochId, ShardId}; use near_primitives::version::{ProtocolVersion, PROTOCOL_VERSION}; use std::io; use std::net::SocketAddr; @@ -32,9 +33,11 @@ pub struct Connection { /// The types of messages it's possible to receive from a `Peer`. Any PeerMessage /// we receive that doesn't fit one of these will just be logged and dropped. +#[derive(Debug)] pub enum ReceivedMessage { AnnounceAccounts(Vec<(AccountId, PeerId, EpochId)>), Pong { nonce: u64, source: PeerId }, + VersionedStateResponse(StateResponseInfo), } impl TryFrom for ReceivedMessage { @@ -47,6 +50,9 @@ impl TryFrom for ReceivedMessage { RoutedMessageBody::Pong(p) => { Ok(Self::Pong { nonce: p.nonce, source: p.source.clone() }) } + RoutedMessageBody::VersionedStateResponse(state_response_info) => { + Ok(Self::VersionedStateResponse(state_response_info.clone())) + } _ => Err(()), }, PeerMessage::SyncRoutingTable(r) => Ok(Self::AnnounceAccounts( @@ -265,4 +271,25 @@ impl Connection { Ok(()) } + + /// Try to send a StateRequestPart message to the given target, with the given nonce and ttl + pub async fn send_state_part_request( + &mut self, + target: &PeerId, + shard_id: ShardId, + block_hash: CryptoHash, + part_id: u64, + ttl: u8, + ) -> anyhow::Result<()> { + let body = RoutedMessageBody::StateRequestPart(shard_id, block_hash, part_id); + let msg = RawRoutedMessage { target: PeerIdOrHash::PeerId(target.clone()), body }.sign( + &self.secret_key, + ttl, + Some(Utc::now_utc()), + ); + + self.write_message(&PeerMessage::Routed(Box::new(msg))).await?; + + Ok(()) + } } diff --git a/chain/network/src/raw/tests.rs b/chain/network/src/raw/tests.rs index d7fa5337481..0ce46738ed3 100644 --- a/chain/network/src/raw/tests.rs +++ b/chain/network/src/raw/tests.rs @@ -3,6 +3,7 @@ use crate::raw; use crate::testonly; use crate::time; use near_o11y::testonly::init_test_logger; +use near_primitives::hash::CryptoHash; use std::sync::Arc; #[tokio::test] @@ -61,3 +62,65 @@ async fn test_raw_conn_pings() { } } } + +#[tokio::test] +async fn test_raw_conn_state_parts() { + init_test_logger(); + let mut rng = testonly::make_rng(33955575545); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + let cfg = chain.make_config(rng); + let peer_id = cfg.node_id(); + let addr = cfg.node_addr.unwrap(); + let genesis_id = chain.genesis_id.clone(); + let _pm = crate::peer_manager::testonly::start( + clock.clock(), + near_store::db::TestDB::new(), + cfg, + chain, + ) + .await; + + let mut conn = raw::Connection::connect( + addr, + peer_id.clone(), + None, + &genesis_id.chain_id, + genesis_id.hash, + 0, + time::Duration::SECOND, + ) + .await + .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_state_part_request(&peer_id, 0, block_hash, part_id, ttl).await.unwrap(); + } + + let mut part_id_received = -1i64; + loop { + let (msg, _timestamp) = conn.recv().await.unwrap(); + if let raw::ReceivedMessage::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; + } + } + } +} diff --git a/chain/network/src/testonly/fake_client.rs b/chain/network/src/testonly/fake_client.rs index e515dfa7937..36c41d49644 100644 --- a/chain/network/src/testonly/fake_client.rs +++ b/chain/network/src/testonly/fake_client.rs @@ -4,27 +4,28 @@ use crate::network_protocol::{ StateResponseInfo, }; use crate::sink::Sink; -use crate::types::{NetworkInfo, ReasonForBan}; +use crate::types::{NetworkInfo, ReasonForBan, StateResponseInfoV2}; use near_primitives::block::{Approval, Block, BlockHeader}; use near_primitives::challenge::Challenge; use near_primitives::hash::CryptoHash; use near_primitives::network::{AnnounceAccount, PeerId}; use near_primitives::sharding::{ChunkHash, PartialEncodedChunk, PartialEncodedChunkPart}; +use near_primitives::syncing::{ShardStateSyncResponse, ShardStateSyncResponseV2}; use near_primitives::transaction::SignedTransaction; use near_primitives::types::{AccountId, EpochId, ShardId}; use near_primitives::views::FinalExecutionOutcomeView; #[derive(Debug, PartialEq, Eq, Clone)] pub enum Event { - BlockRequest(CryptoHash), + AnnounceAccount(Vec<(AnnounceAccount, Option)>), Block(Block), - BlockHeadersRequest(Vec), BlockHeaders(Vec), + BlockHeadersRequest(Vec), + BlockRequest(CryptoHash), + Challenge(Challenge), Chunk(Vec), ChunkRequest(ChunkHash), Transaction(SignedTransaction), - Challenge(Challenge), - AnnounceAccount(Vec<(AnnounceAccount, Option)>), } pub(crate) struct Fake { @@ -53,11 +54,19 @@ impl client::Client for Fake { async fn state_request_part( &self, - _shard_id: ShardId, - _sync_hash: CryptoHash, - _part_id: u64, + shard_id: ShardId, + sync_hash: CryptoHash, + part_id: u64, ) -> Result, ReasonForBan> { - unimplemented!(); + let part = Some((part_id, vec![])); + let state_response = + ShardStateSyncResponse::V2(ShardStateSyncResponseV2 { header: None, part }); + let result = Some(StateResponseInfo::V2(StateResponseInfoV2 { + shard_id, + sync_hash, + state_response, + })); + Ok(result) } async fn state_response(&self, _info: StateResponseInfo) { diff --git a/neard/Cargo.toml b/neard/Cargo.toml index 39c9bba9508..f2abf708a3b 100644 --- a/neard/Cargo.toml +++ b/neard/Cargo.toml @@ -33,17 +33,18 @@ tracing.workspace = true nearcore = { path = "../nearcore" } near-amend-genesis = { path = "../tools/amend-genesis" } near-chain-configs = { path = "../core/chain-configs" } +near-cold-store-tool = { path = "../tools/cold-store", package = "cold-store-tool" } near-dyn-configs = { path = "../core/dyn-configs" } near-jsonrpc-primitives = { path = "../chain/jsonrpc-primitives" } -near-network = { path = "../chain/network" } near-mirror = { path = "../tools/mirror" } -near-primitives = { path = "../core/primitives" } +near-network = { path = "../chain/network" } +near-o11y = { path = "../core/o11y" } near-performance-metrics = { path = "../utils/near-performance-metrics" } near-ping = { path = "../tools/ping" } +near-primitives = { path = "../core/primitives" } +near-state-parts = { path = "../tools/state-parts" } near-state-viewer = { path = "../tools/state-viewer", package = "state-viewer" } -near-cold-store-tool = { path = "../tools/cold-store", package = "cold-store-tool" } near-store = { path = "../core/store" } -near-o11y = { path = "../core/o11y" } [build-dependencies] anyhow.workspace = true diff --git a/neard/src/cli.rs b/neard/src/cli.rs index 8b4f0042a7f..b48b0ba91bb 100644 --- a/neard/src/cli.rs +++ b/neard/src/cli.rs @@ -20,6 +20,7 @@ use near_ping::PingCommand; use near_primitives::hash::CryptoHash; use near_primitives::merkle::compute_root_from_path; use near_primitives::types::{Gas, NumSeats, NumShards}; +use near_state_parts::cli::StatePartsCommand; use near_state_viewer::StateViewerSubCommand; use near_store::db::RocksDB; use near_store::Mode; @@ -115,6 +116,9 @@ impl NeardCmd { NeardSubCommand::ColdStore(cmd) => { cmd.run(&home_dir); } + NeardSubCommand::StateParts(cmd) => { + cmd.run()?; + } }; Ok(()) } @@ -218,6 +222,9 @@ pub(super) enum NeardSubCommand { #[cfg(feature = "cold_store")] /// Testing tool for cold storage ColdStore(ColdStoreCommand), + + /// Connects to a NEAR node and sends state parts requests after the handshake is completed. + StateParts(StatePartsCommand), } #[derive(Parser)] diff --git a/tools/ping/Cargo.toml b/tools/ping/Cargo.toml index 8511445863b..a49063c744a 100644 --- a/tools/ping/Cargo.toml +++ b/tools/ping/Cargo.toml @@ -3,7 +3,6 @@ name = "near-ping" version = "0.0.0" authors.workspace = true publish = false -rust-version.workspace = true edition = "2021" [dependencies] diff --git a/tools/ping/src/cli.rs b/tools/ping/src/cli.rs index 05933d763b9..601dec0ba48 100644 --- a/tools/ping/src/cli.rs +++ b/tools/ping/src/cli.rs @@ -90,12 +90,13 @@ fn display_stats(stats: &mut [(crate::PeerIdentifier, crate::PingStats)], peer_i } } -struct ChainInfo { - chain_id: &'static str, - genesis_hash: CryptoHash, +// TODO: Refactor this struct into a separate crate. +pub struct ChainInfo { + pub chain_id: &'static str, + pub genesis_hash: CryptoHash, } -static CHAIN_INFO: &[ChainInfo] = &[ +pub static CHAIN_INFO: &[ChainInfo] = &[ ChainInfo { chain_id: "mainnet", genesis_hash: CryptoHash([ diff --git a/tools/ping/src/lib.rs b/tools/ping/src/lib.rs index faf55deb4d2..c4713282ea8 100644 --- a/tools/ping/src/lib.rs +++ b/tools/ping/src/lib.rs @@ -344,6 +344,7 @@ fn handle_message( ReceivedMessage::AnnounceAccounts(a) => { app_info.add_announce_accounts(a); } + _ => {} }; Ok(()) } diff --git a/tools/state-parts/Cargo.toml b/tools/state-parts/Cargo.toml new file mode 100644 index 00000000000..9eb57ba588d --- /dev/null +++ b/tools/state-parts/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "near-state-parts" +version = "0.0.0" +authors.workspace = true +publish = false +edition.workspace = true + +[dependencies] +anyhow.workspace = true +chrono.workspace = true +clap.workspace = true +once_cell.workspace = true +tokio.workspace = true +tracing.workspace = true + +near-jsonrpc = { path = "../../chain/jsonrpc" } +near-network = { path = "../../chain/network" } +near-o11y = { path = "../../core/o11y" } +near-ping = { path = "../ping" } +near-primitives = { path = "../../core/primitives" } diff --git a/tools/state-parts/src/cli.rs b/tools/state-parts/src/cli.rs new file mode 100644 index 00000000000..eb6656309d3 --- /dev/null +++ b/tools/state-parts/src/cli.rs @@ -0,0 +1,121 @@ +use clap::Parser; +use near_network::types::PeerInfo; +use near_ping::cli::CHAIN_INFO; +use near_primitives::hash::CryptoHash; +use near_primitives::types::ShardId; +use std::str::FromStr; + +#[derive(Parser)] +pub struct StatePartsCommand { + /// The hash of the first block of an epoch of which we are requesting state. + #[clap(long)] + block_hash: CryptoHash, + + /// shard_id selecting the shard, state of which we are requesting. + #[clap(long)] + shard_id: ShardId, + + /// Chain id of the peer. + #[clap(long)] + chain_id: String, + + #[clap(long)] + /// genesis hash to use in the Handshake we send. This must be provided if --chain-id + /// is not one of "mainnet", "testnet" or "shardnet" + genesis_hash: Option, + + #[clap(long)] + /// head height to use in the Handshake we send. This must be provided if --chain-id + /// is not one of "mainnet", "testnet" or "shardnet" + head_height: Option, + + /// Protocol version to advertise in our handshake + #[clap(long)] + protocol_version: Option, + + /// node public key and socket address in the format {pub key}@{socket addr}. e.g.: + /// ed25519:7PGseFbWxvYVgZ89K1uTJKYoKetWs7BJtbyXDzfbAcqX@127.0.0.1:24567 + #[clap(long)] + peer: String, + + /// ttl to set on our Routed messages + #[clap(long, default_value = "100")] + ttl: u8, + + /// milliseconds to wait between sending state part requests + #[clap(long, default_value = "1000")] + request_frequency_millis: u64, + + /// number of seconds to wait for incoming data before timing out + #[clap(long)] + recv_timeout_seconds: Option, + + /// Starting part id for the state requests. + #[clap(long, default_value = "0")] + start_part_id: u64, + + /// Number of parts in the state of the shard. + /// Assuming the tool doesn't have a valid DB and can't determine the number automatically. + #[clap(long)] + num_parts: u64, +} + +impl StatePartsCommand { + pub fn run(&self) -> anyhow::Result<()> { + tracing::warn!(target: "state-parts", "the state-parts command is not stable, and may be removed or changed arbitrarily at any time"); + + let mut chain_info = None; + for info in CHAIN_INFO.iter() { + if &info.chain_id == &self.chain_id { + chain_info = Some(info); + break; + } + } + + let genesis_hash = if let Some(h) = &self.genesis_hash { + match CryptoHash::from_str(&h) { + Ok(h) => h, + Err(e) => { + anyhow::bail!("Could not parse --genesis-hash {}: {:?}", &h, e) + } + } + } else { + match chain_info { + Some(chain_info) => chain_info.genesis_hash, + None => anyhow::bail!( + "--genesis-hash not given, and genesis hash for --chain-id {} not known", + &self.chain_id + ), + } + }; + + let peer = match PeerInfo::from_str(&self.peer) { + Ok(p) => p, + Err(e) => anyhow::bail!("Could not parse --peer {}: {:?}", &self.peer, e), + }; + if peer.addr.is_none() { + anyhow::bail!("--peer should be in the form [public key]@[socket addr]"); + } + tracing::info!(target: "state-parts", ?genesis_hash, ?peer); + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(async move { + crate::state_parts_from_node( + self.block_hash, + self.shard_id, + &self.chain_id, + genesis_hash, + self.head_height.unwrap_or(0), + self.protocol_version, + peer.id.clone(), + peer.addr.clone().unwrap(), + self.ttl, + self.request_frequency_millis, + self.recv_timeout_seconds.unwrap_or(5), + self.start_part_id, + self.num_parts, + ) + .await?; + Ok(()) + }) + } +} diff --git a/tools/state-parts/src/lib.rs b/tools/state-parts/src/lib.rs new file mode 100644 index 00000000000..e336669e17d --- /dev/null +++ b/tools/state-parts/src/lib.rs @@ -0,0 +1,164 @@ +use anyhow::Context; +use near_network::raw::{ConnectError, Connection, ReceivedMessage}; +use near_network::time; +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 std::collections::HashMap; +use std::net::SocketAddr; + +pub mod cli; + +struct AppInfo { + pub requests_sent: HashMap, +} + +impl AppInfo { + fn new() -> Self { + Self { requests_sent: HashMap::new() } + } +} + +fn handle_message( + app_info: &mut AppInfo, + msg: &ReceivedMessage, + received_at: time::Instant, +) -> anyhow::Result<()> { + match &msg { + ReceivedMessage::VersionedStateResponse(response) => { + let shard_id = response.shard_id(); + let sync_hash = response.sync_hash(); + let state_response = response.clone().take_state_response(); + let part_id = state_response.part_id(); + let duration = if let Some(part_id) = part_id { + let duration = app_info + .requests_sent + .get(&part_id) + .map(|sent| (sent.elapsed() - received_at.elapsed()).as_seconds_f64()); + app_info.requests_sent.remove(&part_id); + duration + } else { + None + }; + tracing::info!( + shard_id, + ?sync_hash, + ?part_id, + ?duration, + "Received VersionedStateResponse" + ); + } + _ => {} + }; + Ok(()) +} + +#[derive(Debug)] +struct PeerIdentifier { + account_id: Option, + peer_id: PeerId, +} + +impl std::fmt::Display for PeerIdentifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + match &self.account_id { + Some(a) => a.fmt(f), + None => self.peer_id.fmt(f), + } + } +} + +async fn state_parts_from_node( + block_hash: CryptoHash, + shard_id: ShardId, + chain_id: &str, + genesis_hash: CryptoHash, + head_height: BlockHeight, + protocol_version: Option, + peer_id: PeerId, + peer_addr: SocketAddr, + ttl: u8, + request_frequency_millis: u64, + recv_timeout_seconds: u32, + start_part_id: u64, + num_parts: u64, +) -> anyhow::Result<()> { + assert!(start_part_id < num_parts && num_parts > 0, "{}/{}", start_part_id, num_parts); + let mut app_info = AppInfo::new(); + + let mut peer = match Connection::connect( + peer_addr, + peer_id.clone(), + protocol_version, + chain_id, + genesis_hash, + head_height, + time::Duration::seconds(recv_timeout_seconds.into())).await { + Ok(p) => p, + Err(ConnectError::HandshakeFailure(reason)) => { + match reason { + HandshakeFailureReason::ProtocolVersionMismatch { version, oldest_supported_version } => anyhow::bail!( + "Received Handshake Failure: {:?}. Try running again with --protocol-version between {} and {}", + reason, oldest_supported_version, version + ), + HandshakeFailureReason::GenesisMismatch(_) => anyhow::bail!( + "Received Handshake Failure: {:?}. Try running again with --chain-id and --genesis-hash set to these values.", + reason, + ), + HandshakeFailureReason::InvalidTarget => anyhow::bail!( + "Received Handshake Failure: {:?}. Is the public key given with --peer correct?", + reason, + ), + } + } + Err(e) => { + anyhow::bail!("Error connecting to {:?}: {}", peer_addr, e); + } + }; + tracing::info!(target: "state-parts", ?peer_addr, ?peer_id, "Connected to peer"); + + let next_request = tokio::time::sleep(std::time::Duration::ZERO); + tokio::pin!(next_request); + + let mut result = Ok(()); + let mut part_id = start_part_id; + loop { + tokio::select! { + _ = &mut next_request => { + let target = peer_id.clone(); + tracing::info!(target: "state-parts", ?target, shard_id, ?block_hash, part_id, ttl, "Sending a request"); + result = peer.send_state_part_request(&target, shard_id, block_hash, part_id, ttl).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() { + break; + } + next_request.as_mut().reset(tokio::time::Instant::now() + std::time::Duration::from_millis(request_frequency_millis)); + part_id = (part_id + 1) % num_parts; + } + res = peer.recv() => { + let (msg, first_byte_time) = match res { + Ok(x) => x, + Err(e) => { + result = Err(e).context("Failed receiving messages"); + break; + } + }; + result = handle_message( + &mut app_info, + &msg, + first_byte_time.try_into().unwrap(), + ); + if result.is_err() { + break; + } + } + _ = tokio::signal::ctrl_c() => { + break; + } + } + } + result +} From d14ef66bb05ce1ebd8cfd63c702e34f8ae0e4eba Mon Sep 17 00:00:00 2001 From: Marcelo Diop-Gonzalez Date: Tue, 29 Nov 2022 13:47:46 -0500 Subject: [PATCH 051/188] fix(migrations): make sure to call finish() on batched updates (#8127) Since this wasn't called at the end of migrate_32_to_33(), any changes still in memory would have been dropped. Luckily this DB migration hasn't made it to any stable version yet, but anyone who's been running unstable/master versions will probably have missing outcomes in the database --- core/store/src/migrations.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/core/store/src/migrations.rs b/core/store/src/migrations.rs index 8397a80237c..92417dc4caf 100644 --- a/core/store/src/migrations.rs +++ b/core/store/src/migrations.rs @@ -263,6 +263,7 @@ pub fn migrate_32_to_33(storage: &crate::NodeStorage) -> anyhow::Result<()> { )?; } } + update.finish()?; let mut delete_old_update = store.store_update(); delete_old_update.delete_all(DBCol::_TransactionResult); delete_old_update.commit()?; From 367962b5cdc45082b0be7d46a6b80fe1f99a7ae1 Mon Sep 17 00:00:00 2001 From: Marcelo Diop-Gonzalez Date: Wed, 30 Nov 2022 11:59:42 -0500 Subject: [PATCH 052/188] feat(mirror): send extra txs for create accounts from function calls (#8139) this is mostly the same as what we want to do for add key actions that come from function calls, but the big difference is that in the case of an add key action adding some key that we want to map to the target chain, we can always send an extra add key tx with the mapped key at any time. But with create accounts, once the account is created, we won't have the opportunity to add keys because the only key available for that account is the original source chain key (which we dont control) so to get around this, we will look thru the upcoming blocks, and for any create accounts that result from function calls, we will send our own create account tx matching it (but with the key mapped) 5 blocks before we expect to see the original tx/receipt succeed. Then if all goes well our tx is the one that makes it on chain, and the original one will fail, and we can send txs for the new account as usual --- pytest/tools/mirror/contract/src/lib.rs | 13 +- pytest/tools/mirror/mirror_utils.py | 35 ++- tools/mirror/src/chain_tracker.rs | 83 ++++-- tools/mirror/src/lib.rs | 367 ++++++++++++++++++------ tools/mirror/src/offline.rs | 53 ++++ tools/mirror/src/online.rs | 71 ++++- 6 files changed, 494 insertions(+), 128 deletions(-) diff --git a/pytest/tools/mirror/contract/src/lib.rs b/pytest/tools/mirror/contract/src/lib.rs index db6555687d4..fe4c31790b7 100644 --- a/pytest/tools/mirror/contract/src/lib.rs +++ b/pytest/tools/mirror/contract/src/lib.rs @@ -1,7 +1,7 @@ -//! Contract that adds keys +//! Contract that adds keys and creates accounts use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; -use near_sdk::{env, near_bindgen, Promise, PublicKey}; +use near_sdk::{env, near_bindgen, AccountId, Promise, PublicKey}; use std::str::FromStr; #[near_bindgen] @@ -24,4 +24,13 @@ impl KeyAdder { Self::ext(signer_id).add_key(public_key) } } + + #[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()) + } } diff --git a/pytest/tools/mirror/mirror_utils.py b/pytest/tools/mirror/mirror_utils.py index e3c36b9926a..3b547f413ea 100644 --- a/pytest/tools/mirror/mirror_utils.py +++ b/pytest/tools/mirror/mirror_utils.py @@ -346,6 +346,27 @@ 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}) + args = bytearray(args, encoding='utf-8') + + actions = [ + transaction.create_function_call_action('create_account', args, 10**13, + 10**24), + transaction.create_payment_action(123) + ] + tx = transaction.sign_and_serialize_transaction('test0', nonce, actions, + block_hash, + signer_key.account_id, + signer_key.decoded_pk(), + signer_key.decoded_sk()) + res = node.send_tx(tx) + logger.info( + f'called create account contract for {account_id} {k.pk}: {res}') + return k + + # a key that we added with an AddKey tx or implicit account transfer. # just for nonce handling convenience class AddedKey: @@ -605,9 +626,19 @@ def send_traffic(near_root, source_nodes, traffic_data, callback): call_addkey(source_nodes[1], source_nodes[1].signer_key, test1_contract_key, traffic_data.nonces[1], block_hash_bytes) 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)) + 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)) + traffic_data.nonces[1] += 1 + test0_deleted_height = None test0_readded_key = None implicit_added = None @@ -655,6 +686,8 @@ def send_traffic(near_root, source_nodes, traffic_data, callback): test0_contract_key, test0_contract_extra_key, test1_contract_key, + test0_subaccount_contract_key, + test1_subaccount_contract_key, ] added_keys_send_transfers(source_nodes, keys, [ traffic_data.implicit_account.account_id(), diff --git a/tools/mirror/src/chain_tracker.rs b/tools/mirror/src/chain_tracker.rs index 0504e643d82..5253e3d813b 100644 --- a/tools/mirror/src/chain_tracker.rs +++ b/tools/mirror/src/chain_tracker.rs @@ -1,8 +1,10 @@ use crate::{ - ChainObjectId, LatestTargetNonce, MappedBlock, MappedTx, MappedTxProvenance, NonceUpdater, - ReceiptInfo, TargetChainTx, TargetNonce, TxInfo, TxOutcome, TxRef, + ChainAccess, ChainError, ChainObjectId, LatestTargetNonce, MappedBlock, MappedTx, + MappedTxProvenance, NonceUpdater, ReceiptInfo, TargetChainTx, TargetNonce, TxInfo, TxOutcome, + TxRef, }; use actix::Addr; +use anyhow::Context; use near_client::ViewClientActor; use near_crypto::PublicKey; use near_indexer::StreamerMessage; @@ -10,7 +12,7 @@ use near_indexer_primitives::IndexerTransactionWithOutcome; use near_primitives::hash::CryptoHash; use near_primitives::transaction::Transaction; use near_primitives::types::{AccountId, BlockHeight}; -use near_primitives_core::types::{Gas, Nonce, ShardId}; +use near_primitives_core::types::{Gas, Nonce}; use rocksdb::DB; use std::cmp::Ordering; use std::collections::hash_map; @@ -28,7 +30,6 @@ struct TxSendInfo { sent_at: Instant, source_height: BlockHeight, provenance: MappedTxProvenance, - source_shard_id: ShardId, source_signer_id: AccountId, source_receiver_id: AccountId, target_signer_id: Option, @@ -40,7 +41,6 @@ struct TxSendInfo { impl TxSendInfo { fn new( tx: &MappedTx, - source_shard_id: ShardId, source_height: BlockHeight, target_height: BlockHeight, now: Instant, @@ -58,7 +58,6 @@ impl TxSendInfo { }; Self { source_height, - source_shard_id: source_shard_id, provenance: tx.provenance, source_signer_id: tx.source_signer_id.clone(), source_receiver_id: tx.source_receiver_id.clone(), @@ -130,6 +129,7 @@ pub(crate) struct TxTracker { // a set of access keys who might be updated by it updater_to_keys: HashMap>, nonces: HashMap<(AccountId, PublicKey), NonceInfo>, + next_heights: VecDeque, height_queued: Option, // the reason we have these (nonempty_height_queued, height_seen, etc) is so that we can // exit after we receive the target block containing the txs we sent for the last source block. @@ -148,15 +148,50 @@ pub(crate) struct TxTracker { } impl TxTracker { - pub(crate) fn new( + // `next_heights` should show the next several valid heights in the chain, starting from + // the first block we want to send txs for. Right now we are assuming this arg is not empty when + // we unwrap() self.height_queued() in Self::next_heights() + pub(crate) fn new<'a, I>( min_block_production_delay: Duration, + next_heights: I, stop_height: Option, - ) -> Self { - Self { min_block_production_delay, stop_height, ..Default::default() } + ) -> Self + where + I: IntoIterator, + { + let next_heights = next_heights.into_iter().map(Clone::clone).collect(); + Self { min_block_production_delay, next_heights, stop_height, ..Default::default() } + } + + pub(crate) async fn next_heights( + &mut self, + source_chain: &T, + ) -> anyhow::Result<(Option, Option)> { + while self.next_heights.len() <= crate::CREATE_ACCOUNT_DELTA { + // we unwrap() the height_queued because Self::new() should have been called with + // nonempty next_heights. + let h = self + .next_heights + .iter() + .next_back() + .cloned() + .unwrap_or_else(|| self.height_queued.unwrap()); + match source_chain.get_next_block_height(h).await { + Ok(h) => self.next_heights.push_back(h), + Err(ChainError::Unknown) => break, + Err(ChainError::Other(e)) => { + return Err(e) + .with_context(|| format!("failed fetching next height after {}", h)) + } + }; + } + let next_height = self.next_heights.get(0).cloned(); + let create_account_height = self.next_heights.get(crate::CREATE_ACCOUNT_DELTA).cloned(); + Ok((next_height, create_account_height)) } - pub(crate) fn height_queued(&self) -> Option { - self.height_queued + pub(crate) fn has_stop_height(&self) -> bool { + self.stop_height.is_some() } pub(crate) fn finished(&self) -> bool { @@ -311,6 +346,8 @@ impl TxTracker { db: &DB, ) -> anyhow::Result<()> { self.height_queued = Some(block.source_height); + self.next_heights.pop_front().unwrap(); + for c in block.chunks.iter() { if !c.txs.is_empty() { self.nonempty_height_queued = Some(block.source_height); @@ -508,13 +545,7 @@ impl TxTracker { if let Some(info) = self.sent_txs.get(&tx.transaction.hash) { write!( log_message, - "source #{}{} {} signer: \"{}\"{} receiver: \"{}\"{} actions: <{}> sent {:?} ago @ target #{}\n", - info.source_height, - if s.shard_id == info.source_shard_id { - String::new() - } else { - format!(" (source shard {})", info.source_shard_id) - }, + "{} signer: \"{}\"{} receiver: \"{}\"{} actions: <{}> sent {:?} ago @ target #{}\n", info.provenance, info.source_signer_id, info.target_signer_id.as_ref().map_or(String::new(), |s| format!(" (mapped to \"{}\")", s)), @@ -751,13 +782,10 @@ impl TxTracker { return Ok(()); } - if matches!( - &tx.provenance, - MappedTxProvenance::ReceiptAddKey(_) | MappedTxProvenance::TxAddKey(_) - ) { + if tx.provenance.is_add_key() || tx.provenance.is_create_account() { tracing::debug!( - target: "mirror", "Successfully sent transaction {} for {} @ source #{} to add extra keys: {:?}", - &hash, &tx.target_tx.transaction.receiver_id, tx_ref.source_height, &tx.target_tx.transaction.actions, + target: "mirror", "Successfully sent transaction {} for {}: {:?}", + &hash, &tx.provenance, &tx.target_tx.transaction.actions, ); } let access_key = ( @@ -766,10 +794,7 @@ impl TxTracker { ); // TODO: don't keep adding txs if we're not ever finding them on chain, since we'll OOM eventually // if that happens. - self.sent_txs.insert( - hash, - TxSendInfo::new(&tx, tx_ref.shard_id, tx_ref.source_height, target_height, now), - ); + self.sent_txs.insert(hash, TxSendInfo::new(&tx, tx_ref.source_height, target_height, now)); let txs = self.txs_by_signer.entry(access_key.clone()).or_default(); if let Some(highest_nonce) = txs.iter().next_back() { @@ -1047,7 +1072,7 @@ impl TxTracker { } } } - crate::set_next_source_height(db, block.source_height + 1)?; + crate::set_last_source_height(db, block.source_height)?; for access_key in access_keys_to_remove { assert!(self.nonces.remove(&access_key).is_some()); diff --git a/tools/mirror/src/lib.rs b/tools/mirror/src/lib.rs index 327c87efbbd..fdd625cb7bb 100644 --- a/tools/mirror/src/lib.rs +++ b/tools/mirror/src/lib.rs @@ -15,14 +15,15 @@ use near_indexer::{Indexer, StreamerMessage}; use near_o11y::WithSpanContextExt; use near_primitives::hash::CryptoHash; use near_primitives::transaction::{ - Action, AddKeyAction, DeleteKeyAction, SignedTransaction, Transaction, + Action, AddKeyAction, CreateAccountAction, DeleteKeyAction, SignedTransaction, Transaction, + TransferAction, }; use near_primitives::types::{ AccountId, BlockHeight, BlockReference, Finality, TransactionOrReceiptId, }; use near_primitives::views::{ - AccessKeyView, ActionView, ExecutionOutcomeWithIdView, ExecutionStatusView, QueryRequest, - QueryResponseKind, ReceiptEnumView, ReceiptView, SignedTransactionView, + ActionView, ExecutionOutcomeWithIdView, ExecutionStatusView, QueryRequest, QueryResponseKind, + ReceiptEnumView, ReceiptView, SignedTransactionView, }; use near_primitives_core::types::{Nonce, ShardId}; use nearcore::config::NearConfig; @@ -210,22 +211,28 @@ fn delete_access_key_outcome(db: &DB, id: &ChainObjectId) -> anyhow::Result<()> )?) } -fn set_next_source_height(db: &DB, height: BlockHeight) -> anyhow::Result<()> { +fn set_last_source_height(db: &DB, height: BlockHeight) -> anyhow::Result<()> { // TODO: we should instead save something like the // (block_height, shard_id, idx_in_chunk) of the last - // transaction sent. Currently we set next_source_height after + // transaction sent. Currently we set last_source_height after // sending all of the transactions in that chunk, so if we get // SIGTERM or something in the middle of sending a batch of // txs, we'll send some that we already sent next time we // start. Not a giant problem but kind of unclean. db.put_cf( db.cf_handle(DBCol::Misc.name()).unwrap(), - "next_source_height", + "last_source_height", height.try_to_vec().unwrap(), )?; Ok(()) } +fn get_last_source_height(db: &DB) -> anyhow::Result> { + Ok(db + .get_cf(db.cf_handle(DBCol::Misc.name()).unwrap(), "last_source_height")? + .map(|v| BlockHeight::try_from_slice(&v).unwrap())) +} + struct SourceBlock { shard_id: ShardId, transactions: Vec, @@ -320,9 +327,15 @@ impl From for ChainError { #[async_trait(?Send)] trait ChainAccess { - async fn init(&self) -> anyhow::Result<()> { - Ok(()) - } + // this should return the first `num_initial_blocks` valid block heights in the + // chain after last_height. It should only return fewer than that if there are no + // more, which only happens if we're not starting a source node (--online-source is + // not given), and the last height we sent txs for is close to the HEAD of the chain + async fn init( + &self, + last_height: BlockHeight, + num_initial_blocks: usize, + ) -> anyhow::Result>; async fn head_height(&self) -> Result; @@ -332,6 +345,8 @@ trait ChainAccess { shards: &[ShardId], ) -> Result, ChainError>; + async fn get_next_block_height(&self, height: BlockHeight) -> Result; + async fn get_outcome( &self, id: TransactionOrReceiptId, @@ -379,6 +394,8 @@ fn execution_status_good(status: &ExecutionStatusView) -> bool { ) } +const CREATE_ACCOUNT_DELTA: usize = 5; + struct TxMirror { target_stream: mpsc::Receiver, source_chain_access: T, @@ -411,17 +428,54 @@ fn open_db>(home: P, config: &NearConfig) -> anyhow::Result { #[derive(Clone, Copy, Debug)] enum MappedTxProvenance { - SourceTxIndex(usize), - TxAddKey(usize), - ReceiptAddKey(usize), + MappedSourceTx(BlockHeight, ShardId, usize), + TxAddKey(BlockHeight, ShardId, usize), + ReceiptAddKey(BlockHeight, ShardId, usize), + TxCreateAccount(BlockHeight, ShardId, usize), + ReceiptCreateAccount(BlockHeight, ShardId, usize), +} + +impl MappedTxProvenance { + fn is_create_account(&self) -> bool { + matches!( + self, + MappedTxProvenance::TxCreateAccount(_, _, _) + | MappedTxProvenance::ReceiptCreateAccount(_, _, _) + ) + } + + fn is_add_key(&self) -> bool { + matches!( + self, + MappedTxProvenance::TxAddKey(_, _, _) | MappedTxProvenance::ReceiptAddKey(_, _, _) + ) + } } impl std::fmt::Display for MappedTxProvenance { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::SourceTxIndex(idx) => write!(f, "tx #{}", idx), - Self::TxAddKey(idx) => write!(f, "extra AddKey for tx #{}", idx), - Self::ReceiptAddKey(idx) => write!(f, "extra AddKey for receipt #{}", idx), + Self::MappedSourceTx(height, shard_id, idx) => { + write!(f, "source #{} shard {} tx #{}", height, shard_id, idx) + } + Self::TxAddKey(height, shard_id, idx) => { + write!(f, "extra AddKey for source #{} shard {} tx #{}", height, shard_id, idx) + } + Self::ReceiptAddKey(height, shard_id, idx) => { + write!(f, "extra AddKey for source #{} shard {} receipt #{}", height, shard_id, idx) + } + Self::TxCreateAccount(height, shard_id, idx) => write!( + f, + "extra CreateAccount for source #{} shard {} tx #{}", + height, shard_id, idx + ), + Self::ReceiptCreateAccount(height, shard_id, idx) => { + write!( + f, + "extra CreateAccount for source #{} shard {} receipt #{}", + height, shard_id, idx + ) + } } } } @@ -827,15 +881,6 @@ impl TxMirror { }) } - fn get_next_source_height(&mut self) -> anyhow::Result { - let height = - self.db.get_cf(self.db.cf_handle(DBCol::Misc.name()).unwrap(), "next_source_height")?; - match height { - Some(h) => Ok(BlockHeight::try_from_slice(&h).unwrap()), - None => Ok(self.target_genesis_height), - } - } - async fn send_transactions(&mut self, block: &mut MappedBlock) -> anyhow::Result<()> { for chunk in block.chunks.iter_mut() { for tx in chunk.txs.iter_mut() { @@ -862,8 +907,8 @@ impl TxMirror { // that some other instance of this code ran and made progress already. For now we can assume // only once instance of this code will run, but this is the place to detect if that's not the case. tracing::error!( - target: "mirror", "Tried to send an invalid tx for ({}, {:?}) from source #{} shard {} {}: {:?}", - &tx.target_tx.transaction.signer_id, &tx.target_tx.transaction.public_key, block.source_height, chunk.shard_id, &tx.provenance, e + target: "mirror", "Tried to send an invalid tx for ({}, {:?}) from {}: {:?}", + &tx.target_tx.transaction.signer_id, &tx.target_tx.transaction.public_key, &tx.provenance, e ); crate::metrics::TRANSACTIONS_SENT .with_label_values(&["invalid"]) @@ -871,8 +916,8 @@ impl TxMirror { } r => { tracing::error!( - target: "mirror", "Unexpected response sending tx from source #{} shard {}: {:?}. The transaction was not sent", - block.source_height, chunk.shard_id, r + target: "mirror", "Unexpected response sending tx from {}: {:?}. The transaction was not sent", + &tx.provenance, r ); crate::metrics::TRANSACTIONS_SENT .with_label_values(&["internal_error"]) @@ -1014,7 +1059,7 @@ impl TxMirror { txs: &mut Vec, predecessor_id: AccountId, receiver_id: AccountId, - access_keys: Vec<(PublicKey, AccessKeyView)>, + actions: Vec, ref_hash: &CryptoHash, provenance: MappedTxProvenance, source_height: BlockHeight, @@ -1086,19 +1131,36 @@ impl TxMirror { crate::key_mapping::map_account(&receiver_id, self.secret.as_ref()); let mut nonce_updates = HashSet::new(); - let mut actions = Vec::new(); - - for (public_key, access_key) in access_keys { - let target_public_key = - crate::key_mapping::map_key(&public_key, self.secret.as_ref()).public_key(); - - nonce_updates.insert((target_receiver_id.clone(), target_public_key.clone())); - actions.push(Action::AddKey(AddKeyAction { - public_key: target_public_key, - access_key: access_key.into(), - })); + let mut target_actions = Vec::new(); + + for a in actions { + match a { + ActionView::AddKey { public_key, access_key } => { + let target_public_key = + crate::key_mapping::map_key(&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 { + public_key: target_public_key, + access_key: access_key.into(), + })); + } + ActionView::CreateAccount => { + target_actions.push(Action::CreateAccount(CreateAccountAction {})) + } + ActionView::Transfer { deposit } => { + if provenance.is_create_account() { + target_actions.push(Action::Transfer(TransferAction { deposit })) + } + } + _ => {} + }; } + tracing::debug!( + target: "mirror", "preparing {} for ({}, {}) with actions: {:?}", + &provenance, &target_signer_id, target_secret_key.public_key(), &target_actions, + ); let target_tx = self .prepare_tx( tracker, @@ -1107,7 +1169,7 @@ impl TxMirror { target_signer_id, target_receiver_id, &target_secret_key, - actions, + target_actions, ref_hash, source_height, provenance, @@ -1139,6 +1201,7 @@ impl TxMirror { if !execution_status_good(&outcome.outcome.status) { return Ok(()); } + for id in outcome.outcome.receipt_ids.iter() { let receipt = match self.source_chain_access.get_receipt(id).await { Ok(r) => r, @@ -1151,22 +1214,39 @@ impl TxMirror { } Err(ChainError::Other(e)) => return Err(e), }; - if let ReceiptEnumView::Action { actions, .. } = &receipt.receipt { - // TODO: if predecessor and receiver are different, then any AddKeys are probably along with a - // CreateAccount. That is a different case we will handler later - if receipt.predecessor_id != receipt.receiver_id { + + if let ReceiptEnumView::Action { actions, .. } = receipt.receipt { + if (provenance.is_create_account() && receipt.predecessor_id == receipt.receiver_id) + || (!provenance.is_create_account() + && receipt.predecessor_id != receipt.receiver_id) + { continue; } - let mut keys = Vec::new(); + // TODO: should also take care of delete key actions, and deploy contract actions to + // implicit accounts, etc... + let mut key_added = false; + let mut account_created = false; for a in actions.iter() { - if let ActionView::AddKey { public_key, access_key } = a { - keys.push((public_key.clone(), access_key.clone())); - } + match a { + ActionView::AddKey { .. } => key_added = true, + ActionView::CreateAccount => account_created = true, + _ => {} + }; } - - if keys.is_empty() { + 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, &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, &actions, + ); + } let outcome = self .source_chain_access .get_outcome(TransactionOrReceiptId::Receipt { @@ -1187,7 +1267,7 @@ impl TxMirror { txs, receipt.predecessor_id, receipt.receiver_id, - keys, + actions, ref_hash, provenance, source_height, @@ -1202,7 +1282,7 @@ impl TxMirror { async fn add_tx_function_call_keys( &self, tx: &SignedTransactionView, - tx_idx: usize, + provenance: MappedTxProvenance, source_height: BlockHeight, ref_hash: &CryptoHash, tracker: &mut crate::chain_tracker::TxTracker, @@ -1226,7 +1306,7 @@ impl TxMirror { &receipt_id, &tx.receiver_id, ref_hash, - MappedTxProvenance::TxAddKey(tx_idx), + provenance, source_height, ) .await @@ -1241,7 +1321,7 @@ impl TxMirror { async fn add_receipt_function_call_keys( &self, receipt: &ReceiptView, - receipt_idx: usize, + provenance: MappedTxProvenance, source_height: BlockHeight, ref_hash: &CryptoHash, tracker: &mut crate::chain_tracker::TxTracker, @@ -1255,7 +1335,7 @@ impl TxMirror { &receipt.receipt_id, &receipt.receiver_id, ref_hash, - MappedTxProvenance::ReceiptAddKey(receipt_idx), + provenance, source_height, ) .await @@ -1267,23 +1347,82 @@ impl TxMirror { } } + async fn add_create_account_txs( + &self, + create_account_height: BlockHeight, + ref_hash: CryptoHash, + tracker: &mut crate::chain_tracker::TxTracker, + chunks: &mut Vec, + ) -> anyhow::Result<()> { + let source_chunks = self + .source_chain_access + .get_txs(create_account_height, &self.tracked_shards) + .await + .with_context(|| { + format!("Failed fetching chunks for source chain #{}", create_account_height) + })?; + for ch in source_chunks { + let txs = match chunks.iter_mut().find(|c| c.shard_id == ch.shard_id) { + Some(c) => &mut c.txs, + None => { + // It doesnt really matter which one we put it in, since we're just going to send them all anyway + tracing::warn!( + "got unexpected source chunk shard id {} for #{}", + ch.shard_id, + create_account_height + ); + &mut chunks[0].txs + } + }; + for (idx, source_tx) in ch.transactions.into_iter().enumerate() { + self.add_tx_function_call_keys( + &source_tx, + MappedTxProvenance::TxCreateAccount(create_account_height, ch.shard_id, idx), + create_account_height, + &ref_hash, + tracker, + txs, + ) + .await?; + } + for (idx, r) in ch.receipts.iter().enumerate() { + // TODO: we're scanning the list of receipts for each block twice. Once here and then again + // when we queue that height's txs. Prob not a big deal but could fix that. + self.add_receipt_function_call_keys( + r, + MappedTxProvenance::ReceiptCreateAccount( + create_account_height, + ch.shard_id, + idx, + ), + create_account_height, + &ref_hash, + tracker, + txs, + ) + .await?; + } + } + Ok(()) + } + // fetch the source chain block at `source_height`, and prepare a // set of transactions that should be valid in the target chain // from it. async fn fetch_txs( &self, source_height: BlockHeight, + create_account_height: Option, ref_hash: CryptoHash, tracker: &mut crate::chain_tracker::TxTracker, - ) -> anyhow::Result> { - let source_chunks = - match self.source_chain_access.get_txs(source_height, &self.tracked_shards).await { - Ok(x) => x, - Err(e) => match e { - ChainError::Unknown => return Ok(None), - ChainError::Other(e) => return Err(e), - }, - }; + ) -> anyhow::Result { + let source_chunks = self + .source_chain_access + .get_txs(source_height, &self.tracked_shards) + .await + .with_context(|| { + format!("Failed fetching chunks for source chain #{}", source_height) + })?; let mut chunks = Vec::new(); for ch in source_chunks { @@ -1314,14 +1453,14 @@ impl TxMirror { actions, &ref_hash, source_height, - MappedTxProvenance::SourceTxIndex(idx), + MappedTxProvenance::MappedSourceTx(source_height, ch.shard_id, idx), nonce_updates, ) .await?; txs.push(target_tx); self.add_tx_function_call_keys( &source_tx, - idx, + MappedTxProvenance::TxAddKey(source_height, ch.shard_id, idx), source_height, &ref_hash, tracker, @@ -1332,7 +1471,7 @@ impl TxMirror { for (idx, r) in ch.receipts.iter().enumerate() { self.add_receipt_function_call_keys( r, - idx, + MappedTxProvenance::ReceiptAddKey(source_height, ch.shard_id, idx), source_height, &ref_hash, tracker, @@ -1346,7 +1485,11 @@ impl TxMirror { ); chunks.push(MappedChunk { txs, shard_id: ch.shard_id }); } - Ok(Some(MappedBlock { source_height, chunks })) + if let Some(create_account_height) = create_account_height { + self.add_create_account_txs(create_account_height, ref_hash, tracker, &mut chunks) + .await?; + } + Ok(MappedBlock { source_height, chunks }) } // Up to a certain capacity, prepare and queue up batches of @@ -1362,33 +1505,34 @@ impl TxMirror { } let next_batch_time = tracker.next_batch_time(); - let source_head = self - .source_chain_access - .head_height() - .await - .context("can't fetch source chain HEAD")?; - let start_height = match tracker.height_queued() { - Some(h) => h + 1, - None => self.get_next_source_height()?, - }; - for height in start_height..=source_head { - if let Some(b) = self - .fetch_txs(height, ref_hash, tracker) - .await - .with_context(|| format!("Can't fetch source #{} transactions", height))? - { - tracker.queue_block(b, &self.target_view_client, &self.db).await?; - if tracker.num_blocks_queued() > 100 { - return Ok(()); - } + loop { + let (next_height, create_account_height) = + tracker.next_heights(&self.source_chain_access).await?; + + let next_height = match next_height { + Some(h) => h, + None => return Ok(()), }; + // if we have a stop height, just send the last few blocks without worrying about + // extra create account txs, otherwise wait until we get more blocks + if !tracker.has_stop_height() && create_account_height.is_none() { + return Ok(()); + } + let b = self + .fetch_txs(next_height, create_account_height, ref_hash, tracker) + .await + .with_context(|| format!("Can't fetch source #{} transactions", next_height))?; + tracker.queue_block(b, &self.target_view_client, &self.db).await?; + if tracker.num_blocks_queued() > 100 { + break; + } if check_send_time && tracker.num_blocks_queued() > 0 && Instant::now() > next_batch_time - Duration::from_millis(20) { - return Ok(()); + break; } } Ok(()) @@ -1437,13 +1581,52 @@ impl TxMirror { } async fn run(mut self, stop_height: Option) -> anyhow::Result<()> { + let (target_height, target_head) = self.wait_target_synced().await; + let last_stored_height = get_last_source_height(&self.db)?; + let last_height = last_stored_height.unwrap_or(self.target_genesis_height - 1); + + let next_heights = self.source_chain_access.init(last_height, CREATE_ACCOUNT_DELTA).await?; + + if next_heights.is_empty() { + anyhow::bail!("no new blocks after #{}", last_height); + } + if let Some(stop_height) = stop_height { + if next_heights[0] > stop_height { + anyhow::bail!( + "--stop-height was {} and the next height to send txs for is {}", + stop_height, + next_heights[0] + ); + } + } + + tracing::debug!(target: "mirror", "source chain initialized with first heights: {:?}", &next_heights); + let mut tracker = crate::chain_tracker::TxTracker::new( self.target_min_block_production_delay, + next_heights.iter(), stop_height, ); - self.source_chain_access.init().await?; - - let (target_height, target_head) = self.wait_target_synced().await; + if last_stored_height.is_none() { + // send any extra function call-initiated create accounts for the first few blocks right now + let chunks = self + .tracked_shards + .iter() + .map(|s| MappedChunk { shard_id: *s, txs: Vec::new() }) + .collect(); + let mut block = MappedBlock { source_height: last_height, chunks }; + for h in next_heights { + self.add_create_account_txs(h, target_head, &mut tracker, &mut block.chunks) + .await?; + } + if block.chunks.iter().any(|c| !c.txs.is_empty()) { + tracing::debug!(target: "mirror", "sending extra create account transactions for the first {} blocks", CREATE_ACCOUNT_DELTA); + tracker.queue_block(block, &self.target_view_client, &self.db).await?; + let b = tracker.next_batch(&self.target_view_client, &self.db).await?; + self.send_transactions(b).await?; + tracker.on_txs_sent(&self.target_view_client, &self.db, target_height).await?; + } + } self.queue_txs(&mut tracker, target_head, false).await?; diff --git a/tools/mirror/src/offline.rs b/tools/mirror/src/offline.rs index 847b27ab291..0eb7d543abf 100644 --- a/tools/mirror/src/offline.rs +++ b/tools/mirror/src/offline.rs @@ -54,6 +54,53 @@ impl ChainAccess { #[async_trait(?Send)] impl crate::ChainAccess for ChainAccess { + async fn init( + &self, + last_height: BlockHeight, + num_initial_blocks: usize, + ) -> anyhow::Result> { + let mut block_heights = Vec::with_capacity(num_initial_blocks); + let head = self.head_height().await?; + + let mut height = last_height + 1; + loop { + if height > head { + return Ok(block_heights); + } + match self.chain.get_block_hash_by_height(height) { + Ok(hash) => { + block_heights.push( + self.chain + .get_block_header(&hash) + .with_context(|| format!("failed fetching block header for {}", &hash))? + .height(), + ); + break; + } + Err(near_chain_primitives::Error::DBNotFoundErr(_)) => { + height += 1; + } + Err(e) => { + return Err(e) + .with_context(|| format!("failed fetching block hash for #{}", height)) + } + }; + } + while block_heights.len() < num_initial_blocks { + let last_height = *block_heights.iter().next_back().unwrap(); + match self.get_next_block_height(last_height).await { + Ok(h) => block_heights.push(h), + Err(ChainError::Unknown) => break, + Err(ChainError::Other(e)) => { + return Err(e).with_context(|| { + format!("failed getting next block height after {}", last_height) + }) + } + }; + } + Ok(block_heights) + } + async fn head_height(&self) -> Result { Ok(self.chain.head()?.height) } @@ -96,6 +143,12 @@ impl crate::ChainAccess for ChainAccess { Ok(chunks) } + async fn get_next_block_height(&self, height: BlockHeight) -> Result { + let hash = self.chain.get_block_hash_by_height(height)?; + let hash = self.chain.get_next_block_hash(&hash)?; + Ok(self.chain.get_block_header(&hash)?.height()) + } + async fn get_outcome( &self, id: TransactionOrReceiptId, diff --git a/tools/mirror/src/online.rs b/tools/mirror/src/online.rs index ed7aebe5c9b..2ed18195788 100644 --- a/tools/mirror/src/online.rs +++ b/tools/mirror/src/online.rs @@ -5,7 +5,7 @@ use async_trait::async_trait; use near_chain_configs::GenesisValidationMode; use near_client::ViewClientActor; use near_client_primitives::types::{ - GetBlock, GetChunk, GetChunkError, GetExecutionOutcome, GetReceipt, Query, + GetBlock, GetBlockError, GetChunk, GetChunkError, GetExecutionOutcome, GetReceipt, Query, }; use near_crypto::PublicKey; use near_o11y::WithSpanContextExt; @@ -39,15 +39,19 @@ impl ChainAccess { #[async_trait(?Send)] impl crate::ChainAccess for ChainAccess { - // wait until HEAD moves. We don't really need it to be fully synced. - async fn init(&self) -> anyhow::Result<()> { + async fn init( + &self, + last_height: BlockHeight, + num_initial_blocks: usize, + ) -> anyhow::Result> { + // first wait until HEAD moves. We don't really need it to be fully synced. let mut first_height = None; loop { match self.head_height().await { Ok(head) => match first_height { Some(h) => { if h != head { - return Ok(()); + break; } } None => { @@ -60,6 +64,31 @@ impl crate::ChainAccess for ChainAccess { tokio::time::sleep(Duration::from_millis(500)).await; } + + let mut block_heights = Vec::with_capacity(num_initial_blocks); + let mut height = last_height; + + loop { + // note that here we are using the fact that get_next_block_height() for this struct + // allows passing a height that doesn't exist in the chain. This is not true for the offline + // version + match self.get_next_block_height(height).await { + Ok(h) => { + block_heights.push(h); + height = h; + if block_heights.len() >= num_initial_blocks { + return Ok(block_heights); + } + } + Err(ChainError::Unknown) => { + tokio::time::sleep(Duration::from_millis(500)).await; + } + Err(ChainError::Other(e)) => { + return Err(e) + .with_context(|| format!("failed fetching next block after #{}", height)) + } + } + } } async fn head_height(&self) -> Result { @@ -109,6 +138,40 @@ impl crate::ChainAccess for ChainAccess { Ok(chunks) } + async fn get_next_block_height( + &self, + mut height: BlockHeight, + ) -> Result { + let head = self.head_height().await?; + + if height >= head { + // let's only return finalized heights + Err(ChainError::Unknown) + } else if height + 1 == head { + Ok(head) + } else { + loop { + height += 1; + if height >= head { + break Err(ChainError::Unknown); + } + match self + .view_client + .send( + GetBlock(BlockReference::BlockId(BlockId::Height(height))) + .with_span_context(), + ) + .await + .unwrap() + { + Ok(b) => break Ok(b.header.height), + Err(GetBlockError::UnknownBlock { .. }) => {} + Err(e) => break Err(ChainError::other(e)), + } + } + } + } + async fn get_outcome( &self, id: TransactionOrReceiptId, From 862e0e8d3fcdabc6633053a160a8ad27d0a17ca5 Mon Sep 17 00:00:00 2001 From: mm-near <91919554+mm-near@users.noreply.github.com> Date: Wed, 30 Nov 2022 22:33:42 +0100 Subject: [PATCH 053/188] [StateSync] Moving StateResponse handling logic into state.rs (#8135) Currently StateResponse logic was also present in client_actor code (which was a little bit confusing). Now I've moved it mostly into state.rs - where the other logic for state sync handling is located. --- chain/client/src/client_actor.rs | 120 ++++++------------------------- chain/client/src/sync/state.rs | 70 +++++++++++++++++- 2 files changed, 88 insertions(+), 102 deletions(-) diff --git a/chain/client/src/client_actor.rs b/chain/client/src/client_actor.rs index fbbda234a1a..2fb434c686f 100644 --- a/chain/client/src/client_actor.rs +++ b/chain/client/src/client_actor.rs @@ -38,8 +38,7 @@ use near_chain_configs::ClientConfig; use near_chunks::client::ShardsManagerResponse; use near_chunks::logic::cares_about_shard_this_or_next_epoch; use near_client_primitives::types::{ - Error, GetNetworkInfo, NetworkInfoResponse, ShardSyncDownload, ShardSyncStatus, Status, - StatusError, StatusSyncInfo, SyncStatus, + Error, GetNetworkInfo, NetworkInfoResponse, Status, StatusError, StatusSyncInfo, SyncStatus, }; use near_dyn_configs::EXPECTED_SHUTDOWN_AT; #[cfg(feature = "test_features")] @@ -497,6 +496,8 @@ impl Handler> for ClientActor { } } +/// StateResponse is used during StateSync and catchup. +/// It contains either StateSync header information (that tells us how many parts there are etc) or a single part. impl Handler> for ClientActor { type Result = (); @@ -513,111 +514,30 @@ impl Handler> for ClientActor { state_response.part().as_ref().map(|(part_id, data)| (part_id, data.len())) ); // Get the download that matches the shard_id and hash - let download = { - let mut download: Option<&mut ShardSyncDownload> = None; - - // ... It could be that the state was requested by the state sync - if let SyncStatus::StateSync(sync_hash, shards_to_download) = - &mut this.client.sync_status - { - if hash == *sync_hash { - if let Some(part_id) = state_response.part_id() { - this.client - .state_sync - .received_requested_part(part_id, shard_id, hash); - } - - if let Some(shard_download) = shards_to_download.get_mut(&shard_id) { - assert!( - download.is_none(), - "Internal downloads set has duplicates" - ); - download = Some(shard_download); - } else { - // This may happen because of sending too many StateRequests to different peers. - // For example, we received StateResponse after StateSync completion. - } - } - } - - // ... Or one of the catchups - if let Some((_, shards_to_download, _)) = - this.client.catchup_state_syncs.get_mut(&hash) - { - if let Some(part_id) = state_response.part_id() { - this.client.state_sync.received_requested_part(part_id, shard_id, hash); - } + // ... It could be that the state was requested by the state sync + if let SyncStatus::StateSync(sync_hash, shards_to_download) = + &mut this.client.sync_status + { + if hash == *sync_hash { if let Some(shard_download) = shards_to_download.get_mut(&shard_id) { - assert!(download.is_none(), "Internal downloads set has duplicates"); - download = Some(shard_download); - } else { - // This may happen because of sending too many StateRequests to different peers. - // For example, we received StateResponse after StateSync completion. + this.client.state_sync.update_download_on_state_response_message(shard_download, hash, shard_id, state_response, &mut this.client.chain); + return; } } - // We should not be requesting the same state twice. - download - }; + } - if let Some(shard_sync_download) = download { - match shard_sync_download.status { - ShardSyncStatus::StateDownloadHeader => { - if let Some(header) = state_response.take_header() { - if !shard_sync_download.downloads[0].done { - match this.client.chain.set_state_header(shard_id, hash, header) - { - Ok(()) => { - shard_sync_download.downloads[0].done = true; - } - Err(err) => { - error!(target: "sync", "State sync set_state_header error, shard = {}, hash = {}: {:?}", shard_id, hash, err); - shard_sync_download.downloads[0].error = true; - } - } - } - } else { - // No header found. - // It may happen because requested node couldn't build state response. - if !shard_sync_download.downloads[0].done { - info!(target: "sync", "state_response doesn't have header, should be re-requested, shard = {}, hash = {}", shard_id, hash); - shard_sync_download.downloads[0].error = true; - } - } - } - ShardSyncStatus::StateDownloadParts => { - if let Some(part) = state_response.take_part() { - let num_parts = shard_sync_download.downloads.len() as u64; - let (part_id, data) = part; - if part_id >= num_parts { - error!(target: "sync", "State sync received incorrect part_id # {:?} for hash {:?}, potential malicious peer", part_id, hash); - return; - } - if !shard_sync_download.downloads[part_id as usize].done { - match this.client.chain.set_state_part( - shard_id, - hash, - PartId::new(part_id, num_parts), - &data, - ) { - Ok(()) => { - shard_sync_download.downloads[part_id as usize].done = - true; - } - Err(err) => { - error!(target: "sync", "State sync set_state_part error, shard = {}, part = {}, hash = {}: {:?}", shard_id, part_id, hash, err); - shard_sync_download.downloads[part_id as usize].error = - true; - } - } - } - } - } - _ => {} + // ... Or one of the catchups + if let Some((state_sync, shards_to_download, _)) = + this.client.catchup_state_syncs.get_mut(&hash) + { + if let Some(shard_download) = shards_to_download.get_mut(&shard_id) { + state_sync.update_download_on_state_response_message(shard_download, hash, shard_id, state_response, &mut this.client.chain); + return; } - } else { - error!(target: "sync", "State sync received hash {} that we're not expecting, potential malicious peer", hash); } + + error!(target: "sync", "State sync received hash {} that we're not expecting, potential malicious peer or a very delayed response.", hash); }) } } diff --git a/chain/client/src/sync/state.rs b/chain/client/src/sync/state.rs index ad33b5391b4..53f22d93fdf 100644 --- a/chain/client/src/sync/state.rs +++ b/chain/client/src/sync/state.rs @@ -21,6 +21,7 @@ //! use near_chain::{near_chain_primitives, Error}; +use near_primitives::state_part::PartId; use std::collections::HashMap; use std::ops::Add; use std::sync::atomic::{AtomicBool, Ordering}; @@ -39,7 +40,7 @@ use near_network::types::{ HighestHeightPeerInfo, NetworkRequests, NetworkResponses, PeerManagerAdapter, }; use near_primitives::hash::CryptoHash; -use near_primitives::syncing::get_num_state_parts; +use near_primitives::syncing::{get_num_state_parts, ShardStateSyncResponse}; use near_primitives::time::{Clock, Utc}; use near_primitives::types::{AccountId, ShardId, StateRoot}; @@ -580,7 +581,7 @@ impl StateSync { } /// Returns new ShardSyncDownload if successful, otherwise returns given shard_sync_download - pub fn request_shard( + fn request_shard( &mut self, me: &Option, shard_id: ShardId, @@ -757,6 +758,71 @@ impl StateSync { StateSyncResult::Unchanged }) } + + pub fn update_download_on_state_response_message( + &mut self, + shard_sync_download: &mut ShardSyncDownload, + hash: CryptoHash, + shard_id: u64, + state_response: ShardStateSyncResponse, + chain: &mut Chain, + ) { + if let Some(part_id) = state_response.part_id() { + // Mark that we have received this part (this will update info on pending parts from peers etc). + self.received_requested_part(part_id, shard_id, hash); + } + match shard_sync_download.status { + ShardSyncStatus::StateDownloadHeader => { + if let Some(header) = state_response.take_header() { + if !shard_sync_download.downloads[0].done { + match chain.set_state_header(shard_id, hash, header) { + Ok(()) => { + shard_sync_download.downloads[0].done = true; + } + Err(err) => { + error!(target: "sync", "State sync set_state_header error, shard = {}, hash = {}: {:?}", shard_id, hash, err); + shard_sync_download.downloads[0].error = true; + } + } + } + } else { + // No header found. + // It may happen because requested node couldn't build state response. + if !shard_sync_download.downloads[0].done { + info!(target: "sync", "state_response doesn't have header, should be re-requested, shard = {}, hash = {}", shard_id, hash); + shard_sync_download.downloads[0].error = true; + } + } + } + ShardSyncStatus::StateDownloadParts => { + if let Some(part) = state_response.take_part() { + let num_parts = shard_sync_download.downloads.len() as u64; + let (part_id, data) = part; + if part_id >= num_parts { + error!(target: "sync", "State sync received incorrect part_id # {:?} for hash {:?}, potential malicious peer", part_id, hash); + return; + } + if !shard_sync_download.downloads[part_id as usize].done { + match chain.set_state_part( + shard_id, + hash, + PartId::new(part_id, num_parts), + &data, + ) { + Ok(()) => { + shard_sync_download.downloads[part_id as usize].done = true; + } + Err(err) => { + error!(target: "sync", "State sync set_state_part error, shard = {}, part = {}, hash = {}: {:?}", shard_id, part_id, hash, err); + shard_sync_download.downloads[part_id as usize].error = true; + } + } + } + } + } + _ => {} + } + } } /// Create an abstract collection of elements to be shuffled. From 31c6f3763a244ce511cbbf6da2b093cf22e95edc Mon Sep 17 00:00:00 2001 From: LeKarimDerradji <53260686+LeKarimDerradji@users.noreply.github.com> Date: Thu, 1 Dec 2022 10:25:11 +0100 Subject: [PATCH 054/188] typo (#8144) tiny typo contact -> contract --- runtime/near-test-contracts/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/near-test-contracts/src/lib.rs b/runtime/near-test-contracts/src/lib.rs index 9fb3c9e0fd7..f4d54a07f25 100644 --- a/runtime/near-test-contracts/src/lib.rs +++ b/runtime/near-test-contracts/src/lib.rs @@ -10,7 +10,7 @@ pub fn wat_contract(wat: &str) -> Vec { wat::parse_str(wat).unwrap_or_else(|err| panic!("invalid wat: {err}\n{wat}")) } -/// Trivial contact with a do-nothing main function. +/// Trivial contract with a do-nothing main function. pub fn trivial_contract() -> &'static [u8] { static CONTRACT: OnceCell> = OnceCell::new(); CONTRACT.get_or_init(|| wat_contract(r#"(module (func (export "main")))"#)).as_slice() From 1f28c55dd4dafcf464d066bce79595527dda7a4e Mon Sep 17 00:00:00 2001 From: pompon0 Date: Thu, 1 Dec 2022 11:39:17 +0100 Subject: [PATCH 055/188] Rearranged code for the coming TIER1 implementation (#8142) * added support for awaiting connection handshake synchronously * removed redundant edge validation * rearranged routed message handler code * moved TCP server start to PeerManagerActor::spawn. * moved loop connection detection outside of connection::Pool, because we will need support for TIER1 loop connection (to verify public IPs). --- chain/network/src/peer/peer_actor.rs | 342 ++++++++++-------- chain/network/src/peer/testonly.rs | 6 +- .../src/peer_manager/connection/mod.rs | 49 ++- .../src/peer_manager/peer_manager_actor.rs | 133 +++---- .../src/peer_manager/tests/connection_pool.rs | 9 +- .../network/src/peer_manager/tests/routing.rs | 20 + chain/network/src/stats/metrics.rs | 2 + 7 files changed, 322 insertions(+), 239 deletions(-) diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index cfbaac4663c..074bab0ef2b 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -159,13 +159,36 @@ struct HandshakeSpec { partial_edge_info: PartialEdgeInfo, } +type HandshakeSignalSender = tokio::sync::oneshot::Sender; +pub type HandshakeSignal = tokio::sync::oneshot::Receiver; + impl PeerActor { - pub(crate) fn spawn( + /// Spawns a PeerActor on a separate actix::Arbiter and awaits for the + /// handshake to succeed/fail. The actual result is not returned because + /// actix makes everything complicated. + pub(crate) async fn spawn_and_handshake( clock: time::Clock, stream: tcp::Stream, force_encoding: Option, network_state: Arc, ) -> anyhow::Result> { + let (addr, handshake_signal) = Self::spawn(clock, stream, force_encoding, network_state)?; + // Await for the handshake to complete, by awaiting the handshake_signal channel. + // This is a receiver of Infallible, so it only completes when the channel is closed. + handshake_signal.await.err().unwrap(); + Ok(addr) + } + + /// Spawns a PeerActor on a separate actix arbiter. + /// Returns the actor address and a HandshakeSignal: an asynchronous channel + /// which will be closed as soon as the handshake is finished (successfully or not). + /// You can asynchronously await the returned HandshakeSignal. + pub(crate) fn spawn( + clock: time::Clock, + stream: tcp::Stream, + force_encoding: Option, + network_state: Arc, + ) -> anyhow::Result<(actix::Addr, HandshakeSignal)> { let stream_id = stream.id(); match Self::spawn_inner(clock, stream, force_encoding, network_state.clone()) { Ok(it) => Ok(it), @@ -183,7 +206,7 @@ impl PeerActor { stream: tcp::Stream, force_encoding: Option, network_state: Arc, - ) -> Result, ClosingReason> { + ) -> Result<(actix::Addr, HandshakeSignal), ClosingReason> { let connecting_status = match &stream.type_ { tcp::StreamType::Inbound => ConnectingStatus::Inbound( network_state @@ -193,10 +216,23 @@ impl PeerActor { .map_err(|_| ClosingReason::TooManyInbound)?, ), tcp::StreamType::Outbound { peer_id } => ConnectingStatus::Outbound { - _permit: network_state - .tier2 - .start_outbound(peer_id.clone()) - .map_err(ClosingReason::OutboundNotAllowed)?, + _permit: { + // This block will be executed for TIER2 only. + { + // A loop connection is not allowed on TIER2 + // (it is allowed on TIER1 to verify node's public IP). + // TODO(gprusak): try to make this more consistent. + if peer_id == &network_state.config.node_id() { + return Err(ClosingReason::OutboundNotAllowed( + connection::PoolError::UnexpectedLoopConnection, + )); + } + network_state + .tier2 + .start_outbound(peer_id.clone()) + .map_err(ClosingReason::OutboundNotAllowed)? + } + }, handshake_spec: HandshakeSpec { partial_edge_info: network_state.propose_edge(&clock, peer_id, None), protocol_version: PROTOCOL_VERSION, @@ -210,42 +246,48 @@ impl PeerActor { addr: network_state.config.node_addr.clone(), account_id: network_state.config.validator.as_ref().map(|v| v.account_id()), }; + // recv is the HandshakeSignal returned by this spawn_inner() call. + let (send, recv): (HandshakeSignalSender, HandshakeSignal) = + tokio::sync::oneshot::channel(); // Start PeerActor on separate thread. - Ok(Self::start_in_arbiter(&actix::Arbiter::new().handle(), move |ctx| { - let stats = Arc::new(connection::Stats::default()); - let stream_id = stream.id(); - let peer_addr = stream.peer_addr; - let stream_type = stream.type_.clone(); - let framed = stream::FramedStream::spawn(ctx, stream, stats.clone()); - Self { - closing_reason: None, - clock, - my_node_info, - stream_id, - peer_addr, - peer_type: match &stream_type { - tcp::StreamType::Inbound => PeerType::Inbound, - tcp::StreamType::Outbound { .. } => PeerType::Outbound, - }, - peer_status: PeerStatus::Connecting(connecting_status), - framed, - tracker: Default::default(), - stats, - routed_message_cache: LruCache::new(ROUTED_MESSAGE_CACHE_SIZE), - protocol_buffers_supported: false, - force_encoding, - peer_info: match &stream_type { - tcp::StreamType::Inbound => None, - tcp::StreamType::Outbound { peer_id } => Some(PeerInfo { - id: peer_id.clone(), - addr: Some(peer_addr), - account_id: None, - }), + Ok(( + Self::start_in_arbiter(&actix::Arbiter::new().handle(), move |ctx| { + let stream_id = stream.id(); + let peer_addr = stream.peer_addr; + let stream_type = stream.type_.clone(); + let stats = Arc::new(connection::Stats::default()); + let framed = stream::FramedStream::spawn(ctx, stream, stats.clone()); + Self { + closing_reason: None, + clock, + my_node_info, + stream_id, + peer_addr, + peer_type: match &stream_type { + tcp::StreamType::Inbound => PeerType::Inbound, + tcp::StreamType::Outbound { .. } => PeerType::Outbound, + }, + peer_status: PeerStatus::Connecting(send, connecting_status), + framed, + tracker: Default::default(), + stats, + routed_message_cache: LruCache::new(ROUTED_MESSAGE_CACHE_SIZE), + protocol_buffers_supported: false, + force_encoding, + peer_info: match &stream_type { + tcp::StreamType::Inbound => None, + tcp::StreamType::Outbound { peer_id } => Some(PeerInfo { + id: peer_id.clone(), + addr: Some(peer_addr), + account_id: None, + }), + } + .into(), + network_state, } - .into(), - network_state, - } - })) + }), + recv, + )) } // Determines the encoding to use for communication with the peer. @@ -373,14 +415,6 @@ impl PeerActor { self.peer_info.as_ref().as_ref().map(|peer_info| &peer_info.id) } - /// Update stats when receiving msg - fn update_stats_on_receiving_message(&mut self, msg_len: usize) { - metrics::PEER_DATA_RECEIVED_BYTES.inc_by(msg_len as u64); - metrics::PEER_MESSAGE_RECEIVED_TOTAL.inc(); - tracing::trace!(target: "network", msg_len); - self.tracker.lock().increment_received(&self.clock, msg_len as u64); - } - fn process_handshake( &mut self, ctx: &mut ::Context, @@ -388,7 +422,7 @@ impl PeerActor { ) { tracing::debug!(target: "network", "{:?}: Received handshake {:?}", self.my_node_info.id, handshake); let cs = match &self.peer_status { - PeerStatus::Connecting(it) => it, + PeerStatus::Connecting(_, it) => it, _ => panic!("process_handshake called in non-connecting state"), }; match cs { @@ -485,18 +519,6 @@ impl PeerActor { } } - // Verify that the received partial edge is valid. - // WARNING: signature is verified against the 2nd argument. - if !Edge::partial_verify( - &self.my_node_id(), - &handshake.sender_peer_id, - &handshake.partial_edge_info, - ) { - tracing::warn!(target: "network", "partial edge with invalid signature, disconnecting"); - self.stop(ctx, ClosingReason::Ban(ReasonForBan::InvalidSignature)); - return; - } - // Merge partial edges. let nonce = handshake.partial_edge_info.nonce; let partial_edge_info = match cs { @@ -514,7 +536,6 @@ impl PeerActor { partial_edge_info.signature.clone(), handshake.partial_edge_info.signature.clone(), ); - debug_assert!(edge.verify()); // TODO(gprusak): not enabling a port for listening is also a valid setup. // In that case peer_info.addr should be None (same as now), however @@ -618,76 +639,78 @@ impl PeerActor { protocol_version: handshake.protocol_version, partial_edge_info: partial_edge_info, }); - } else { - // Outbound peer triggers the inital full accounts data sync. - // TODO(gprusak): implement triggering the periodic full sync. - act.send_message_or_log(&PeerMessage::SyncAccountsData(SyncAccountsData{ - accounts_data: act.network_state.accounts_data.load().data.values().cloned().collect(), - incremental: false, - requesting_full_sync: true, - })); } - - // Exchange peers periodically. - ctx.spawn(wrap_future({ - let clock = act.clock.clone(); - let conn = conn.clone(); - async move { - let mut interval = time::Interval::new(clock.now(),REQUEST_PEERS_INTERVAL); - loop { - interval.tick(&clock).await; - conn.send_message(Arc::new(PeerMessage::PeersRequest)); - } + // TODO(gprusak): This block will be executed for TIER2 only. + { + if conn.peer_type == PeerType::Outbound { + // Outbound peer triggers the inital full accounts data sync. + // TODO(gprusak): implement triggering the periodic full sync. + act.send_message_or_log(&PeerMessage::SyncAccountsData(SyncAccountsData{ + accounts_data: act.network_state.accounts_data.load().data.values().cloned().collect(), + incremental: false, + requesting_full_sync: true, + })); } - })); - // Send latest block periodically - ctx.spawn(wrap_future({ - let clock = act.clock.clone(); - let conn = conn.clone(); - let state = act.network_state.clone(); - async move { - let mut interval = - time::Interval::new(clock.now(), SYNC_LATEST_BLOCK_INTERVAL); - loop { - // the first tick is immediate, so the tick should go sync_latest_block - interval.tick(&clock).await; - if let Some(chain_info) = state.chain_info.load().as_ref() { - conn.send_message(Arc::new(PeerMessage::Block( - chain_info.block.clone(), - ))); + // Exchange peers periodically. + ctx.spawn(wrap_future({ + let clock = act.clock.clone(); + let conn = conn.clone(); + let mut interval = time::Interval::new(clock.now(),REQUEST_PEERS_INTERVAL); + async move { + loop { + interval.tick(&clock).await; + conn.send_message(Arc::new(PeerMessage::PeersRequest)); } } - } - })); - - // Refresh connection nonces but only if we're outbound. For inbound connection, the other party should - // take care of nonce refresh. - if act.peer_type == PeerType::Outbound { + })); + // Send latest block periodically ctx.spawn(wrap_future({ - let conn = conn.clone(); - let network_state = act.network_state.clone(); let clock = act.clock.clone(); + let conn = conn.clone(); + let state = act.network_state.clone(); + let mut interval = time::Interval::new(clock.now(), SYNC_LATEST_BLOCK_INTERVAL); async move { - // How often should we refresh a nonce from a peer. - // It should be smaller than PRUNE_EDGES_AFTER. - let mut interval = time::Interval::new(start_time + PRUNE_EDGES_AFTER / 3, PRUNE_EDGES_AFTER / 3); loop { interval.tick(&clock).await; - conn.send_message(Arc::new( - PeerMessage::RequestUpdateNonce(PartialEdgeInfo::new( - &network_state.config.node_id(), - &conn.peer_info.id, - Edge::create_fresh_nonce(&clock), - &network_state.config.node_key, - ) - ))); - + if let Some(chain_info) = state.chain_info.load().as_ref() { + conn.send_message(Arc::new(PeerMessage::Block( + chain_info.block.clone(), + ))); + } } } })); + + // Refresh connection nonces but only if we're outbound. For inbound connection, the other party should + // take care of nonce refresh. + if act.peer_type == PeerType::Outbound { + ctx.spawn(wrap_future({ + let conn = conn.clone(); + let network_state = act.network_state.clone(); + let clock = act.clock.clone(); + // How often should we refresh a nonce from a peer. + // It should be smaller than PRUNE_EDGES_AFTER. + let mut interval = time::Interval::new(start_time + PRUNE_EDGES_AFTER / 3, PRUNE_EDGES_AFTER / 3); + async move { + loop { + interval.tick(&clock).await; + conn.send_message(Arc::new( + PeerMessage::RequestUpdateNonce(PartialEdgeInfo::new( + &network_state.config.node_id(), + &conn.peer_info.id, + Edge::create_fresh_nonce(&clock), + &network_state.config.node_key, + ) + ))); + + } + } + })); + } + // Sync the RoutingTable. + act.sync_routing_table(); } - // Sync the RoutingTable. - act.sync_routing_table(); + act.network_state.config.event_sink.push(Event::HandshakeCompleted(HandshakeCompletedEvent{ stream_id: act.stream_id, edge: conn.edge.load().as_ref().clone(), @@ -720,7 +743,7 @@ impl PeerActor { fn handle_msg_connecting(&mut self, ctx: &mut actix::Context, msg: PeerMessage) { match (&mut self.peer_status, msg) { ( - PeerStatus::Connecting(ConnectingStatus::Outbound { handshake_spec, .. }), + PeerStatus::Connecting(_, ConnectingStatus::Outbound { handshake_spec, .. }), PeerMessage::HandshakeFailure(peer_info, reason), ) => { match reason { @@ -762,7 +785,7 @@ impl PeerActor { // TODO(gprusak): LastEdge should rather be a variant of HandshakeFailure. // Clean this up (you don't have to modify the proto, just the translation layer). ( - PeerStatus::Connecting(ConnectingStatus::Outbound { handshake_spec, .. }), + PeerStatus::Connecting(_, ConnectingStatus::Outbound { handshake_spec, .. }), PeerMessage::LastEdge(edge), ) => { // Check that the edge provided: @@ -893,6 +916,7 @@ impl PeerActor { .delayed_push(|| Event::MessageProcessed(msg.clone())); let was_requested = match &msg { PeerMessage::Block(block) => { + self.network_state.txns_since_last_block.store(0, Ordering::Release); let hash = *block.hash(); let height = block.header().height(); conn.last_block.rcu(|last_block| { @@ -1086,6 +1110,37 @@ impl PeerActor { "Received routed message from {} to {:?}.", self.peer_info, msg.target); + let for_me = self.network_state.message_for_me(&msg.target); + if for_me { + metrics::record_routed_msg_metrics(&self.clock, &msg); + } + + // Drop duplicated messages routed within DROP_DUPLICATED_MESSAGES_PERIOD ms + let key = (msg.author.clone(), msg.target.clone(), msg.signature.clone()); + let now = self.clock.now(); + if let Some(&t) = self.routed_message_cache.get(&key) { + if now <= t + DROP_DUPLICATED_MESSAGES_PERIOD { + metrics::MessageDropped::Duplicate.inc(&msg.body); + self.network_state.config.event_sink.push(Event::RoutedMessageDropped); + tracing::debug!(target: "network", "Dropping duplicated message from {} to {:?}", msg.author, msg.target); + return; + } + } + if let RoutedMessageBody::ForwardTx(_) = &msg.body { + // Check whenever we exceeded number of transactions we got since last block. + // If so, drop the transaction. + let r = self.network_state.txns_since_last_block.load(Ordering::Acquire); + // TODO(gprusak): this constraint doesn't take into consideration such + // parameters as number of nodes or number of shards. Reconsider why do we need + // this and whether this is really the right way of handling it. + if r > MAX_TRANSACTIONS_PER_BLOCK_MESSAGE { + metrics::MessageDropped::TransactionsPerBlockExceeded.inc(&msg.body); + return; + } + self.network_state.txns_since_last_block.fetch_add(1, Ordering::AcqRel); + } + self.routed_message_cache.put(key, now); + if !msg.verify() { // Received invalid routed message from peer. self.stop(ctx, ClosingReason::Ban(ReasonForBan::InvalidSignature)); @@ -1100,8 +1155,7 @@ impl PeerActor { from.clone(), ); } - if self.network_state.message_for_me(&msg.target) { - metrics::record_routed_msg_metrics(&self.clock, &msg); + if for_me { // Handle Ping and Pong message if they are for us without sending to client. // i.e. Return false in case of Ping and Pong match &msg.body { @@ -1194,7 +1248,7 @@ impl actix::Actor for PeerActor { ); // If outbound peer, initiate handshake. - if let PeerStatus::Connecting(ConnectingStatus::Outbound { handshake_spec, .. }) = + if let PeerStatus::Connecting(_, ConnectingStatus::Outbound { handshake_spec, .. }) = &self.peer_status { self.send_handshake(handshake_spec.clone()); @@ -1306,7 +1360,14 @@ impl actix::Handler for PeerActor { return; } - self.update_stats_on_receiving_message(msg.len()); + // Message type agnostic stats. + { + metrics::PEER_DATA_RECEIVED_BYTES.inc_by(msg.len() as u64); + metrics::PEER_MESSAGE_RECEIVED_TOTAL.inc(); + tracing::trace!(target: "network", msg_len=msg.len()); + self.tracker.lock().increment_received(&self.clock, msg.len() as u64); + } + let mut peer_msg = match self.parse_message(&msg) { Ok(msg) => msg, Err(err) => { @@ -1315,35 +1376,6 @@ impl actix::Handler for PeerActor { } }; - match &peer_msg { - PeerMessage::Routed(msg) => { - let key = (msg.author.clone(), msg.target.clone(), msg.signature.clone()); - let now = self.clock.now(); - // Drop duplicated messages routed within DROP_DUPLICATED_MESSAGES_PERIOD ms - if let Some(&t) = self.routed_message_cache.get(&key) { - if now <= t + DROP_DUPLICATED_MESSAGES_PERIOD { - tracing::debug!(target: "network", "Dropping duplicated message from {} to {:?}", msg.author, msg.target); - self.network_state.config.event_sink.push(Event::RoutedMessageDropped); - return; - } - } - if let RoutedMessageBody::ForwardTx(_) = &msg.body { - // Check whenever we exceeded number of transactions we got since last block. - // If so, drop the transaction. - let r = self.network_state.txns_since_last_block.load(Ordering::Acquire); - if r > MAX_TRANSACTIONS_PER_BLOCK_MESSAGE { - return; - } - self.network_state.txns_since_last_block.fetch_add(1, Ordering::AcqRel); - } - self.routed_message_cache.put(key, now); - } - PeerMessage::Block(_) => { - self.network_state.txns_since_last_block.store(0, Ordering::Release); - } - _ => {} - } - tracing::trace!(target: "network", "Received message: {}", peer_msg); { @@ -1440,7 +1472,7 @@ enum ConnectingStatus { #[derive(Debug)] enum PeerStatus { /// Handshake in progress. - Connecting(ConnectingStatus), + Connecting(HandshakeSignalSender, ConnectingStatus), /// Ready to go. Ready(Arc), } diff --git a/chain/network/src/peer/testonly.rs b/chain/network/src/peer/testonly.rs index 19c4c8012db..70e0bcd7144 100644 --- a/chain/network/src/peer/testonly.rs +++ b/chain/network/src/peer/testonly.rs @@ -2,7 +2,8 @@ use crate::broadcast; use crate::config::NetworkConfig; use crate::network_protocol::testonly as data; use crate::network_protocol::{ - Edge, PartialEdgeInfo, PeerMessage, RawRoutedMessage, RoutedMessageBody, RoutedMessageV2, + Edge, PartialEdgeInfo, PeerIdOrHash, PeerMessage, RawRoutedMessage, RoutedMessageBody, + RoutedMessageV2, }; use crate::peer::peer_actor::PeerActor; use crate::peer_manager::network_state::NetworkState; @@ -14,7 +15,6 @@ use crate::tcp; use crate::testonly::actix::ActixSystem; use crate::testonly::fake_client; use crate::time; -use crate::types::PeerIdOrHash; use near_o11y::WithSpanContextExt; use near_primitives::network::PeerId; use std::sync::Arc; @@ -112,7 +112,7 @@ impl PeerHandle { let actix = ActixSystem::spawn({ let clock = clock.clone(); let cfg = cfg.clone(); - move || PeerActor::spawn(clock, stream, cfg.force_encoding, network_state).unwrap() + move || PeerActor::spawn(clock, stream, cfg.force_encoding, network_state).unwrap().0 }) .await; Self { actix, cfg, events: recv, edge: None } diff --git a/chain/network/src/peer_manager/connection/mod.rs b/chain/network/src/peer_manager/connection/mod.rs index c200fcacdd5..bf66bc654cd 100644 --- a/chain/network/src/peer_manager/connection/mod.rs +++ b/chain/network/src/peer_manager/connection/mod.rs @@ -49,9 +49,9 @@ pub(crate) struct Connection { pub addr: actix::Addr, pub peer_info: PeerInfo, + pub edge: ArcMutex, /// AccountKey ownership proof. pub owned_account: Option, - pub edge: ArcMutex, /// Chain Id and hash of genesis block. pub genesis_id: GenesisId, /// Shards that the peer is tracking. @@ -198,6 +198,8 @@ pub(crate) struct PoolSnapshot { /// In this scenario A will fail to obtain a permit, because it has already accepted a /// connection from B. pub outbound_handshakes: im::HashSet, + /// Inbound end of the loop connection. The outbound end is added to the `ready` set. + pub loop_inbound: Option>, } pub(crate) struct OutboundHandshakePermit(PeerId, Weak>); @@ -237,12 +239,13 @@ pub(crate) enum PoolError { #[error("already started another outbound connection to this peer")] AlreadyStartedConnecting, #[error("loop connections are not allowed")] - LoopConnection, + UnexpectedLoopConnection, } impl Pool { pub fn new(me: PeerId) -> Pool { Self(Arc::new(ArcMutex::new(PoolSnapshot { + loop_inbound: None, me, ready: im::HashMap::new(), ready_by_account_key: im::HashMap::new(), @@ -256,16 +259,28 @@ impl Pool { pub fn insert_ready(&self, peer: Arc) -> Result<(), PoolError> { self.0.try_update(move |mut pool| { - let id = &peer.peer_info.id; - if id == &pool.me { - return Err(PoolError::LoopConnection); - } - if pool.ready.contains_key(id) { - return Err(PoolError::AlreadyConnected); + let id = peer.peer_info.id.clone(); + // We support loopback connections for the purpose of + // validating our own external IP. This is the only case + // in which we allow 2 connections in a pool to have the same + // PeerId. The outbound connection is added to the + // `ready` set, the inbound connection is put into dedicated `loop_inbound` field. + if id==pool.me && peer.peer_type==PeerType::Inbound { + if pool.loop_inbound.is_some() { + return Err(PoolError::AlreadyConnected); + } + // Detect a situation in which a different node tries to connect + // to us with the same PeerId. This can happen iff the node key + // has been stolen (or just copied over by mistake). + if !pool.ready.contains_key(&id) && !pool.outbound_handshakes.contains(&id) { + return Err(PoolError::UnexpectedLoopConnection); + } + pool.loop_inbound = Some(peer); + return Ok(((),pool)); } match peer.peer_type { PeerType::Inbound => { - if pool.outbound_handshakes.contains(id) && id >= &pool.me { + if pool.outbound_handshakes.contains(&id) && id >= pool.me { return Err(PoolError::AlreadyStartedConnecting); } } @@ -277,7 +292,7 @@ impl Pool { // that permit is dropped properly. However we will still // need a runtime check to verify that the permit comes // from the same Pool instance and is for the right PeerId. - if !pool.outbound_handshakes.contains(id) { + if !pool.outbound_handshakes.contains(&id) { panic!("bug detected: OutboundHandshakePermit dropped before calling Pool.insert_ready()") } } @@ -326,11 +341,19 @@ impl Pool { }) } + /// Reserves an OutboundHandshakePermit for the given peer_id. + /// It should be called before attempting to connect to this peer. + /// The returned permit shouldn't be dropped until insert_ready for this + /// outbound connection is called. + /// + /// This is required to resolve race conditions in case 2 nodes try to connect + /// to each other at the same time. + /// + /// NOTE: Pool supports loop connections (i.e. connections in which both ends are the same + /// node) for the purpose of verifying one's own public IP. + // TODO(gprusak): simplify this flow. pub fn start_outbound(&self, peer_id: PeerId) -> Result { self.0.try_update(move |mut pool| { - if peer_id == pool.me { - return Err(PoolError::LoopConnection); - } if pool.ready.contains_key(&peer_id) { return Err(PoolError::AlreadyConnected); } diff --git a/chain/network/src/peer_manager/peer_manager_actor.rs b/chain/network/src/peer_manager/peer_manager_actor.rs index c38123af59b..33f3bda4248 100644 --- a/chain/network/src/peer_manager/peer_manager_actor.rs +++ b/chain/network/src/peer_manager/peer_manager_actor.rs @@ -121,37 +121,6 @@ impl actix::Actor for PeerManagerActor { type Context = actix::Context; fn started(&mut self, ctx: &mut Self::Context) { - // Start server if address provided. - if let Some(server_addr) = self.state.config.node_addr { - tracing::debug!(target: "network", at = ?server_addr, "starting public server"); - let clock = self.clock.clone(); - let state = self.state.clone(); - ctx.spawn(wrap_future(async move { - let mut listener = match tcp::Listener::bind(server_addr).await { - Ok(it) => it, - Err(e) => { - panic!("failed to start listening on server_addr={server_addr:?} e={e:?}") - } - }; - state.config.event_sink.push(Event::ServerStarted); - loop { - if let Ok(stream) = listener.accept().await { - // Always let the new peer to send a handshake message. - // Only then we can decide whether we should accept a connection. - // It is expected to be reasonably cheap: eventually, for TIER2 network - // we would like to exchange set of connected peers even without establishing - // a proper connection. - tracing::debug!(target: "network", from = ?stream.peer_addr, "got new connection"); - if let Err(err) = - PeerActor::spawn(clock.clone(), stream, None, state.clone()) - { - tracing::info!(target:"network", ?err, "PeerActor::spawn()"); - } - } - } - })); - } - // Periodically push network information to client. self.push_network_info_trigger(ctx, self.state.config.push_info_period); @@ -210,8 +179,6 @@ impl PeerManagerActor { banned = peer_store.count_banned(), "Found known peers"); tracing::debug!(target: "network", blacklist = ?config.peer_store.blacklist, "Blacklist"); - - let my_peer_id = config.node_id(); let whitelist_nodes = { let mut v = vec![]; for wn in &config.whitelist_nodes { @@ -219,30 +186,71 @@ impl PeerManagerActor { } v }; + let my_peer_id = config.node_id(); let arbiter = actix::Arbiter::new().handle(); + let clock = clock.clone(); let state = Arc::new(NetworkState::new( &clock, store.clone(), peer_store, - config, + config.clone(), genesis_id, client, whitelist_nodes, )); - if let Some(cfg) = state.config.tier1.clone() { - // Connect to TIER1 proxies and broadcast the list those connections periodically. - arbiter.spawn({ - let clock = clock.clone(); - let state = state.clone(); - let mut interval = time::Interval::new(clock.now(), cfg.advertise_proxies_interval); - async move { - loop { - interval.tick(&clock).await; - state.tier1_advertise_proxies(&clock).await; - } + arbiter.spawn({ + let arbiter = arbiter.clone(); + let state = state.clone(); + let clock = clock.clone(); + async move { + // Start server if address provided. + if let Some(server_addr) = state.config.node_addr { + tracing::debug!(target: "network", at = ?server_addr, "starting public server"); + let mut listener = match tcp::Listener::bind(server_addr).await { + Ok(it) => it, + Err(e) => { + panic!("failed to start listening on server_addr={server_addr:?} e={e:?}") + } + }; + state.config.event_sink.push(Event::ServerStarted); + arbiter.spawn({ + let clock = clock.clone(); + let state = state.clone(); + async move { + loop { + if let Ok(stream) = listener.accept().await { + // Always let the new peer to send a handshake message. + // Only then we can decide whether we should accept a connection. + // It is expected to be reasonably cheap: eventually, for TIER2 network + // we would like to exchange set of connected peers even without establishing + // a proper connection. + tracing::debug!(target: "network", from = ?stream.peer_addr, "got new connection"); + if let Err(err) = + PeerActor::spawn(clock.clone(), stream, None, state.clone()) + { + tracing::info!(target:"network", ?err, "PeerActor::spawn()"); + } + } + } + } + }); } - }); - } + if let Some(cfg) = state.config.tier1.clone() { + // Connect to TIER1 proxies and broadcast the list those connections periodically. + arbiter.spawn({ + let clock = clock.clone(); + let state = state.clone(); + let mut interval = time::Interval::new(clock.now(), cfg.advertise_proxies_interval); + async move { + loop { + interval.tick(&clock).await; + state.tier1_advertise_proxies(&clock).await; + } + } + }); + } + } + }); Ok(Self::start_in_arbiter(&arbiter, move |_ctx| Self { my_peer_id: my_peer_id.clone(), started_connect_attempts: false, @@ -509,7 +517,7 @@ impl PeerManagerActor { async move { let result = async { let stream = tcp::Stream::connect(&peer_info).await.context("tcp::Stream::connect()")?; - PeerActor::spawn(clock.clone(),stream,None,state.clone()).context("PeerActor::spawn()")?; + PeerActor::spawn_and_handshake(clock.clone(),stream,None,state.clone()).await.context("PeerActor::spawn()")?; anyhow::Ok(()) }.await; @@ -569,24 +577,19 @@ impl PeerManagerActor { pub(crate) fn get_network_info(&self) -> NetworkInfo { let tier2 = self.state.tier2.load(); + let now = self.clock.now(); + let connected_peer = |cp: &Arc| ConnectedPeerInfo { + full_peer_info: cp.full_peer_info(), + received_bytes_per_sec: cp.stats.received_bytes_per_sec.load(Ordering::Relaxed), + sent_bytes_per_sec: cp.stats.sent_bytes_per_sec.load(Ordering::Relaxed), + last_time_peer_requested: cp.last_time_peer_requested.load().unwrap_or(now), + last_time_received_message: cp.last_time_received_message.load(), + connection_established_time: cp.established_time, + peer_type: cp.peer_type, + nonce: cp.edge.load().nonce(), + }; NetworkInfo { - connected_peers: tier2 - .ready - .values() - .map(|cp| ConnectedPeerInfo { - full_peer_info: cp.full_peer_info(), - received_bytes_per_sec: cp.stats.received_bytes_per_sec.load(Ordering::Relaxed), - sent_bytes_per_sec: cp.stats.sent_bytes_per_sec.load(Ordering::Relaxed), - last_time_peer_requested: cp - .last_time_peer_requested - .load() - .unwrap_or(self.clock.now()), - last_time_received_message: cp.last_time_received_message.load(), - connection_established_time: cp.established_time, - peer_type: cp.peer_type, - nonce: cp.edge.load().nonce(), - }) - .collect(), + connected_peers: tier2.ready.values().map(connected_peer).collect(), num_connected_peers: tier2.ready.len(), peer_max_count: self.state.max_num_peers.load(Ordering::Relaxed), highest_height_peers: self.highest_height_peers(), diff --git a/chain/network/src/peer_manager/tests/connection_pool.rs b/chain/network/src/peer_manager/tests/connection_pool.rs index 0ed2e8d1a79..35b006f21a2 100644 --- a/chain/network/src/peer_manager/tests/connection_pool.rs +++ b/chain/network/src/peer_manager/tests/connection_pool.rs @@ -24,6 +24,9 @@ async fn connection_spam_security_test() { let mut clock = time::FakeClock::default(); let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + const PEERS_OVER_LIMIT: usize = 10; + let mut cfg = chain.make_config(rng); + cfg.max_num_peers = (LIMIT_PENDING_PEERS + PEERS_OVER_LIMIT) as u32; let mut cfg = chain.make_config(rng); // Make sure that connections will never get dropped. cfg.handshake_timeout = time::Duration::hours(1); @@ -41,7 +44,7 @@ async fn connection_spam_security_test() { conns.push(pm.start_inbound(chain.clone(), chain.make_config(rng)).await); } // Try to establish additional connections. Should fail. - for _ in 0..10 { + for _ in 0..PEERS_OVER_LIMIT { let conn = pm.start_inbound(chain.clone(), chain.make_config(rng)).await; assert_eq!( ClosingReason::TooManyInbound, @@ -75,7 +78,7 @@ async fn loop_connection() { // Starting an outbound loop connection should be stopped without sending the handshake. let conn = pm.start_outbound(chain.clone(), cfg).await; assert_eq!( - ClosingReason::OutboundNotAllowed(connection::PoolError::LoopConnection), + ClosingReason::OutboundNotAllowed(connection::PoolError::UnexpectedLoopConnection), conn.manager_fail_handshake(&clock.clock()).await ); @@ -115,7 +118,7 @@ async fn loop_connection() { .await; assert_eq!( ClosingReason::RejectedByPeerManager(RegisterPeerError::PoolError( - connection::PoolError::LoopConnection + connection::PoolError::UnexpectedLoopConnection )), reason ); diff --git a/chain/network/src/peer_manager/tests/routing.rs b/chain/network/src/peer_manager/tests/routing.rs index 08121f474e0..797722faeba 100644 --- a/chain/network/src/peer_manager/tests/routing.rs +++ b/chain/network/src/peer_manager/tests/routing.rs @@ -838,6 +838,26 @@ async fn max_num_peers_limit() { tracing::info!(target:"test", "wait for {id3} routing table"); pm3.wait_for_routing_table(&[]).await; + + // These drop() calls fix the place at which we want the values to be dropped, + // so that they live long enough for the test to succeed. + // AFAIU (gprusak) NLL (https://blog.rust-lang.org/2022/08/05/nll-by-default.html) + // this is NOT strictly necessary, as the destructors of these values should + // be called exactly at the end of the function anyway + // (unless the problem is even deeper: for example the compiler incorrectly + // figures out that it can reorder some instructions). However, I add those every time + // I observe some flakiness that I can't reproduce, just to eliminate the possibility that + // I just don't understand how NLL works. + // + // The proper solution would be to: + // 1. Make nearcore presubmit run tests with "[panic=abort]" (which is not supported with + // "cargo test"), so that the test actually stop if some thread panic. + // 2. Set some timeout on the test execution (with an enourmous heardoom), so that we actually get some logs + // if the presubmit times out (also not supported by "cargo test"). + drop(pm0); + drop(pm1); + drop(pm2); + drop(pm3); } // test that TTL is handled property. diff --git a/chain/network/src/stats/metrics.rs b/chain/network/src/stats/metrics.rs index edb6cabb4e6..c64a18d48dd 100644 --- a/chain/network/src/stats/metrics.rs +++ b/chain/network/src/stats/metrics.rs @@ -387,6 +387,8 @@ pub(crate) enum MessageDropped { UnknownAccount, InputTooLong, MaxCapacityExceeded, + TransactionsPerBlockExceeded, + Duplicate, } impl MessageDropped { From 74bfdf8b44cba466483ff444d677147311e0495a Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Thu, 1 Dec 2022 17:33:42 +0100 Subject: [PATCH 056/188] refactor: various changes to address clippy warnings (#8147) Related to #8145 --- .../estimator-warehouse/src/estimate.rs | 4 ++-- .../estimator-warehouse/src/import.rs | 2 +- .../estimator-warehouse/src/main.rs | 12 ++++++------ .../estimator-warehouse/src/zulip.rs | 8 ++++---- tools/rpctypegen/core/src/lib.rs | 5 +---- tools/themis/src/utils.rs | 2 +- utils/near-cache/src/cell.rs | 5 +++++ 7 files changed, 20 insertions(+), 18 deletions(-) diff --git a/runtime/runtime-params-estimator/estimator-warehouse/src/estimate.rs b/runtime/runtime-params-estimator/estimator-warehouse/src/estimate.rs index bfeada6ff92..0d6fcae1b9a 100644 --- a/runtime/runtime-params-estimator/estimator-warehouse/src/estimate.rs +++ b/runtime/runtime-params-estimator/estimator-warehouse/src/estimate.rs @@ -25,7 +25,7 @@ pub(crate) fn run_estimation(db: &Db, config: &EstimateConfig) -> anyhow::Result let mut _maybe_tmp = None; let estimator_home = match &config.home { - Some(home) => &home, + Some(home) => home, None => { _maybe_tmp = Some(tempfile::tempdir()?); _maybe_tmp.as_ref().unwrap().path().to_str().unwrap() @@ -65,7 +65,7 @@ pub(crate) fn run_estimation(db: &Db, config: &EstimateConfig) -> anyhow::Result maybe_drop_cache.push("--drop-os-cache"); } - if maybe_drop_cache.len() == 0 { + if maybe_drop_cache.is_empty() { eprintln!("Running as non-root, storage related costs might be inaccurate because OS caches cannot be dropped"); }; diff --git a/runtime/runtime-params-estimator/estimator-warehouse/src/import.rs b/runtime/runtime-params-estimator/estimator-warehouse/src/import.rs index 725ebad3a66..aff44f7cfa8 100644 --- a/runtime/runtime-params-estimator/estimator-warehouse/src/import.rs +++ b/runtime/runtime-params-estimator/estimator-warehouse/src/import.rs @@ -37,7 +37,7 @@ struct EstimationResult { impl Db { pub(crate) fn import_json_lines(&self, info: &ImportConfig, input: &str) -> anyhow::Result<()> { for line in input.lines() { - self.import(info, &line)?; + self.import(info, line)?; } Ok(()) } diff --git a/runtime/runtime-params-estimator/estimator-warehouse/src/main.rs b/runtime/runtime-params-estimator/estimator-warehouse/src/main.rs index 095b0c34b61..6d3d5ddf8f4 100644 --- a/runtime/runtime-params-estimator/estimator-warehouse/src/main.rs +++ b/runtime/runtime-params-estimator/estimator-warehouse/src/main.rs @@ -80,8 +80,8 @@ fn generate_stats(db: &Db) -> anyhow::Result { &mut buf, "{:>24}{:>24}{:>24}", "icount", - EstimationRow::count_by_metric(&db, Metric::ICount)?, - EstimationRow::last_updated(&db, Metric::ICount)? + EstimationRow::count_by_metric(db, Metric::ICount)?, + EstimationRow::last_updated(db, Metric::ICount)? .map(|dt| dt.to_string()) .as_deref() .unwrap_or("never") @@ -90,8 +90,8 @@ fn generate_stats(db: &Db) -> anyhow::Result { &mut buf, "{:>24}{:>24}{:>24}", "time", - EstimationRow::count_by_metric(&db, Metric::Time)?, - EstimationRow::last_updated(&db, Metric::Time)? + EstimationRow::count_by_metric(db, Metric::Time)?, + EstimationRow::last_updated(db, Metric::Time)? .map(|dt| dt.to_string()) .as_deref() .unwrap_or("never") @@ -100,8 +100,8 @@ fn generate_stats(db: &Db) -> anyhow::Result { &mut buf, "{:>24}{:>24}{:>24}", "parameter", - ParameterRow::count(&db)?, - ParameterRow::latest_protocol_version(&db)? + ParameterRow::count(db)?, + ParameterRow::latest_protocol_version(db)? .map(|version| format!("v{version}")) .as_deref() .unwrap_or("never") diff --git a/runtime/runtime-params-estimator/estimator-warehouse/src/zulip.rs b/runtime/runtime-params-estimator/estimator-warehouse/src/zulip.rs index 8f297cd8db7..acb40862de2 100644 --- a/runtime/runtime-params-estimator/estimator-warehouse/src/zulip.rs +++ b/runtime/runtime-params-estimator/estimator-warehouse/src/zulip.rs @@ -51,13 +51,13 @@ impl ZulipEndpoint { } fn send_raw_message(&self, msg: &str, topic: &str) -> anyhow::Result<()> { let params = if let Some(user_list) = &self.user_list { - vec![("type", "private"), ("to", user_list), ("content", &msg)] + vec![("type", "private"), ("to", user_list), ("content", msg)] } else { vec![ ("type", "stream"), ("to", self.stream.as_deref().unwrap()), ("topic", topic), - ("content", &msg), + ("content", msg), ] }; self.client.post(&self.full_endpoint_url).form(¶ms).send()?; @@ -89,7 +89,7 @@ impl std::fmt::Display for ZulipReport { writeln!(f, "*Current commit: {}*", self.after)?; writeln!(f, "*Compared to: {}*", self.before)?; writeln!(f, "### Relative gas estimation changes above threshold: {}", self.changes.len())?; - if self.changes.len() > 0 { + if !self.changes.is_empty() { writeln!(f, "```")?; for change in &self.changes { let percent_change = 100.0 * (change.after - change.before) / change.before; @@ -106,7 +106,7 @@ impl std::fmt::Display for ZulipReport { writeln!(f, "```")?; } writeln!(f, "### Gas estimator uncertain estimations: {}", self.changes_uncertain.len())?; - if self.changes_uncertain.len() > 0 { + if !self.changes_uncertain.is_empty() { writeln!(f, "```")?; for change in &self.changes_uncertain { writeln!( diff --git a/tools/rpctypegen/core/src/lib.rs b/tools/rpctypegen/core/src/lib.rs index ae4e245ec94..a683647f62d 100644 --- a/tools/rpctypegen/core/src/lib.rs +++ b/tools/rpctypegen/core/src/lib.rs @@ -19,10 +19,7 @@ fn parse_rpc_error_variant(input: &DeriveInput) -> String { type_kind[0].to_string() } -fn error_type_name<'a>( - schema: &'a mut BTreeMap, - name: String, -) -> &'a mut ErrorType { +fn error_type_name(schema: &mut BTreeMap, name: String) -> &mut ErrorType { let error_type = ErrorType { name: name.clone(), ..Default::default() }; schema.entry(name).or_insert(error_type) } diff --git a/tools/themis/src/utils.rs b/tools/themis/src/utils.rs index c248dfe387a..dd2baada65f 100644 --- a/tools/themis/src/utils.rs +++ b/tools/themis/src/utils.rs @@ -63,5 +63,5 @@ where s += if items.peek().is_some() { ", " } else { " and " }; s += i.as_ref(); } - return s; + s } diff --git a/utils/near-cache/src/cell.rs b/utils/near-cache/src/cell.rs index 2125408b22e..d6f29bfe48c 100644 --- a/utils/near-cache/src/cell.rs +++ b/utils/near-cache/src/cell.rs @@ -24,6 +24,11 @@ where self.inner.borrow().len() } + /// Returns true if the cache is empty and false otherwise + pub fn is_empty(&self) -> bool { + self.inner.borrow().is_empty() + } + /// Return the value of they key in the cache otherwise computes the value and inserts it into /// the cache. If the key is already in the cache, they gets gets moved to the head of /// the LRU list. From 234919a721775042e5969c992cb5ae1e42ad7732 Mon Sep 17 00:00:00 2001 From: Marcelo Diop-Gonzalez Date: Thu, 1 Dec 2022 18:26:09 -0500 Subject: [PATCH 057/188] fix(migrations): dedup TransactionResult keys by id and block hash (#8131) Deduping by only the id is a bug because there can be multiple outcomes for the same ID if some tx or receipt appears in a fork. --- core/store/src/migrations.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/store/src/migrations.rs b/core/store/src/migrations.rs index 92417dc4caf..c680ff15b3c 100644 --- a/core/store/src/migrations.rs +++ b/core/store/src/migrations.rs @@ -250,8 +250,8 @@ pub fn migrate_32_to_33(storage: &crate::NodeStorage) -> anyhow::Result<()> { // It appears that it was possible that the same entry in the original column contained // duplicate outcomes. We remove them here to avoid panicing due to issuing a // self-overwriting transaction. - outcomes.sort_by_key(|outcome| outcome.id().clone()); - outcomes.dedup_by_key(|outcome| outcome.id().clone()); + outcomes.sort_by_key(|outcome| (outcome.id().clone(), outcome.block_hash.clone())); + outcomes.dedup_by_key(|outcome| (outcome.id().clone(), outcome.block_hash.clone())); for outcome in outcomes { update.insert_ser( DBCol::TransactionResultForBlock, From 8453612876a5776e388bf85d36e427e18db8a688 Mon Sep 17 00:00:00 2001 From: mm-near <91919554+mm-near@users.noreply.github.com> Date: Fri, 2 Dec 2022 14:38:50 +0100 Subject: [PATCH 058/188] documentation about future plans (#8143) * documentation about future plans * Update docs/architecture/next/malicious_chunk_producer_and_phase2.md Co-authored-by: nikurt <86772482+nikurt@users.noreply.github.com> * Update docs/architecture/next/malicious_chunk_producer_and_phase2.md Co-authored-by: nikurt <86772482+nikurt@users.noreply.github.com> * review Co-authored-by: nikurt <86772482+nikurt@users.noreply.github.com> Co-authored-by: near-bulldozer[bot] <73298989+near-bulldozer[bot]@users.noreply.github.com> --- docs/SUMMARY.md | 3 + docs/architecture/next/README.md | 6 + .../next/catchup_and_state_sync.md | 59 ++++++++++ .../malicious_chunk_producer_and_phase2.md | 108 ++++++++++++++++++ 4 files changed, 176 insertions(+) create mode 100644 docs/architecture/next/README.md create mode 100644 docs/architecture/next/catchup_and_state_sync.md create mode 100644 docs/architecture/next/malicious_chunk_producer_and_phase2.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 82ecffa3f1d..921675faa14 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -14,6 +14,9 @@ - [Cross shard transactions - deep dive](./architecture/how/cross-shard.md) - [Serialization: Borsh, Json, ProtoBuf](./architecture/how/serialization.md) - [Proofs](./architecture/how/proofs.md) +- [How neard will work](./architecture/next/README.md) + - [Catchup and state sync improvements](./architecture/next/catchup_and_state_sync.md) + - [Malicious producers and phase 2](./architecture/next/malicious_chunk_producer_and_phase2.md) - [Trie](./architecture/trie.md) - [Network](./architecture/network.md) - [Gas Cost Parameters](./architecture/gas/README.md) diff --git a/docs/architecture/next/README.md b/docs/architecture/next/README.md new file mode 100644 index 00000000000..867a4cd09d4 --- /dev/null +++ b/docs/architecture/next/README.md @@ -0,0 +1,6 @@ +# How neard will work +The documents under this chapter are talking about the future of NEAR - what we're planning on improving and how. + +(This also means that they can get out of date quickly :-). + +If you have comments, suggestions or want to help us designing and implementing some of these things here - please reach out on Zulip or github. diff --git a/docs/architecture/next/catchup_and_state_sync.md b/docs/architecture/next/catchup_and_state_sync.md new file mode 100644 index 00000000000..9b0e5f5ee31 --- /dev/null +++ b/docs/architecture/next/catchup_and_state_sync.md @@ -0,0 +1,59 @@ +This document is still a DRAFT. + + + +This document covers our improvement plans for state sync and catchup. +**Before reading this doc, you should take a look at [How sync works](../how/sync.md)** + + + +State sync is used in two situations: +* when your node is behind for more than 2 epochs (and it is not an archival node) - then rather than trying to apply block by block (that can take hours) - you 'give up' and download the fresh state (a.k.a state sync) and apply blocks from there. +* when you're a block (or chunk) producer - and in the upcoming epoch, you'll have to track a shard that you are not currently tracking. + +In the past (and currently) - the state sync was mostly used in the first scenario (as all block & chunk producers had to track all the shards for security reasons - so they didn't actually have to do catchup at all). + +As we progress towards phase 2 and keep increasing number of shards - the catchup part starts being a lot more critical. When we're running a network with a 100 shards, the single machine is simply not capable of tracking (a.k.a applying all transactions) of all shards - so it will have to track just a subset. And it will have to change this subset almost every epoch (as protocol rebalances the shard-to-producer assignment based on the stakes). + +This means that we have to do some larger changes to the state sync design, as requirements start to differ a lot: +* catchups are high priority (the validator MUST catchup within 1 epoch - otherwise it will not be able to produce blocks for the new shards in the next epoch - and therefore it will not earn rewards). +* a lot more catchups in progress (with lots of shards basically every validator would have to catchup at least one shard at each epoch boundary) - this leads to a lot more potential traffic on the network +* malicious attacks & incentives - the state data can be large and can cause a lot of network traffic. At the same time it is quite critical (see point above), so we'll have to make sure that the nodes are incetivised to provide the state parts upon request. +* only a subset of peers will be available to request the state sync from (as not everyone from our peers will be tracking the shard that we're interested in). + + +## Things that we're actively analysing + +### Performance of state sync on the receiver side +We're looking at the performance of state sync: +* how long does it take to create the parts, +* pro-actively creating the parts as soon as epoch starts +* creating them in parallel +* allowing user to ask for many at once +* allowing user to provide a bitmask of parts that are required (therefore allowing the server to return only the ones that it already cached). + +### Better performance on the requestor side + +Currently the parts are applied only once all them are downloaded - instead we should try to apply them in parallel - after each part is received. + +When we receive a part, we should announce this information to our peers - so that they know that they can request it from us if they need it. + +## Ideas - not actively working on them yet + +### Better networking (a.k.a Tier 3) +Currently our networking code is picking the peers to connect at random (as most of them are tracking all the shards). With phase2 it will no longer be the case, so we should work on improvements of our peer-selection mechanism. + +In general - we should make sure that we have direct connection to at least a few nodes that are tracking the same shards that we're tracking right now (or that we'll want to track in the near future). + +### Dedicated nodes optimized towards state sync responses +The idea is to create a set of nodes that would specialize in state sync responses (similar to how we have archival nodes today). + +The sub-idea of this, is to store such data on one of the cloud providers (AWS, GCP). + +### Sending deltas instead of full state syncs +In case of catchup, the requesting node might have tracked that shard in the past. So we could consider just sending a delta of the state rather than the whole state. + +While this helps with the amount of data being sent - it might require the receiver to do a lot more work (as the data that it is about to send cannot be easily cached). + + + diff --git a/docs/architecture/next/malicious_chunk_producer_and_phase2.md b/docs/architecture/next/malicious_chunk_producer_and_phase2.md new file mode 100644 index 00000000000..2bcb19528d9 --- /dev/null +++ b/docs/architecture/next/malicious_chunk_producer_and_phase2.md @@ -0,0 +1,108 @@ +# Malicious producers in phase 2 of sharding. + +In this document, we'll compare the impact of the hypothethical malicious producer on the NEAR system (both in the current setup and how it will work when phase2 is implemented). + +## Current state (Phase 1) + +Let's assume that a malicious chunk producer ``C1`` has produced a bad chunk +and sent it to the block producer at this height ``B1``. + +The block producer IS going to add the chunk to the block (as we don't validate +the chunks before adding to blocks - but only when signing the block - see +[Transcations and receipts - last section](./../how/tx_receipts.md)). + +After this block is produced, it is sent to all the validators to get the +signatures. + +As currently all the validators are tracking all the shards - they will quickly +notice that the chunk is invalid, so they will not sign the block. + +Therefore the next block producer ``B2`` is going to ignore ``B1``'s block, and +select block from ``B0`` as a parent instead. + +So TL;DR - **a bad chunk would not be added to the chain.** + +## Phase 2 and sharding + +Unfortunately things get a lot more complicated, once we scale. + +Let's assume the same setup as above (a single chunk producer ``C1`` being +malicious). But this time, we have 100 shards - each validator is tracking just +a few (they cannot track all - as today - as they would have to run super +powerful machines with > 100 cores). + +So in the similar scenario as above - ``C1`` creates a malicious chunks, and +sends it to ``B1``, which includes it in the block. + +And here's where the complexity starts - as most of the valiators will NOT +track the shard which ``C1`` was producing - so they will still sign the block. + +The validators that do track that shard will of course (assuming that they are non-malicious) refuse the sign. But overall, they will be a small majority - so the block is going to get enough signatures and be added to the chain. + +### Challenges, Slashing and Rollbacks + +So we're in a pickle - as a malicious chunk was just added to the chain. And +that's why need to have mechanisms to automatically recover from such situations: +Challenges, Slashing and Rollbacks. + +#### Challenge + +Challenge is a self-contained proof, that something went wrong in the chunk +processing. It must contain all the inputs (with their merkle proof), the code +that was executed, and the outputs (also with merkle proofs). + +Such a challenge allows anyone (even nodes that don't track that shard or have +any state) to verify the validity of the challenge. + +When anyone notices that a current chain contains a wrong transition - they +submit such challenge to the next block producer, which can easily verify it +and it to the next block. + +Then the validators do the verification themselves, and if successful, they +sign the block. + +When such block is succesfully signed, the protocol automatically slashes +malicious nodes (more details below) and initiates the rollback to bring the +state back to the state before the bad chunk (so in our case, back to the block +produced by `B0`). + + +#### Slashing + +Slashing is the process of taking away the part of the stake from validators +that are considered malicious. + +In the example above, we'll definately need to slash the ``C1`` - and potentially also any validators that were tracking that shard and did sign the bad block. + +Things that we'll have to figure out in the future: +* how much do we slash? all of the stake? some part? +* what happens to the slashed stake? is it burned? does it go to some pool? + +#### State rollbacks + +// TODO: add + + +## Problems with the current Phase 2 design + +### Is slashing painful enough? +In the example above, we'd succesfully slash the ``C1`` producer - but was it +enough? + +Currently (with 4 shards) you need around 20k NEAR to become a chunk producer. +If we increase the number of shards to 100, it would drop the minimum stake to +around 1k NEAR. + +In such scenario, by sacrificing 1k NEAR, the malicious node can cause the +system to rollback a couple blocks (potentially having bad impact on the bridge +contracts etc). + +On the other side, you could be a non-malicious chunk producer with a corrupted +database (or a nasty bug in the code) - and the effect would be the same - the +chunk that you produced would be marked as malicious, and you'd lose your stake +(which will be a super-scary even for any legitimate validator). + + +So the open question is - can we do something 'smarter' in the protocol to +detect the case, where there is 'just a single' malicious (or buggy) chunk +producer and avoid the expensive rollback? \ No newline at end of file From ea41f251a81af1b0eb43ec14d5fb3f7bb20adfb9 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Fri, 2 Dec 2022 15:23:17 +0100 Subject: [PATCH 059/188] refactor: more clippy fixes (#8150) Continuing on top of #8147, fixes some more clippy warnings. Related to #8145 --- chain/network/build.rs | 2 +- runtime/near-test-contracts/build.rs | 2 +- .../estimator-warehouse/src/check.rs | 2 +- .../estimator-warehouse/src/main.rs | 6 +++--- tools/rpctypegen/core/src/lib.rs | 4 ++-- tools/themis/src/rules.rs | 2 +- utils/near-cache/src/sync.rs | 5 +++++ 7 files changed, 14 insertions(+), 9 deletions(-) diff --git a/chain/network/build.rs b/chain/network/build.rs index 7b66c022c92..af6fb632b87 100644 --- a/chain/network/build.rs +++ b/chain/network/build.rs @@ -2,7 +2,7 @@ fn main() -> anyhow::Result<()> { println!("cargo:rerun-if-changed=src/network_protocol/network.proto"); protobuf_codegen::Codegen::new() .pure() - .includes(&["src/"]) + .includes(["src/"]) .input("src/network_protocol/network.proto") .cargo_out_dir("proto") .run() diff --git a/runtime/near-test-contracts/build.rs b/runtime/near-test-contracts/build.rs index 4da3663f0cb..1ec24556684 100644 --- a/runtime/near-test-contracts/build.rs +++ b/runtime/near-test-contracts/build.rs @@ -56,7 +56,7 @@ fn cargo_build_cmd(target_dir: &Path) -> Command { res.env("RUSTFLAGS", "-Dwarnings"); res.env("CARGO_TARGET_DIR", target_dir); - res.args(&["build", "--target=wasm32-unknown-unknown", "--release"]); + res.args(["build", "--target=wasm32-unknown-unknown", "--release"]); res } diff --git a/runtime/runtime-params-estimator/estimator-warehouse/src/check.rs b/runtime/runtime-params-estimator/estimator-warehouse/src/check.rs index 01facf8c9c6..d59d15d4fd0 100644 --- a/runtime/runtime-params-estimator/estimator-warehouse/src/check.rs +++ b/runtime/runtime-params-estimator/estimator-warehouse/src/check.rs @@ -95,7 +95,7 @@ pub(crate) fn create_report(db: &Db, config: &CheckConfig) -> anyhow::Result anyhow::bail!("you have to either specify both commits for comparison or neither"), }; - let estimations = if config.estimations.len() > 0 { + let estimations = if !config.estimations.is_empty() { config.estimations.clone() } else { let rows_a = EstimationRow::select_by_commit_and_metric(db, &commit_after, config.metric)?; diff --git a/runtime/runtime-params-estimator/estimator-warehouse/src/main.rs b/runtime/runtime-params-estimator/estimator-warehouse/src/main.rs index 6d3d5ddf8f4..c1c66151e99 100644 --- a/runtime/runtime-params-estimator/estimator-warehouse/src/main.rs +++ b/runtime/runtime-params-estimator/estimator-warehouse/src/main.rs @@ -71,9 +71,9 @@ enum Metric { fn generate_stats(db: &Db) -> anyhow::Result { let mut buf = String::new(); - writeln!(&mut buf, "")?; + writeln!(&mut buf)?; writeln!(&mut buf, "{:=^72}", " Warehouse statistics ")?; - writeln!(&mut buf, "")?; + writeln!(&mut buf)?; writeln!(&mut buf, "{:>24}{:>24}{:>24}", "metric", "records", "last updated")?; writeln!(&mut buf, "{:>24}{:>24}{:>24}", "------", "-------", "------------")?; writeln!( @@ -106,7 +106,7 @@ fn generate_stats(db: &Db) -> anyhow::Result { .as_deref() .unwrap_or("never") )?; - writeln!(&mut buf, "")?; + writeln!(&mut buf)?; writeln!(&mut buf, "{:=^72}", " END STATS ")?; Ok(buf) diff --git a/tools/rpctypegen/core/src/lib.rs b/tools/rpctypegen/core/src/lib.rs index a683647f62d..46e981a1d13 100644 --- a/tools/rpctypegen/core/src/lib.rs +++ b/tools/rpctypegen/core/src/lib.rs @@ -44,8 +44,8 @@ pub fn parse_error_type(schema: &mut BTreeMap, input: &Derive } Fields::Named(FieldsNamed { ref named, .. }) => { // If variant is Enum with a named fields - create a new type for each variant with named props - let mut error_type = ErrorType::default(); - error_type.name = variant.ident.to_string(); + let mut error_type = + ErrorType { name: variant.ident.to_string(), ..Default::default() }; for field in named { error_type.props.insert( field diff --git a/tools/themis/src/rules.rs b/tools/themis/src/rules.rs index 78d809cd3d7..0bdd855862e 100644 --- a/tools/themis/src/rules.rs +++ b/tools/themis/src/rules.rs @@ -63,7 +63,7 @@ pub fn rust_version_matches_toolchain(workspace: &Workspace) -> anyhow::Result<( if toolchain_version != workspace_version { bail!(ComplianceError { - msg: format!("rust-version in rust-toolchain.toml and workspace Cargo.toml differ"), + msg: "rust-version in rust-toolchain.toml and workspace Cargo.toml differ".to_string(), expected: None, outliers: Vec::new(), }); diff --git a/utils/near-cache/src/sync.rs b/utils/near-cache/src/sync.rs index 3c2af02ced3..f3cb9d75102 100644 --- a/utils/near-cache/src/sync.rs +++ b/utils/near-cache/src/sync.rs @@ -24,6 +24,11 @@ where self.inner.lock().unwrap().len() } + /// Returns true if the cache is empty and false otherwise. + pub fn is_empty(&self) -> bool { + self.inner.lock().unwrap().is_empty() + } + /// Return the value of they key in the cache otherwise computes the value and inserts it into /// the cache. If the key is already in the cache, they gets gets moved to the head of /// the LRU list. From bb707113e30f592ab62fbf388878687afb7ac682 Mon Sep 17 00:00:00 2001 From: mm-near <91919554+mm-near@users.noreply.github.com> Date: Fri, 2 Dec 2022 15:39:54 +0100 Subject: [PATCH 060/188] Added document describing different testing libraries (#8152) Co-authored-by: near-bulldozer[bot] <73298989+near-bulldozer[bot]@users.noreply.github.com> --- docs/SUMMARY.md | 1 + docs/practices/testing/test_utils.md | 100 +++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 docs/practices/testing/test_utils.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 921675faa14..d309f5380ee 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -39,6 +39,7 @@ - [Fast Builds](./practices/fast_builds.md) - [Testing](./practices/testing/README.md) - [Python Tests](./practices/testing/python_tests.md) + - [Testing Utils](./practices/testing/test_utils.md) - [Protocol Upgrade](./practices/protocol_upgrade.md) # Misc diff --git a/docs/practices/testing/test_utils.md b/docs/practices/testing/test_utils.md new file mode 100644 index 00000000000..fa0ecfaf31d --- /dev/null +++ b/docs/practices/testing/test_utils.md @@ -0,0 +1,100 @@ +# Cheat-sheet / overview of testing utils + +This page covers the different testing utils / libraries that we have for easier unittesting in Rust. + +## Basics + +### CryptoHash + +To create a new cryptohash: +```rust +"ADns6sqVyFLRZbSMCGdzUiUPaDDtjTmKCWzR8HxWsfDU".parse().unwrap() +``` + +### Account +Also prefer doing parse + unwrap +```rust +let alice: AccountId = "alice.near".parse().unwrap() +``` + +### Signatures +In memory signer (generates the key based on seed). There is a slight preference to use the seed that is matching the account name. + +```rust +InMemoryValidatorSigner::from_seed("account".parse().unwrap(), KeyType::ED25519, "account".to_owned()) +``` + +## Store +Use the in memory test store in tests: +```rust +let store = create_test_store(); +``` + +## Runtime +You can use the KeyValueRuntime (instead of the Nightshade one): + +```rust +KeyValueRuntime::new(store, epoch_length) +``` + +## EpochManager +Currently still embedded into Runtime - see above to use the KeyValueRuntime + +## Chain +No fakes or mock. + +### Chain genesis +We have a test method: + +```rust +ChainGenesis::test() +``` + +## Client + +TestEnv - for testing multiple clients (without network) +```rust +TestEnvBuilder::new(genesis).client(vec!["aa"]).validators(..).runtime_adapters(..).build() +``` + +## Network + +### PeerManager + +To create a PeerManager handler: + +```rust +let pm = peer_manager::testonly::start(...).await; +``` + +To connect to others: +```rust +pm.connect_to(&pm2.peer_info).await; +``` + +### Events handling + +To wait / handle a given event (as a lot of network code is running in async fashion): + +```rust +pm.events.recv_util(|event| match event {...}).await +``` + + + +## End to End + +### chain, runtime, signer + +in chain/chain/src/test_utils.rs: +```rust +// Creates 1-validator (test): chain, KVRuntime and a signer +let (chain, runtime, signer) = setup() +``` + +### block, client actor, view client +in chain/client/src/test_utils.rs +```rust +let (block, client, view_client) = setup(MANY_FIELDS) +``` + From 89dc59597528fc5221dbf43b8682ce1196a44b65 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Fri, 2 Dec 2022 16:43:35 +0100 Subject: [PATCH 061/188] stdx: add as_chunks function (#8136) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stdx::as_chunks function implements interface of a nightly [T]::as_chunks method. Also add as_chunks_exact which returns an error if slice doesn’t divide evenly into chunks of requested size. With this, there’s no longer need for ArrayChunks and doing unwrapping inside of ArrayChunks::next method. Furthermore, since ArrayChunks did not implement ExactSizeIterator, switching to the new function eliminate vector reallocation when collecting. --- Cargo.lock | 1 + core/store/src/trie/mod.rs | 17 ++++----- integration-tests/Cargo.toml | 1 + .../src/tests/runtime/sanity_checks.rs | 11 +++--- runtime/near-vm-logic/src/alt_bn128.rs | 18 ++++----- runtime/near-vm-logic/src/array_utils.rs | 33 ----------------- runtime/near-vm-logic/src/lib.rs | 1 - utils/stdx/src/lib.rs | 37 +++++++++++++++++++ 8 files changed, 61 insertions(+), 58 deletions(-) delete mode 100644 runtime/near-vm-logic/src/array_utils.rs diff --git a/Cargo.lock b/Cargo.lock index 347992844a6..3d8b07fd1b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2344,6 +2344,7 @@ dependencies = [ "near-performance-metrics", "near-primitives", "near-primitives-core", + "near-stdx", "near-store", "near-telemetry", "near-test-contracts", diff --git a/core/store/src/trie/mod.rs b/core/store/src/trie/mod.rs index b3b97a18660..ba3aeeeb3fa 100644 --- a/core/store/src/trie/mod.rs +++ b/core/store/src/trie/mod.rs @@ -683,15 +683,14 @@ impl Trie { // Converts the list of Nibbles to a readable string. fn nibbles_to_string(&self, prefix: &[u8]) -> String { - let mut result = String::new(); - for chunk in prefix.chunks(2) { - if chunk.len() == 2 { - let chr: char = ((chunk[0] * 16) + chunk[1]).into(); - write!(&mut result, "{}", chr.escape_default()).unwrap(); - } else { - // Final, sole nibble - write!(&mut result, " + {:x}", chunk[0]).unwrap(); - } + let (chunks, remainder) = stdx::as_chunks::<2, _>(prefix); + let mut result = chunks + .into_iter() + .map(|chunk| (chunk[0] * 16) + chunk[1]) + .flat_map(|ch| std::ascii::escape_default(ch).map(char::from)) + .collect::(); + if let Some(final_nibble) = remainder.first() { + write!(&mut result, "\\x{:x}_", final_nibble).unwrap(); } result } diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 86affac68c0..820cf38d1e8 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -21,6 +21,7 @@ rand.workspace = true serde.workspace = true serde_json.workspace = true smart-default.workspace = true +stdx.workspace = true strum.workspace = true tempfile.workspace = true tokio.workspace = true diff --git a/integration-tests/src/tests/runtime/sanity_checks.rs b/integration-tests/src/tests/runtime/sanity_checks.rs index 56c036f0dcd..4d2a9183858 100644 --- a/integration-tests/src/tests/runtime/sanity_checks.rs +++ b/integration-tests/src/tests/runtime/sanity_checks.rs @@ -224,12 +224,11 @@ fn test_sanity_used_gas() { }; assert_eq!(returned_bytes.len(), num_return_values * size_of::()); - let mut used_gas = vec![]; - for i in 0..num_return_values { - let bytes = &returned_bytes[i * size_of::()..(i + 1) * size_of::()]; - let val = u64::from_le_bytes(bytes.try_into().unwrap()); - used_gas.push(val); - } + let used_gas = stdx::as_chunks_exact::<{ size_of::() }, _>(&returned_bytes) + .unwrap() + .iter() + .map(|bytes| u64::from_le_bytes(*bytes)) + .collect::>(); // Executing `used_gas` costs `base_cost`. When executing `used_gas` twice // within a metered block, the returned values should differ by that amount. diff --git a/runtime/near-vm-logic/src/alt_bn128.rs b/runtime/near-vm-logic/src/alt_bn128.rs index 2b3e652b6eb..0836905223e 100644 --- a/runtime/near-vm-logic/src/alt_bn128.rs +++ b/runtime/near-vm-logic/src/alt_bn128.rs @@ -1,4 +1,3 @@ -use crate::array_utils::ArrayChunks; use bn::Group; use near_vm_errors::{HostError, VMLogicError}; @@ -25,20 +24,19 @@ impl From for VMLogicError { pub(crate) fn split_elements( data: &[u8], -) -> Result, InvalidInput> { - ArrayChunks::new(data).map_err(|()| { - let msg = - format!("invalid array, byte length {}, element size {}", data.len(), ELEMENT_SIZE); - InvalidInput { msg } +) -> Result<&[[u8; ELEMENT_SIZE]], InvalidInput> { + stdx::as_chunks_exact(data).map_err(|()| InvalidInput { + msg: format!("invalid array, byte length {}, element size {}", data.len(), ELEMENT_SIZE), }) } const G1_MULTIEXP_ELEMENT_SIZE: usize = POINT_SIZE + SCALAR_SIZE; pub(crate) fn g1_multiexp( - elements: ArrayChunks<'_, G1_MULTIEXP_ELEMENT_SIZE>, + elements: &[[u8; G1_MULTIEXP_ELEMENT_SIZE]], ) -> Result<[u8; POINT_SIZE], InvalidInput> { let elements: Vec<(bn::G1, bn::Fr)> = elements + .iter() .map(|chunk| { let (g1, fr) = stdx::split_array(chunk); let g1 = decode_g1(g1)?; @@ -55,10 +53,11 @@ pub(crate) fn g1_multiexp( const G1_SUM_ELEMENT_SIZE: usize = BOOL_SIZE + POINT_SIZE; pub(crate) fn g1_sum( - elements: ArrayChunks<'_, G1_SUM_ELEMENT_SIZE>, + elements: &[[u8; G1_SUM_ELEMENT_SIZE]], ) -> Result<[u8; POINT_SIZE], InvalidInput> { let elements: Vec<(bool, bn::G1)> = { elements + .iter() .map(|chunk| { let (sign, g1) = stdx::split_array(chunk); let sign = decode_bool(sign)?; @@ -78,9 +77,10 @@ pub(crate) fn g1_sum( const PAIRING_CHECK_ELEMENT_SIZE: usize = POINT_SIZE + POINT_SIZE * 2; pub(crate) fn pairing_check( - elements: ArrayChunks<'_, PAIRING_CHECK_ELEMENT_SIZE>, + elements: &[[u8; PAIRING_CHECK_ELEMENT_SIZE]], ) -> Result { let elements: Vec<(bn::G1, bn::G2)> = elements + .iter() .map(|chunk| { let (g1, g2) = stdx::split_array(chunk); let g1 = decode_g1(g1)?; diff --git a/runtime/near-vm-logic/src/array_utils.rs b/runtime/near-vm-logic/src/array_utils.rs deleted file mode 100644 index df86d4d7413..00000000000 --- a/runtime/near-vm-logic/src/array_utils.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Polyfils for missing const-generic array APIs in the std. - -use std::slice::ChunksExact; - -/// Converts an `&[u8]` slice of length `N * k` into iterator of `k` `[u8; N]` -/// chunks. -pub(crate) struct ArrayChunks<'a, const N: usize> { - inner: ChunksExact<'a, u8>, -} - -impl<'a, const N: usize> ArrayChunks<'a, N> { - pub(crate) fn new(bytes: &'a [u8]) -> Result, ()> { - let inner = bytes.chunks_exact(N); - if !inner.remainder().is_empty() { - return Err(()); - } - Ok(ArrayChunks { inner }) - } -} - -impl<'a, const N: usize> Iterator for ArrayChunks<'a, N> { - type Item = &'a [u8; N]; - - fn next(&mut self) -> Option { - self.inner.next().map(|it| it.try_into().unwrap()) - } - - fn size_hint(&self) -> (usize, Option) { - self.inner.size_hint() - } -} - -impl<'a, const N: usize> ExactSizeIterator for ArrayChunks<'a, N> {} diff --git a/runtime/near-vm-logic/src/lib.rs b/runtime/near-vm-logic/src/lib.rs index 4bc646e3cfc..e1def03580d 100644 --- a/runtime/near-vm-logic/src/lib.rs +++ b/runtime/near-vm-logic/src/lib.rs @@ -1,7 +1,6 @@ #![doc = include_str!("../README.md")] mod alt_bn128; -mod array_utils; mod context; mod dependencies; pub mod gas_counter; diff --git a/utils/stdx/src/lib.rs b/utils/stdx/src/lib.rs index 45e1ad31c25..97091e31681 100644 --- a/utils/stdx/src/lib.rs +++ b/utils/stdx/src/lib.rs @@ -84,8 +84,45 @@ fn test_join() { assert_eq!([0, 1, 2, 3], join_array([0, 1], [2, 3])); } +/// Splits a slice into a slice of N-element arrays. +// TODO(mina86): Replace with [T]::as_chunks once that’s stabilised. +pub fn as_chunks(slice: &[T]) -> (&[[T; N]], &[T]) { + let () = AssertNonZero::::OK; + + let len = slice.len() / N; + let (head, tail) = slice.split_at(len * N); + + // SAFETY: We cast a slice of `len * N` elements into a slice of `len` many + // `N` elements chunks. + let head = unsafe { std::slice::from_raw_parts(head.as_ptr().cast(), len) }; + (head, tail) +} + +/// Like `as_chunks` but returns an error if there’s a remainder. +pub fn as_chunks_exact(slice: &[T]) -> Result<&[[T; N]], ()> { + let (chunks, remainder) = as_chunks(slice); + if remainder.is_empty() { + Ok(chunks) + } else { + Err(()) + } +} + +#[test] +fn test_as_chunks() { + assert_eq!((&[[0, 1], [2, 3]][..], &[4][..]), as_chunks::<2, _>(&[0, 1, 2, 3, 4])); + assert_eq!(Ok(&[[0, 1], [2, 3]][..]), as_chunks_exact::<2, _>(&[0, 1, 2, 3])); + assert_eq!(Err(()), as_chunks_exact::<2, _>(&[0, 1, 2, 3, 4])); +} + /// Asserts, at compile time, that `S == A + B`. struct AssertEqSum; impl AssertEqSum { const OK: () = [()][A + B - S]; } + +/// Asserts, at compile time, that `N` is non-zero. +struct AssertNonZero; +impl AssertNonZero { + const OK: () = [()][if N == 0 { 1 } else { 0 }]; +} From 6a6856155629e50775ea392b0832189ec12345e5 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Mon, 5 Dec 2022 15:15:47 +0100 Subject: [PATCH 062/188] revived UpdateNonceResponse message. Dropping it in this release was backward incompatible (#8159) Force pushing - as it fails the proto backwards compatibility (as we're re-adding old field) --- chain/network/src/network_protocol/network.proto | 8 +++++++- .../src/network_protocol/proto_conv/peer_message.rs | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/chain/network/src/network_protocol/network.proto b/chain/network/src/network_protocol/network.proto index 4997cd1865f..3640343fb0b 100644 --- a/chain/network/src/network_protocol/network.proto +++ b/chain/network/src/network_protocol/network.proto @@ -264,6 +264,11 @@ message UpdateNonceRequest { PartialEdgeInfo partial_edge_info = 1; } +// Deprecated. Use SyncRoutingTable instead. +message UpdateNonceResponse { + Edge edge = 1; +} + // SyncAccountData message can represent: // - incremental sync (incremental = true, requesting_full_sync = false) // - full sync request (incremental = false, requesting_full_sync = true) @@ -381,7 +386,7 @@ message PeerMessage { // https://docs.google.com/document/d/1gCWmt9O-h_-5JDXIqbKxAaSS3Q9pryB1f9DDY1mMav4/edit reserved 1,2,3; // Deprecated fields. - reserved 9,20,21,22,23,24; + reserved 20,21,22,23,24; // Inter-process tracing information. TraceContext trace_context = 26; @@ -404,6 +409,7 @@ message PeerMessage { RoutingTableUpdate sync_routing_table = 7; UpdateNonceRequest update_nonce_request = 8; + UpdateNonceResponse update_nonce_response = 9; SyncAccountsData sync_accounts_data = 25; 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 5c74b39c367..3770ab7899c 100644 --- a/chain/network/src/network_protocol/proto_conv/peer_message.rs +++ b/chain/network/src/network_protocol/proto_conv/peer_message.rs @@ -221,6 +221,14 @@ impl TryFrom<&proto::PeerMessage> for PeerMessage { try_from_required(&unr.partial_edge_info) .map_err(Self::Error::UpdateNonceRequest)?, ), + ProtoMT::UpdateNonceResponse(unr) => { + PeerMessage::SyncRoutingTable(RoutingTableUpdate { + edges: vec![ + try_from_required(&unr.edge).map_err(Self::Error::UpdateNonceResponse)? + ], + accounts: vec![], + }) + } ProtoMT::SyncAccountsData(msg) => PeerMessage::SyncAccountsData(SyncAccountsData { accounts_data: try_from_slice(&msg.accounts_data) .map_err(Self::Error::SyncAccountsData)? From 0bc3bb95f33aaa0650bdee2e9f97260dcdafea5e Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Mon, 5 Dec 2022 18:12:34 +0100 Subject: [PATCH 063/188] refactor: fix clippy deprecated since warning (#8158) Fixes the following clippy error: ``` error: the since field must contain a semver-compliant version --> core/account-id/src/lib.rs:302:18 | 302 | #[deprecated(since = "#4440", note = "AccountId construction without validation is illegal")] | ^^^^^^^^^^^^^^^ | = note: `#[deny(clippy::deprecated_semver)]` on by default = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#deprecated_semver ``` --- core/account-id/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/account-id/src/lib.rs b/core/account-id/src/lib.rs index aa3a43a4ac6..d040d5fa285 100644 --- a/core/account-id/src/lib.rs +++ b/core/account-id/src/lib.rs @@ -299,7 +299,7 @@ impl AccountId { /// ``` #[doc(hidden)] #[cfg(feature = "internal_unstable")] - #[deprecated(since = "#4440", note = "AccountId construction without validation is illegal")] + #[deprecated = "AccountId construction without validation is illegal since #4440"] pub fn new_unvalidated(account_id: String) -> Self { Self(account_id.into_boxed_str()) } From 03eb0d62161e8a3e69d1d6abd74ee55f27046c2f Mon Sep 17 00:00:00 2001 From: pompon0 Date: Mon, 5 Dec 2022 19:32:13 +0100 Subject: [PATCH 064/188] TIER1 implementation (#8141) Implemented TIER1 connections: * added TIER1 support to PeerActor * added logic connecting a TIER1 node to its proxies before broadcasting its AccountData. * added logic making TIER1 nodes connect to other TIER1 nodes (or proxies) based on the collected AccountData. * made TIER1 nodes send some specific message types over TIER1 connections (with a fallback to TIER2). * made TIER1 proxies route the TIER1 messages. * added e2e tests of the TIER1 functionality Monitoring of the TIER1 performance will come in the next PR. --- chain/network/src/config.rs | 32 +- chain/network/src/config_json.rs | 37 ++ .../network/src/network_protocol/testonly.rs | 4 + chain/network/src/peer/peer_actor.rs | 184 ++++++++-- chain/network/src/peer/tests/communication.rs | 8 +- chain/network/src/peer/tests/stream.rs | 2 +- .../src/peer_manager/connection/mod.rs | 31 +- .../src/peer_manager/connection/tests.rs | 13 +- .../src/peer_manager/network_state/mod.rs | 195 +++++++--- .../src/peer_manager/network_state/tier1.rs | 285 ++++++++++++++- .../src/peer_manager/peer_manager_actor.rs | 36 +- chain/network/src/peer_manager/testonly.rs | 31 +- .../src/peer_manager/tests/accounts_data.rs | 21 +- .../src/peer_manager/tests/connection_pool.rs | 8 +- chain/network/src/peer_manager/tests/mod.rs | 1 + chain/network/src/peer_manager/tests/nonce.rs | 4 +- .../network/src/peer_manager/tests/routing.rs | 71 ++-- chain/network/src/peer_manager/tests/tier1.rs | 343 ++++++++++++++++++ chain/network/src/private_actix.rs | 2 + chain/network/src/tcp.rs | 29 +- chain/network/src/testonly/fake_client.rs | 5 +- chain/network/src/types.rs | 3 +- integration-tests/src/tests/network/runner.rs | 2 +- 23 files changed, 1159 insertions(+), 188 deletions(-) create mode 100644 chain/network/src/peer_manager/tests/tier1.rs diff --git a/chain/network/src/config.rs b/chain/network/src/config.rs index 4924ed3d20d..5fb320bad60 100644 --- a/chain/network/src/config.rs +++ b/chain/network/src/config.rs @@ -12,6 +12,7 @@ use near_crypto::{KeyType, SecretKey}; use near_primitives::network::PeerId; use near_primitives::types::AccountId; use near_primitives::validator_signer::{InMemoryValidatorSigner, ValidatorSigner}; +use std::collections::HashSet; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::sync::Arc; @@ -67,9 +68,23 @@ impl ValidatorConfig { #[derive(Clone)] pub struct Tier1 { + /// Interval between attempts to connect to proxies of other TIER1 nodes. + pub connect_interval: time::Duration, + /// Maximal number of new connections established every connect_interval. + /// TIER1 can consists of hundreds of nodes, so it is not feasible to connect to all of them at + /// once. + pub new_connections_per_attempt: u64, /// Interval between broacasts of the list of validator's proxies. /// Before the broadcast, validator tries to establish all the missing connections to proxies. pub advertise_proxies_interval: time::Duration, + /// Support for gradual TIER1 feature rollout: + /// - establishing connection to node's own proxies is always enabled (it is a part of peer + /// discovery mechanism). Note that unless the proxy has enable_inbound set, establishing + /// those connections will fail anyway. + /// - a node will start accepting TIER1 inbound connections iff `enable_inbound` is true. + /// - a node will try to start outbound TIER1 connections iff `enable_outbound` is true. + pub enable_inbound: bool, + pub enable_outbound: bool, } /// Validated configuration for the peer-to-peer manager. @@ -164,7 +179,12 @@ impl NetworkConfig { if cfg.public_addrs.len() > 0 && cfg.trusted_stun_servers.len() > 0 { anyhow::bail!("you cannot specify both public_addrs and trusted_stun_servers"); } + let mut proxies = HashSet::new(); for proxy in &cfg.public_addrs { + if proxies.contains(&proxy.peer_id) { + anyhow::bail!("public_addrs: found multiple entries with peer_id {}. Only 1 entry per peer_id is supported.",proxy.peer_id); + } + proxies.insert(proxy.peer_id.clone()); let ip = proxy.addr.ip(); if cfg.allow_private_ip_in_public_addrs { if ip.is_unspecified() { @@ -253,7 +273,13 @@ impl NetworkConfig { archive, accounts_data_broadcast_rate_limit: rate::Limit { qps: 0.1, burst: 1 }, routing_table_update_rate_limit: rate::Limit { qps: 1., burst: 1 }, - tier1: Some(Tier1 { advertise_proxies_interval: time::Duration::minutes(15) }), + tier1: Some(Tier1 { + connect_interval: cfg.experimental.tier1_connect_interval.try_into()?, + new_connections_per_attempt: cfg.experimental.tier1_new_connections_per_attempt, + advertise_proxies_interval: time::Duration::minutes(15), + enable_inbound: cfg.experimental.tier1_enable_inbound, + enable_outbound: cfg.experimental.tier1_enable_outbound, + }), inbound_disabled: cfg.experimental.inbound_disabled, skip_tombstones: if cfg.experimental.skip_sending_tombstones_seconds > 0 { Some(time::Duration::seconds(cfg.experimental.skip_sending_tombstones_seconds)) @@ -321,7 +347,11 @@ impl NetworkConfig { tier1: Some(Tier1 { // Interval is very large, so that it doesn't happen spontaneously in tests. // It should rather be triggered manually in tests. + connect_interval: time::Duration::hours(1000), + new_connections_per_attempt: 10000, advertise_proxies_interval: time::Duration::hours(1000), + enable_inbound: true, + enable_outbound: true, }), skip_tombstones: None, event_sink: Sink::null(), diff --git a/chain/network/src/config_json.rs b/chain/network/src/config_json.rs index a86c446a576..696d03fda6b 100644 --- a/chain/network/src/config_json.rs +++ b/chain/network/src/config_json.rs @@ -176,6 +176,23 @@ pub struct Config { pub experimental: ExperimentalConfig, } +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 +} + +fn default_tier1_connect_interval() -> Duration { + Duration::from_secs(60) +} + +fn default_tier1_new_connections_per_attempt() -> u64 { + 50 +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ExperimentalConfig { // If true - don't allow any inbound connections. @@ -192,6 +209,22 @@ pub struct ExperimentalConfig { // compatibility. #[serde(default = "default_skip_tombstones")] pub skip_sending_tombstones_seconds: i64, + + /// See `near_network::config::Tier1::enable_inbound`. + #[serde(default = "default_tier1_enable_inbound")] + pub tier1_enable_inbound: bool, + + /// See `near_network::config::Tier1::enable_outbound`. + #[serde(default = "default_tier1_enable_outbound")] + pub tier1_enable_outbound: bool, + + /// See `near_network::config::Tier1::connect_interval`. + #[serde(default = "default_tier1_connect_interval")] + pub tier1_connect_interval: Duration, + + /// See `near_network::config::Tier1::new_connections_per_attempt`. + #[serde(default = "default_tier1_new_connections_per_attempt")] + pub tier1_new_connections_per_attempt: u64, } impl Default for ExperimentalConfig { @@ -200,6 +233,10 @@ impl Default for ExperimentalConfig { inbound_disabled: false, connect_only_to_boot_nodes: false, skip_sending_tombstones_seconds: default_skip_tombstones(), + tier1_enable_inbound: default_tier1_enable_inbound(), + tier1_enable_outbound: default_tier1_enable_outbound(), + tier1_connect_interval: default_tier1_connect_interval(), + tier1_new_connections_per_attempt: default_tier1_new_connections_per_attempt(), } } } diff --git a/chain/network/src/network_protocol/testonly.rs b/chain/network/src/network_protocol/testonly.rs index 63033b118af..555325ed8b6 100644 --- a/chain/network/src/network_protocol/testonly.rs +++ b/chain/network/src/network_protocol/testonly.rs @@ -229,6 +229,10 @@ impl ChunkSet { } } +pub fn make_hash(rng: &mut R) -> CryptoHash { + CryptoHash::hash_bytes(&rng.gen::<[u8; 19]>()) +} + pub fn make_account_keys(signers: &[InMemoryValidatorSigner]) -> AccountKeys { let mut account_keys = AccountKeys::new(); for s in signers { diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index 074bab0ef2b..dea5d2c8407 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -4,7 +4,7 @@ use crate::concurrency::atomic_cell::AtomicCell; use crate::concurrency::demux; use crate::network_protocol::{ Edge, EdgeState, Encoding, OwnedAccount, ParsePeerMessageError, PartialEdgeInfo, - PeerChainInfoV2, PeerIdOrHash, PeerInfo, RawRoutedMessage, RoutedMessageBody, + PeerChainInfoV2, PeerIdOrHash, PeerInfo, RawRoutedMessage, RoutedMessageBody, RoutedMessageV2, RoutingTableUpdate, SyncAccountsData, }; use crate::peer::stream; @@ -60,6 +60,8 @@ const ROUTED_MESSAGE_CACHE_SIZE: usize = 1000; const DROP_DUPLICATED_MESSAGES_PERIOD: time::Duration = time::Duration::milliseconds(50); /// How often to send the latest block to peers. const SYNC_LATEST_BLOCK_INTERVAL: time::Duration = time::Duration::seconds(60); +/// How often to perform a full sync of AccountsData with the peer. +const ACCOUNTS_DATA_FULL_SYNC_INTERVAL: time::Duration = time::Duration::minutes(10); #[derive(Debug, Clone, PartialEq, Eq)] pub struct ConnectionClosedEvent { @@ -76,6 +78,7 @@ pub struct HandshakeStartedEvent { pub struct HandshakeCompletedEvent { pub(crate) stream_id: tcp::StreamId, pub(crate) edge: Edge, + pub(crate) tier: tcp::Tier, } #[derive(thiserror::Error, Clone, PartialEq, Eq, Debug)] @@ -93,6 +96,10 @@ pub(crate) enum ClosingReason { RejectedByPeerManager(RegisterPeerError), #[error("stream error")] StreamError, + /// Read through `tcp::Tier::is_allowed()` to see which message types + /// are allowed for a connection of each tier. + #[error("Received a message of type not allowed on this connection.")] + DisallowedMessage, #[error("PeerManager requested to close the connection")] PeerManager, #[error("Received DisconnectMessage from peer")] @@ -155,6 +162,7 @@ impl Debug for PeerActor { struct HandshakeSpec { /// ID of the peer on the other side of the connection. peer_id: PeerId, + tier: tcp::Tier, protocol_version: ProtocolVersion, partial_edge_info: PartialEdgeInfo, } @@ -215,10 +223,13 @@ impl PeerActor { .try_acquire_owned() .map_err(|_| ClosingReason::TooManyInbound)?, ), - tcp::StreamType::Outbound { peer_id } => ConnectingStatus::Outbound { - _permit: { - // This block will be executed for TIER2 only. - { + tcp::StreamType::Outbound { tier, peer_id } => ConnectingStatus::Outbound { + _permit: match tier { + tcp::Tier::T1 => network_state + .tier1 + .start_outbound(peer_id.clone()) + .map_err(ClosingReason::OutboundNotAllowed)?, + tcp::Tier::T2 => { // A loop connection is not allowed on TIER2 // (it is allowed on TIER1 to verify node's public IP). // TODO(gprusak): try to make this more consistent. @@ -236,11 +247,19 @@ impl PeerActor { handshake_spec: HandshakeSpec { partial_edge_info: network_state.propose_edge(&clock, peer_id, None), protocol_version: PROTOCOL_VERSION, + tier: *tier, peer_id: peer_id.clone(), }, }, }; - + // Override force_encoding for outbound Tier1 connections, + // since Tier1Handshake is supported only with proto encoding. + let force_encoding = match &stream.type_ { + tcp::StreamType::Outbound { tier, .. } if tier == &tcp::Tier::T1 => { + Some(Encoding::Proto) + } + _ => force_encoding, + }; let my_node_info = PeerInfo { id: network_state.config.node_id(), addr: network_state.config.node_addr.clone(), @@ -276,7 +295,7 @@ impl PeerActor { force_encoding, peer_info: match &stream_type { tcp::StreamType::Inbound => None, - tcp::StreamType::Outbound { peer_id } => Some(PeerInfo { + tcp::StreamType::Outbound { peer_id, .. } => Some(PeerInfo { id: peer_id.clone(), addr: Some(peer_addr), account_id: None, @@ -393,7 +412,10 @@ impl PeerActor { .sign(vc.signer.as_ref()) }), }; - let msg = PeerMessage::Tier2Handshake(handshake); + let msg = match spec.tier { + tcp::Tier::T1 => PeerMessage::Tier1Handshake(handshake), + tcp::Tier::T2 => PeerMessage::Tier2Handshake(handshake), + }; self.send_message_or_log(&msg); } @@ -418,6 +440,7 @@ impl PeerActor { fn process_handshake( &mut self, ctx: &mut ::Context, + tier: tcp::Tier, handshake: Handshake, ) { tracing::debug!(target: "network", "{:?}: Received handshake {:?}", self.my_node_info.id, handshake); @@ -442,6 +465,14 @@ impl PeerActor { self.stop(ctx, ClosingReason::HandshakeFailed); return; } + // This can happen only in case of a malicious node. + // Outbound peer requests a connection of a given TIER, the inbound peer can just + // confirm the TIER or drop connection. TIER is not negotiable during handshake. + if tier != spec.tier { + tracing::warn!(target: "network", "Connection TIER mismatch. Disconnecting peer {}", handshake.sender_peer_id); + self.stop(ctx, ClosingReason::HandshakeFailed); + return; + } if handshake.partial_edge_info.nonce != spec.partial_edge_info.nonce { tracing::warn!(target: "network", "Nonce mismatch. Disconnecting peer {}", handshake.sender_peer_id); self.stop(ctx, ClosingReason::HandshakeFailed); @@ -552,6 +583,7 @@ impl PeerActor { let now = self.clock.now(); let conn = Arc::new(connection::Connection { + tier, addr: ctx.address(), peer_info: peer_info.clone(), edge: ArcMutex::new(edge), @@ -576,6 +608,7 @@ impl PeerActor { let tracker = self.tracker.clone(); let clock = self.clock.clone(); + let mut interval = time::Interval::new(clock.now(), self.network_state.config.peer_stats_period); ctx.spawn({ @@ -636,19 +669,39 @@ impl PeerActor { if act.peer_type == PeerType::Inbound { act.send_handshake(HandshakeSpec{ peer_id: handshake.sender_peer_id.clone(), + tier, protocol_version: handshake.protocol_version, partial_edge_info: partial_edge_info, }); } - // TODO(gprusak): This block will be executed for TIER2 only. - { + // TIER1 is strictly reserved for BFT consensensus messages, + // so all kinds of periodical syncs happen only on TIER2 connections. + if tier==tcp::Tier::T2 { + // Trigger a full accounts data sync periodically. + // Note that AccountsData is used to establish TIER1 network, + // it is broadcasted over TIER2 network. This is a bootstrapping + // mechanism, because TIER2 is established before TIER1. + // + // TODO(gprusak): consider whether it wouldn't be more uniform to just + // send full sync from both sides of the connection independently. Or + // perhaps make the full sync request a separate message which doesn't + // carry the accounts_data at all. if conn.peer_type == PeerType::Outbound { - // Outbound peer triggers the inital full accounts data sync. - // TODO(gprusak): implement triggering the periodic full sync. - act.send_message_or_log(&PeerMessage::SyncAccountsData(SyncAccountsData{ - accounts_data: act.network_state.accounts_data.load().data.values().cloned().collect(), - incremental: false, - requesting_full_sync: true, + ctx.spawn(wrap_future({ + let clock = act.clock.clone(); + let conn = conn.clone(); + let network_state = act.network_state.clone(); + let mut interval = time::Interval::new(clock.now(),ACCOUNTS_DATA_FULL_SYNC_INTERVAL); + async move { + loop { + interval.tick(&clock).await; + conn.send_message(Arc::new(PeerMessage::SyncAccountsData(SyncAccountsData{ + accounts_data: network_state.accounts_data.load().data.values().cloned().collect(), + incremental: false, + requesting_full_sync: true, + }))); + } + } })); } // Exchange peers periodically. @@ -714,6 +767,7 @@ impl PeerActor { act.network_state.config.event_sink.push(Event::HandshakeCompleted(HandshakeCompletedEvent{ stream_id: act.stream_id, edge: conn.edge.load().as_ref().clone(), + tier: conn.tier, })); }, Err(err) => { @@ -821,8 +875,11 @@ impl PeerActor { actix::fut::ready(()) })); } + (PeerStatus::Connecting { .. }, PeerMessage::Tier1Handshake(msg)) => { + self.process_handshake(ctx, tcp::Tier::T1, msg) + } (PeerStatus::Connecting { .. }, PeerMessage::Tier2Handshake(msg)) => { - self.process_handshake(ctx, msg) + self.process_handshake(ctx, tcp::Tier::T2, msg) } (_, msg) => { tracing::warn!(target:"network","unexpected message during handshake: {}",msg) @@ -913,7 +970,7 @@ impl PeerActor { .network_state .config .event_sink - .delayed_push(|| Event::MessageProcessed(msg.clone())); + .delayed_push(|| Event::MessageProcessed(conn.tier, msg.clone())); let was_requested = match &msg { PeerMessage::Block(block) => { self.network_state.txns_since_last_block.store(0, Ordering::Release); @@ -987,6 +1044,26 @@ impl PeerActor { ); } + fn add_route_back(&self, conn: &connection::Connection, msg: &RoutedMessageV2) { + if !msg.expect_response() { + return; + } + tracing::trace!(target: "network", route_back = ?msg.clone(), "Received peer message that requires response"); + let from = &conn.peer_info.id; + match conn.tier { + tcp::Tier::T1 => self.network_state.tier1_route_back.lock().insert( + &self.clock, + msg.hash(), + from.clone(), + ), + tcp::Tier::T2 => self.network_state.graph.routing_table.add_route_back( + &self.clock, + msg.hash(), + from.clone(), + ), + } + } + fn handle_msg_ready( &mut self, ctx: &mut actix::Context, @@ -1003,7 +1080,7 @@ impl PeerActor { tracing::debug!(target: "network", "Disconnect signal. Me: {:?} Peer: {:?}", self.my_node_info.id, self.other_peer_id()); self.stop(ctx, ClosingReason::DisconnectMessage); } - PeerMessage::Tier2Handshake(_) => { + PeerMessage::Tier1Handshake(_) | PeerMessage::Tier2Handshake(_) => { // Received handshake after already have seen handshake from this peer. tracing::debug!(target: "network", "Duplicate handshake from {}", self.peer_info); } @@ -1016,7 +1093,10 @@ impl PeerActor { tracing::debug!(target: "network", "Peers request from {}: sending {} peers.", self.peer_info, peers.len()); self.send_message_or_log(&PeerMessage::PeersResponse(peers)); } - self.network_state.config.event_sink.push(Event::MessageProcessed(peer_msg)); + self.network_state + .config + .event_sink + .push(Event::MessageProcessed(conn.tier, peer_msg)); } PeerMessage::PeersResponse(peers) => { tracing::debug!(target: "network", "Received peers from {}: {} peers.", self.peer_info, peers.len()); @@ -1027,7 +1107,10 @@ impl PeerActor { ) { tracing::error!(target: "network", ?err, "Fail to update peer store"); }; - self.network_state.config.event_sink.push(Event::MessageProcessed(peer_msg)); + self.network_state + .config + .event_sink + .push(Event::MessageProcessed(conn.tier, peer_msg)); } PeerMessage::RequestUpdateNonce(edge_info) => { let clock = self.clock.clone(); @@ -1055,15 +1138,23 @@ impl PeerActor { conn.send_message(Arc::new(PeerMessage::SyncRoutingTable( RoutingTableUpdate::from_edges(vec![edge]), ))); - network_state.config.event_sink.push(Event::MessageProcessed(peer_msg)); + network_state + .config + .event_sink + .push(Event::MessageProcessed(conn.tier, peer_msg)); })); } PeerMessage::SyncRoutingTable(rtu) => { let clock = self.clock.clone(); + let conn = conn.clone(); let network_state = self.network_state.clone(); ctx.spawn(wrap_future(async move { - Self::handle_sync_routing_table(&clock, &network_state, conn, rtu).await; - network_state.config.event_sink.push(Event::MessageProcessed(peer_msg)); + Self::handle_sync_routing_table(&clock, &network_state, conn.clone(), rtu) + .await; + network_state + .config + .event_sink + .push(Event::MessageProcessed(conn.tier, peer_msg)); })); } PeerMessage::SyncAccountsData(msg) => { @@ -1085,7 +1176,10 @@ impl PeerActor { } // Early exit, if there is no data in the message. if msg.accounts_data.is_empty() { - network_state.config.event_sink.push(Event::MessageProcessed(peer_msg)); + network_state + .config + .event_sink + .push(Event::MessageProcessed(conn.tier, peer_msg)); return; } let network_state = self.network_state.clone(); @@ -1101,7 +1195,10 @@ impl PeerActor { } })); } - network_state.config.event_sink.push(Event::MessageProcessed(peer_msg)); + network_state + .config + .event_sink + .push(Event::MessageProcessed(conn.tier, peer_msg)); })); } PeerMessage::Routed(mut msg) => { @@ -1146,44 +1243,42 @@ impl PeerActor { self.stop(ctx, ClosingReason::Ban(ReasonForBan::InvalidSignature)); return; } - let from = &conn.peer_info.id; - if msg.expect_response() { - tracing::trace!(target: "network", route_back = ?msg.clone(), "Received peer message that requires response"); - self.network_state.graph.routing_table.add_route_back( - &self.clock, - msg.hash(), - from.clone(), - ); - } + + self.add_route_back(&conn, msg.as_ref()); if for_me { // Handle Ping and Pong message if they are for us without sending to client. // i.e. Return false in case of Ping and Pong match &msg.body { RoutedMessageBody::Ping(ping) => { - self.network_state.send_pong(&self.clock, ping.nonce, msg.hash()); + self.network_state.send_pong( + &self.clock, + conn.tier, + ping.nonce, + msg.hash(), + ); // TODO(gprusak): deprecate Event::Ping/Pong in favor of // MessageProcessed. self.network_state.config.event_sink.push(Event::Ping(ping.clone())); self.network_state .config .event_sink - .push(Event::MessageProcessed(PeerMessage::Routed(msg))); + .push(Event::MessageProcessed(conn.tier, PeerMessage::Routed(msg))); } RoutedMessageBody::Pong(pong) => { self.network_state.config.event_sink.push(Event::Pong(pong.clone())); self.network_state .config .event_sink - .push(Event::MessageProcessed(PeerMessage::Routed(msg))); + .push(Event::MessageProcessed(conn.tier, PeerMessage::Routed(msg))); } _ => self.receive_message(ctx, &conn, PeerMessage::Routed(msg.clone())), } } else { if msg.decrease_ttl() { - self.network_state.send_message_to_peer(&self.clock, msg); + self.network_state.send_message_to_peer(&self.clock, conn.tier, msg); } else { self.network_state.config.event_sink.push(Event::RoutedMessageDropped); - tracing::warn!(target: "network", ?msg, ?from, "Message dropped because TTL reached 0."); + tracing::warn!(target: "network", ?msg, from = ?conn.peer_info.id, "Message dropped because TTL reached 0."); metrics::ROUTED_MESSAGE_DROPPED .with_label_values(&[msg.body_variant()]) .inc(); @@ -1393,6 +1488,15 @@ impl actix::Handler for PeerActor { return; } conn.last_time_received_message.store(self.clock.now()); + // Check if the message type is allowed given the TIER of the connection: + // TIER1 connections are reserved exclusively for BFT consensus messages. + if !conn.tier.is_allowed(&peer_msg) { + tracing::warn!(target: "network", "Received {} on {:?} connection, disconnecting",peer_msg.msg_variant(),conn.tier); + // TODO(gprusak): this is abusive behavior. Consider banning for it. + self.stop(ctx, ClosingReason::DisallowedMessage); + return; + } + // Optionally, ignore any received tombstones after startup. This is to // prevent overload from too much accumulated deleted edges. // diff --git a/chain/network/src/peer/tests/communication.rs b/chain/network/src/peer/tests/communication.rs index 7a3963c6346..7334a2f102b 100644 --- a/chain/network/src/peer/tests/communication.rs +++ b/chain/network/src/peer/tests/communication.rs @@ -35,7 +35,8 @@ async fn test_peer_communication( network: chain.make_config(&mut rng), force_encoding: outbound_encoding, }; - let (outbound_stream, inbound_stream) = tcp::Stream::loopback(inbound_cfg.id()).await; + let (outbound_stream, inbound_stream) = + tcp::Stream::loopback(inbound_cfg.id(), tcp::Tier::T2).await; let mut inbound = PeerHandle::start_endpoint(clock.clock(), inbound_cfg, inbound_stream).await; let mut outbound = PeerHandle::start_endpoint(clock.clock(), outbound_cfg, outbound_stream).await; @@ -45,7 +46,7 @@ async fn test_peer_communication( let message_processed = |want| { move |ev| match ev { - Event::Network(PME::MessageProcessed(got)) if got == want => Some(()), + Event::Network(PME::MessageProcessed(_, got)) if got == want => Some(()), _ => None, } }; @@ -190,7 +191,8 @@ async fn test_handshake(outbound_encoding: Option, inbound_encoding: O chain: chain.clone(), force_encoding: outbound_encoding, }; - let (outbound_stream, inbound_stream) = tcp::Stream::loopback(inbound_cfg.id()).await; + let (outbound_stream, inbound_stream) = + tcp::Stream::loopback(inbound_cfg.id(), tcp::Tier::T2).await; let inbound = PeerHandle::start_endpoint(clock.clock(), inbound_cfg, inbound_stream).await; let outbound_port = outbound_stream.local_addr.port(); let mut outbound = Stream::new(outbound_encoding, outbound_stream); diff --git a/chain/network/src/peer/tests/stream.rs b/chain/network/src/peer/tests/stream.rs index ce9961b7b43..a28e5da41c1 100644 --- a/chain/network/src/peer/tests/stream.rs +++ b/chain/network/src/peer/tests/stream.rs @@ -67,7 +67,7 @@ impl Actor { #[tokio::test] async fn send_recv() { let mut rng = make_rng(98324532); - let (s1, s2) = tcp::Stream::loopback(data::make_peer_id(&mut rng)).await; + let (s1, s2) = tcp::Stream::loopback(data::make_peer_id(&mut rng), tcp::Tier::T2).await; let a1 = Actor::spawn(s1).await; let mut a2 = Actor::spawn(s2).await; diff --git a/chain/network/src/peer_manager/connection/mod.rs b/chain/network/src/peer_manager/connection/mod.rs index bf66bc654cd..89231af4cce 100644 --- a/chain/network/src/peer_manager/connection/mod.rs +++ b/chain/network/src/peer_manager/connection/mod.rs @@ -2,12 +2,14 @@ use crate::concurrency::arc_mutex::ArcMutex; use crate::concurrency::atomic_cell::AtomicCell; use crate::concurrency::demux; use crate::network_protocol::{ - Edge, PeerInfo, PeerMessage, SignedAccountData, SignedOwnedAccount, SyncAccountsData, + Edge, PeerInfo, PeerMessage, RoutedMessageBody, SignedAccountData, SignedOwnedAccount, + SyncAccountsData, }; use crate::peer::peer_actor; use crate::peer::peer_actor::PeerActor; use crate::private_actix::SendMessage; use crate::stats::metrics; +use crate::tcp; use crate::time; use crate::types::{BlockInfo, FullPeerInfo, PeerChainInfo, PeerType, ReasonForBan}; use arc_swap::ArcSwap; @@ -25,6 +27,31 @@ use std::sync::{Arc, Weak}; #[cfg(test)] mod tests; +impl tcp::Tier { + /// Checks if the given message type is allowed on a connection of the given Tier. + /// TIER1 is reserved exclusively for BFT consensus messages. + /// Each validator establishes a lot of TIER1 connections, so bandwidth shouldn't be + /// wasted on broadcasting or periodic state syncs on TIER1 connections. + pub(crate) fn is_allowed(self, msg: &PeerMessage) -> bool { + match msg { + PeerMessage::Tier1Handshake(_) => self == tcp::Tier::T1, + PeerMessage::Tier2Handshake(_) => self == tcp::Tier::T2, + PeerMessage::HandshakeFailure(_, _) => true, + PeerMessage::LastEdge(_) => true, + PeerMessage::Routed(msg) => self.is_allowed_routed(&msg.body), + _ => self == tcp::Tier::T2, + } + } + + pub(crate) fn is_allowed_routed(self, body: &RoutedMessageBody) -> bool { + match body { + RoutedMessageBody::BlockApproval(..) => true, + RoutedMessageBody::VersionedPartialEncodedChunk(..) => true, + _ => self == tcp::Tier::T2, + } + } +} + #[derive(Default)] pub(crate) struct Stats { /// Number of messages received since the last reset of the counter. @@ -44,6 +71,8 @@ pub(crate) struct Stats { /// Contains information relevant to a connected peer. pub(crate) struct Connection { + // TODO(gprusak): add rate limiting on TIER1 connections for defence in-depth. + pub tier: tcp::Tier, // TODO(gprusak): addr should be internal, so that Connection will become an API of the // PeerActor. pub addr: actix::Addr, diff --git a/chain/network/src/peer_manager/connection/tests.rs b/chain/network/src/peer_manager/connection/tests.rs index e2aec85b7ae..0af99d16368 100644 --- a/chain/network/src/peer_manager/connection/tests.rs +++ b/chain/network/src/peer_manager/connection/tests.rs @@ -3,6 +3,7 @@ use crate::peer::peer_actor::ClosingReason; use crate::peer_manager; use crate::peer_manager::connection; use crate::private_actix::RegisterPeerError; +use crate::tcp; use crate::testonly::make_rng; use crate::time; use near_o11y::testonly::init_test_logger; @@ -28,7 +29,7 @@ async fn connection_tie_break() { .await; // pm.id is lower - let outbound_conn = pm.start_outbound(chain.clone(), cfgs[2].clone()).await; + let outbound_conn = pm.start_outbound(chain.clone(), cfgs[2].clone(), tcp::Tier::T2).await; let inbound_conn = pm.start_inbound(chain.clone(), cfgs[2].clone()).await; // inbound should be rejected, outbound accepted. assert_eq!( @@ -40,7 +41,7 @@ async fn connection_tie_break() { outbound_conn.handshake(&clock.clock()).await; // pm.id is higher - let outbound_conn = pm.start_outbound(chain.clone(), cfgs[0].clone()).await; + let outbound_conn = pm.start_outbound(chain.clone(), cfgs[0].clone(), tcp::Tier::T2).await; let inbound_conn = pm.start_inbound(chain.clone(), cfgs[0].clone()).await; // inbound should be accepted, outbound rejected by PM. let inbound = inbound_conn.handshake(&clock.clock()).await; @@ -71,8 +72,8 @@ async fn duplicate_connections() { // Double outbound. let cfg = chain.make_config(rng); - let conn1 = pm.start_outbound(chain.clone(), cfg.clone()).await; - let conn2 = pm.start_outbound(chain.clone(), cfg.clone()).await; + let conn1 = pm.start_outbound(chain.clone(), cfg.clone(), tcp::Tier::T2).await; + let conn2 = pm.start_outbound(chain.clone(), cfg.clone(), tcp::Tier::T2).await; // conn2 shouldn't even be started, so it should fail before conn1 completes. assert_eq!( ClosingReason::OutboundNotAllowed(connection::PoolError::AlreadyStartedConnecting), @@ -99,7 +100,7 @@ async fn duplicate_connections() { let cfg = chain.make_config(rng); let conn1 = pm.start_inbound(chain.clone(), cfg.clone()).await; let conn1 = conn1.handshake(&clock.clock()).await; - let conn2 = pm.start_outbound(chain.clone(), cfg.clone()).await; + let conn2 = pm.start_outbound(chain.clone(), cfg.clone(), tcp::Tier::T2).await; assert_eq!( ClosingReason::OutboundNotAllowed(connection::PoolError::AlreadyConnected), conn2.manager_fail_handshake(&clock.clock()).await, @@ -108,7 +109,7 @@ async fn duplicate_connections() { // Outbound then inbound. let cfg = chain.make_config(rng); - let conn1 = pm.start_outbound(chain.clone(), cfg.clone()).await; + let conn1 = pm.start_outbound(chain.clone(), cfg.clone(), tcp::Tier::T2).await; let conn1 = conn1.handshake(&clock.clock()).await; let conn2 = pm.start_inbound(chain.clone(), cfg.clone()).await; assert_eq!( diff --git a/chain/network/src/peer_manager/network_state/mod.rs b/chain/network/src/peer_manager/network_state/mod.rs index 03d5fcb5795..67a2cfd3d38 100644 --- a/chain/network/src/peer_manager/network_state/mod.rs +++ b/chain/network/src/peer_manager/network_state/mod.rs @@ -12,6 +12,7 @@ use crate::peer_manager::connection; use crate::peer_manager::peer_manager_actor::Event; use crate::peer_manager::peer_store; use crate::private_actix::RegisterPeerError; +use crate::routing::route_back_cache::RouteBackCache; use crate::stats::metrics; use crate::store; use crate::tcp; @@ -89,6 +90,7 @@ pub(crate) struct NetworkState { pub accounts_data: Arc, /// Connected peers (inbound and outbound) with their full peer information. pub tier2: connection::Pool, + pub tier1: connection::Pool, /// Semaphore limiting inflight inbound handshakes. pub inbound_handshake_permits: Arc, /// Peer store that provides read/write access to peers. @@ -96,6 +98,15 @@ pub(crate) struct NetworkState { /// A graph of the whole NEAR network. pub graph: Arc, + /// Hash of messages that requires routing back to respective previous hop. + /// Currently unused, as TIER1 messages do not require a response. + /// Also TIER1 connections are direct by design (except for proxies), + /// so routing shouldn't really be needed. + /// TODO(gprusak): consider removing it altogether. + /// + /// Note that the route_back table for TIER2 is stored in graph.routing_table_view. + pub tier1_route_back: Mutex, + /// Shared counter across all PeerActors, which counts number of `RoutedMessageBody::ForwardTx` /// messages sincce last block. pub txns_since_last_block: AtomicUsize, @@ -110,6 +121,8 @@ pub(crate) struct NetworkState { /// in the first place. pub max_num_peers: AtomicU32, + /// Mutex which prevents overlapping calls to tier1_advertise_proxies. + tier1_advertise_proxies_mutex: tokio::sync::Mutex<()>, /// Demultiplexer aggregating calls to add_edges(). add_edges_demux: demux::Demux, ()>, @@ -142,9 +155,11 @@ impl NetworkState { client, chain_info: Default::default(), tier2: connection::Pool::new(config.node_id()), + tier1: connection::Pool::new(config.node_id()), inbound_handshake_permits: Arc::new(tokio::sync::Semaphore::new(LIMIT_PENDING_PEERS)), peer_store, accounts_data: Arc::new(accounts_data::Cache::new()), + tier1_route_back: Mutex::new(RouteBackCache::default()), txns_since_last_block: AtomicUsize::new(0), whitelist_nodes, max_num_peers: AtomicU32::new(config.max_num_peers), @@ -152,6 +167,7 @@ impl NetworkState { set_chain_info_mutex: Mutex::new(()), config, created_at: clock.now(), + tier1_advertise_proxies_mutex: tokio::sync::Mutex::new(()), } } @@ -243,28 +259,48 @@ impl NetworkState { return Err(RegisterPeerError::Banned); } - if conn.peer_type == PeerType::Inbound { - if !this.is_inbound_allowed(&peer_info) { - // TODO(1896): Gracefully drop inbound connection for other peer. - let tier2 = this.tier2.load(); - tracing::debug!(target: "network", - tier2 = tier2.ready.len(), outgoing_peers = tier2.outbound_handshakes.len(), - max_num_peers = this.max_num_peers.load(Ordering::Relaxed), - "Dropping handshake (network at max capacity)." - ); - return Err(RegisterPeerError::ConnectionLimitExceeded); + match conn.tier { + tcp::Tier::T1 => { + if conn.peer_type == PeerType::Inbound { + if !this.config.tier1.as_ref().map_or(false, |c| c.enable_inbound) { + return Err(RegisterPeerError::Tier1InboundDisabled); + } + // Allow for inbound TIER1 connections only directly from a TIER1 peers. + let owned_account = match &conn.owned_account { + Some(it) => it, + None => return Err(RegisterPeerError::NotTier1Peer), + }; + if !this.accounts_data.load().keys.contains(&owned_account.account_key) { + return Err(RegisterPeerError::NotTier1Peer); + } + } + this.tier1.insert_ready(conn).map_err(RegisterPeerError::PoolError)?; + } + tcp::Tier::T2 => { + if conn.peer_type == PeerType::Inbound { + if !this.is_inbound_allowed(&peer_info) { + // TODO(1896): Gracefully drop inbound connection for other peer. + let tier2 = this.tier2.load(); + tracing::debug!(target: "network", + tier2 = tier2.ready.len(), outgoing_peers = tier2.outbound_handshakes.len(), + max_num_peers = this.max_num_peers.load(Ordering::Relaxed), + "Dropping handshake (network at max capacity)." + ); + return Err(RegisterPeerError::ConnectionLimitExceeded); + } + } + // Verify and broadcast the edge of the connection. Only then insert the new + // connection to TIER2 pool, so that nothing is broadcasted to conn. + // TODO(gprusak): consider actually banning the peer for consistency. + this.add_edges(&clock, vec![conn.edge.load().as_ref().clone()]) + .await + .map_err(|_: ReasonForBan| RegisterPeerError::InvalidEdge)?; + this.tier2.insert_ready(conn.clone()).map_err(RegisterPeerError::PoolError)?; + // Best effort write to DB. + if let Err(err) = this.peer_store.peer_connected(&clock, peer_info) { + tracing::error!(target: "network", ?err, "Failed to save peer data"); + } } - } - // Verify and broadcast the edge of the connection. Only then insert the new - // connection to TIER2 pool, so that nothing is broadcasted to conn. - // TODO(gprusak): consider actually banning the peer for consistency. - this.add_edges(&clock, vec![conn.edge.load().as_ref().clone()]) - .await - .map_err(|_: ReasonForBan| RegisterPeerError::InvalidEdge)?; - this.tier2.insert_ready(conn.clone()).map_err(RegisterPeerError::PoolError)?; - // Best effort write to DB. - if let Err(err) = this.peer_store.peer_connected(&clock, peer_info) { - tracing::error!(target: "network", ?err, "Failed to save peer data"); } Ok(()) }).await.unwrap() @@ -286,6 +322,12 @@ impl NetworkState { let conn = conn.clone(); self.spawn(async move { let peer_id = conn.peer_info.id.clone(); + if conn.tier == tcp::Tier::T1 { + // There is no banning or routing table for TIER1. + // Just remove the connection from the network_state. + this.tier1.remove(&conn); + return; + } this.tier2.remove(&conn); // If the last edge we have with this peer represent a connection addition, create the edge @@ -325,16 +367,16 @@ impl NetworkState { } } - pub fn send_ping(&self, clock: &time::Clock, nonce: u64, target: PeerId) { + pub fn send_ping(&self, clock: &time::Clock, tier: tcp::Tier, nonce: u64, target: PeerId) { let body = RoutedMessageBody::Ping(Ping { nonce, source: self.config.node_id() }); let msg = RawRoutedMessage { target: PeerIdOrHash::PeerId(target), body }; - self.send_message_to_peer(clock, self.sign_message(clock, msg)); + self.send_message_to_peer(clock, tier, self.sign_message(clock, msg)); } - pub fn send_pong(&self, clock: &time::Clock, nonce: u64, target: CryptoHash) { + pub fn send_pong(&self, clock: &time::Clock, tier: tcp::Tier, nonce: u64, target: CryptoHash) { let body = RoutedMessageBody::Pong(Pong { nonce, source: self.config.node_id() }); let msg = RawRoutedMessage { target: PeerIdOrHash::Hash(target), body }; - self.send_message_to_peer(clock, self.sign_message(clock, msg)); + self.send_message_to_peer(clock, tier, self.sign_message(clock, msg)); } pub fn sign_message(&self, clock: &time::Clock, msg: RawRoutedMessage) -> Box { @@ -347,7 +389,12 @@ impl NetworkState { /// Route signed message to target peer. /// Return whether the message is sent or not. - pub fn send_message_to_peer(&self, clock: &time::Clock, msg: Box) -> bool { + pub fn send_message_to_peer( + &self, + clock: &time::Clock, + tier: tcp::Tier, + msg: Box, + ) -> bool { let my_peer_id = self.config.node_id(); // Check if the message is for myself and don't try to send it in that case. @@ -358,40 +405,87 @@ impl NetworkState { return false; } } - match self.graph.routing_table.find_route(&clock, &msg.target) { - Ok(peer_id) => { - // Remember if we expect a response for this message. - if msg.author == my_peer_id && msg.expect_response() { - tracing::trace!(target: "network", ?msg, "initiate route back"); - self.graph.routing_table.add_route_back(&clock, msg.hash(), my_peer_id); - } - return self.tier2.send_message(peer_id, Arc::new(PeerMessage::Routed(msg))); + match tier { + tcp::Tier::T1 => { + let peer_id = match &msg.target { + // If a message is a response, we try to load the target from the route back + // cache. + PeerIdOrHash::Hash(hash) => { + match self.tier1_route_back.lock().remove(clock, hash) { + Some(peer_id) => peer_id, + None => return false, + } + } + PeerIdOrHash::PeerId(peer_id) => peer_id.clone(), + }; + return self.tier1.send_message(peer_id, Arc::new(PeerMessage::Routed(msg))); } - Err(find_route_error) => { - // TODO(MarX, #1369): Message is dropped here. Define policy for this case. - metrics::MessageDropped::NoRouteFound.inc(&msg.body); + tcp::Tier::T2 => match self.graph.routing_table.find_route(&clock, &msg.target) { + Ok(peer_id) => { + // Remember if we expect a response for this message. + if msg.author == my_peer_id && msg.expect_response() { + tracing::trace!(target: "network", ?msg, "initiate route back"); + self.graph.routing_table.add_route_back(&clock, msg.hash(), my_peer_id); + } + return self.tier2.send_message(peer_id, Arc::new(PeerMessage::Routed(msg))); + } + Err(find_route_error) => { + // TODO(MarX, #1369): Message is dropped here. Define policy for this case. + metrics::MessageDropped::NoRouteFound.inc(&msg.body); - tracing::debug!(target: "network", - account_id = ?self.config.validator.as_ref().map(|v|v.account_id()), - to = ?msg.target, - reason = ?find_route_error, - known_peers = ?self.graph.routing_table.reachable_peers(), - msg = ?msg.body, - "Drop signed message" - ); - return false; - } + tracing::debug!(target: "network", + account_id = ?self.config.validator.as_ref().map(|v|v.account_id()), + to = ?msg.target, + reason = ?find_route_error, + known_peers = ?self.graph.routing_table.reachable_peers(), + msg = ?msg.body, + "Drop signed message" + ); + return false; + } + }, } } /// Send message to specific account. /// Return whether the message is sent or not. + /// The message might be sent over TIER1 and/or TIER2 connection depending on the message type. pub fn send_message_to_account( &self, clock: &time::Clock, account_id: &AccountId, msg: RoutedMessageBody, ) -> bool { + let mut success = false; + // All TIER1 messages are being sent over both TIER1 and TIER2 connections for now, + // so that we can actually observe the latency/reliability improvements in practice: + // for each message we track over which network tier it arrived faster? + if tcp::Tier::T1.is_allowed_routed(&msg) { + let accounts_data = self.accounts_data.load(); + for key in accounts_data.keys_by_id.get(account_id).iter().flat_map(|keys| keys.iter()) + { + let data = match accounts_data.data.get(key) { + Some(data) => data, + None => continue, + }; + let conn = match self.get_tier1_proxy(data) { + Some(conn) => conn, + None => continue, + }; + // TODO(gprusak): in case of PartialEncodedChunk, consider stripping everything + // but the header. This will bound the message size + conn.send_message(Arc::new(PeerMessage::Routed(self.sign_message( + clock, + RawRoutedMessage { + target: PeerIdOrHash::PeerId(data.peer_id.clone()), + body: msg.clone(), + }, + )))); + success |= true; + break; + } + } + let target = match self.graph.routing_table.account_owner(account_id) { Some(peer_id) => peer_id, None => { @@ -410,14 +504,13 @@ impl NetworkState { let msg = RawRoutedMessage { target: PeerIdOrHash::PeerId(target), body: msg }; let msg = self.sign_message(clock, msg); if msg.body.is_important() { - let mut success = false; for _ in 0..IMPORTANT_MESSAGE_RESENT_COUNT { - success |= self.send_message_to_peer(clock, msg.clone()); + success |= self.send_message_to_peer(clock, tcp::Tier::T2, msg.clone()); } - success } else { - self.send_message_to_peer(clock, msg) + success |= self.send_message_to_peer(clock, tcp::Tier::T2, msg) } + success } pub async fn add_accounts_data( diff --git a/chain/network/src/peer_manager/network_state/tier1.rs b/chain/network/src/peer_manager/network_state/tier1.rs index f9ecbe6e486..23d29065333 100644 --- a/chain/network/src/peer_manager/network_state/tier1.rs +++ b/chain/network/src/peer_manager/network_state/tier1.rs @@ -1,8 +1,20 @@ use crate::accounts_data; use crate::config; -use crate::network_protocol::{AccountData, PeerMessage, SignedAccountData, SyncAccountsData}; +use crate::network_protocol::{ + AccountData, PeerAddr, PeerInfo, PeerMessage, SignedAccountData, SyncAccountsData, +}; +use crate::peer::peer_actor::PeerActor; +use crate::peer_manager::connection; use crate::peer_manager::peer_manager_actor::Event; +use crate::tcp; use crate::time; +use crate::types::PeerType; +use near_crypto::PublicKey; +use near_o11y::log_assert; +use near_primitives::network::PeerId; +use rand::seq::IteratorRandom as _; +use rand::seq::SliceRandom as _; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; impl super::NetworkState { @@ -20,6 +32,40 @@ impl super::NetworkState { .filter(|cfg| accounts_data.keys.contains(&cfg.signer.public_key())) } + async fn tier1_connect_to_my_proxies( + self: &Arc, + clock: &time::Clock, + proxies: &[PeerAddr], + ) { + let tier1 = self.tier1.load(); + // Try to connect to all proxies in parallel. + let mut handles = vec![]; + for proxy in proxies { + // Skip the proxies we are already connected to. + if tier1.ready.contains_key(&proxy.peer_id) { + continue; + } + handles.push(async move { + let res = async { + let stream = tcp::Stream::connect( + &PeerInfo { + id: proxy.peer_id.clone(), + addr: Some(proxy.addr), + account_id: None, + }, + tcp::Tier::T1, + ) + .await?; + anyhow::Ok(PeerActor::spawn_and_handshake(clock.clone(), stream, None, self.clone()).await?) + }.await; + if let Err(err) = res { + tracing::warn!(target:"network", ?err, "failed to establish connection to TIER1 proxy {:?}",proxy); + } + }); + } + futures_util::future::join_all(handles).await; + } + /// Tries to connect to ALL trusted proxies from the config, then broadcasts AccountData with /// the set of proxies it managed to connect to. This way other TIER1 nodes can just connect /// to ANY proxy of this node. @@ -27,16 +73,80 @@ impl super::NetworkState { self: &Arc, clock: &time::Clock, ) -> Vec> { + // Tier1 advertise proxies calls should be disjoint, + // to avoid a race condition while connecting to the proxies. + // TODO(gprusak): there are more corner cases to cover, because + // tier1_connect may also spawn TIER1 connections conflicting with + // tier1_advertise_proxies. It would be better to be able to await + // handshake on connection attempts, even if another call spawned them. + let _lock = self.tier1_advertise_proxies_mutex.lock().await; let accounts_data = self.accounts_data.load(); - let Some(vc) = self.tier1_validator_config(&accounts_data) else { - return vec![]; + let vc = match self.tier1_validator_config(&accounts_data) { + Some(it) => it, + None => { + return vec![]; + } + }; + let proxies = match &vc.proxies { + config::ValidatorProxies::Dynamic(_) => { + // TODO(gprusak): If Dynamic are specified, + // it means that this node is its own proxy. + // Resolve the public IP of this node using those STUN servers, + // then connect to yourself (to verify the public IP). + vec![] + } + config::ValidatorProxies::Static(peer_addrs) => peer_addrs.clone(), }; - // TODO(gprusak): for now we just blindly broadcast the static list of proxies, however - // here we should try to connect to the TIER1 proxies, before broadcasting them. + self.tier1_connect_to_my_proxies(clock, &proxies).await; + + // Snapshot tier1 connections again before broadcasting. + let tier1 = self.tier1.load(); + let my_proxies = match &vc.proxies { - config::ValidatorProxies::Dynamic(_) => vec![], - config::ValidatorProxies::Static(proxies) => proxies.clone(), + // In case of dynamic configuration, only the node itself can be its proxy, + // so we look for a loop connection which would prove our node's address. + config::ValidatorProxies::Dynamic(_) => match tier1.ready.get(&self.config.node_id()) { + Some(conn) => { + log_assert!(PeerType::Outbound == conn.peer_type); + log_assert!(conn.peer_info.addr.is_some()); + match conn.peer_info.addr { + Some(addr) => vec![PeerAddr { peer_id: self.config.node_id(), addr }], + None => vec![], + } + } + None => vec![], + }, + // In case of static configuration, we look for connections to proxies matching the config. + config::ValidatorProxies::Static(proxies) => { + let mut connected_proxies = vec![]; + for proxy in proxies { + match tier1.ready.get(&proxy.peer_id) { + // Here we compare the address from the config with the + // address of the connection (which is the IP, to which the + // TCP socket is connected + port indicated by the peer). + // We will broadcast only those addresses which we confirmed are + // valid (i.e. we managed to connect to them). + // + // TODO(gprusak): It may happen that a single peer will be + // available under multiple IPs, in which case, we should + // prefer to connect to the IP from the config, however + // that would require having separate inbound and outbound + // pools, so that both endpoints can keep a connection + // to the IP that they prefer. This is a corner case which can happen + // only if 2 TIER1 validators are proxies for some other validator. + Some(conn) if conn.peer_info.addr == Some(proxy.addr) => { + connected_proxies.push(proxy.clone()); + } + Some(conn) => { + tracing::info!(target:"network", "connected to {}, but got addr {:?}, while want {}",conn.peer_info.id,conn.peer_info.addr,proxy.addr) + } + _ => {} + } + } + connected_proxies + } }; + tracing::info!(target:"network","connected to proxies {my_proxies:?}"); let now = clock.now_utc(); let version = self.accounts_data.load().data.get(&vc.signer.public_key()).map_or(0, |d| d.version) @@ -77,4 +187,165 @@ impl super::NetworkState { self.config.event_sink.push(Event::Tier1AdvertiseProxies(new_data.clone())); new_data } + + /// Closes TIER1 connections from nodes which are not TIER1 any more. + /// If this node is TIER1, it additionally connects to proxies of other TIER1 nodes. + pub async fn tier1_connect(self: &Arc, clock: &time::Clock) { + let tier1_cfg = match &self.config.tier1 { + Some(it) => it, + None => return, + }; + if !tier1_cfg.enable_outbound { + return; + } + let accounts_data = self.accounts_data.load(); + let validator_cfg = self.tier1_validator_config(&accounts_data); + + // Construct indices on accounts_data. + let mut accounts_by_proxy = HashMap::<_, Vec<_>>::new(); + let mut proxies_by_account = HashMap::<_, Vec<_>>::new(); + for d in accounts_data.data.values() { + proxies_by_account.entry(&d.account_key).or_default().extend(d.proxies.iter()); + for p in &d.proxies { + accounts_by_proxy.entry(&p.peer_id).or_default().push(&d.account_key); + } + } + + // Browse the connections from newest to oldest. + let tier1 = self.tier1.load(); + let mut ready: Vec<_> = tier1.ready.values().collect(); + ready.sort_unstable_by_key(|c| c.established_time); + ready.reverse(); + + // Select the oldest TIER1 connection for each account. + let mut safe = HashMap::<&PublicKey, &PeerId>::new(); + + match validator_cfg { + // TIER1 nodes can establish outbound connections to other TIER1 nodes and TIER1 proxies. + // TIER1 nodes can also accept inbound connections from TIER1 nodes. + Some(_) => { + for conn in &ready { + if conn.peer_type != PeerType::Outbound { + continue; + } + let peer_id = &conn.peer_info.id; + for key in accounts_by_proxy.get(peer_id).into_iter().flatten() { + safe.insert(key, peer_id); + } + } + // Direct TIER1 connections have priority over proxy connections. + for key in &accounts_data.keys { + if let Some(conn) = tier1.ready_by_account_key.get(&key) { + safe.insert(key, &conn.peer_info.id); + } + } + } + // All the other nodes should accept inbound connections from TIER1 nodes + // (to act as a TIER1 proxy). + None => { + for key in &accounts_data.keys { + if let Some(conn) = tier1.ready_by_account_key.get(&key) { + if conn.peer_type == PeerType::Inbound { + safe.insert(key, &conn.peer_info.id); + } + } + } + } + } + + // Construct a safe set of connections. + let mut safe_set: HashSet = safe.values().map(|v| (*v).clone()).collect(); + // Add proxies of our node to the safe set. + if let Some(vc) = validator_cfg { + match &vc.proxies { + config::ValidatorProxies::Dynamic(_) => { + safe_set.insert(self.config.node_id()); + } + config::ValidatorProxies::Static(peer_addrs) => { + // TODO(gprusak): here we add peer_id to a safe set, even if + // the conn.peer_addr doesn't match the address from the validator config + // (so we cannot advertise it as our proxy). Consider making it more precise. + safe_set.extend(peer_addrs.iter().map(|pa| pa.peer_id.clone())); + } + } + } + // Close all other connections, as they are redundant or are no longer TIER1. + for conn in tier1.ready.values() { + if !safe_set.contains(&conn.peer_info.id) { + conn.stop(None); + } + } + if let Some(vc) = validator_cfg { + // Try to establish new TIER1 connections to accounts in random order. + let mut handles = vec![]; + let mut account_keys: Vec<_> = proxies_by_account.keys().copied().collect(); + account_keys.shuffle(&mut rand::thread_rng()); + for account_key in account_keys { + // tier1_connect() is responsible for connecting to proxies + // of this node. tier1_connect() connects only to proxies + // of other TIER1 nodes. + if account_key == &vc.signer.public_key() { + continue; + } + // Bound the number of connections established at a single call to + // tier1_connect(). + if handles.len() as u64 >= tier1_cfg.new_connections_per_attempt { + break; + } + // If we are already connected to some proxy of account_key, then + // don't establish another connection. + if safe.contains_key(account_key) { + continue; + } + // Find addresses of proxies of account_key. + let proxies: Vec<&PeerAddr> = + proxies_by_account.get(account_key).into_iter().flatten().map(|x| *x).collect(); + // Select a random proxy of the account_key and try to connect to it. + let proxy = proxies.iter().choose(&mut rand::thread_rng()); + if let Some(proxy) = proxy { + let proxy = (*proxy).clone(); + handles.push(async move { + let stream = tcp::Stream::connect( + &PeerInfo { + id: proxy.peer_id, + addr: Some(proxy.addr), + account_id: None, + }, + tcp::Tier::T1, + ) + .await?; + PeerActor::spawn_and_handshake(clock.clone(), stream, None, self.clone()) + .await + }); + } + } + tracing::debug!(target:"network","{}: establishing {} new connections",self.config.node_id(),handles.len()); + for res in futures_util::future::join_all(handles).await { + if let Err(err) = res { + tracing::info!(target:"network", ?err, "{}: failed to establish a TIER1 connection",self.config.node_id()); + } + } + tracing::debug!(target:"network","{}: establishing new connections DONE",self.config.node_id()); + } + } + + /// Finds a TIER1 connection for the given SignedAccountData. + /// It is expected to perform <10 lookups total on average, + /// so the call latency should be negligible wrt sending a TCP packet. + // TODO(gprusak): If not, consider precomputing the AccountKey -> Connection mapping. + pub fn get_tier1_proxy(&self, data: &SignedAccountData) -> Option> { + let tier1 = self.tier1.load(); + // Prefer direct connections. + if let Some(conn) = tier1.ready_by_account_key.get(&data.account_key) { + return Some(conn.clone()); + } + // In case there is no direct connection and our node is a TIER1 validator, use a proxy. + // TODO(gprusak): add a check that our node is actually a TIER1 validator. + for proxy in &data.proxies { + if let Some(conn) = tier1.ready.get(&proxy.peer_id) { + return Some(conn.clone()); + } + } + None + } } diff --git a/chain/network/src/peer_manager/peer_manager_actor.rs b/chain/network/src/peer_manager/peer_manager_actor.rs index 33f3bda4248..351705fa8be 100644 --- a/chain/network/src/peer_manager/peer_manager_actor.rs +++ b/chain/network/src/peer_manager/peer_manager_actor.rs @@ -2,8 +2,8 @@ use crate::client; use crate::config; use crate::debug::{DebugStatus, GetDebugStatus}; use crate::network_protocol::{ - AccountOrPeerIdOrHash, Edge, PeerMessage, Ping, Pong, RawRoutedMessage, RoutedMessageBody, - SignedAccountData, StateResponseInfo, + AccountOrPeerIdOrHash, Edge, PeerIdOrHash, PeerMessage, Ping, Pong, RawRoutedMessage, + RoutedMessageBody, SignedAccountData, StateResponseInfo, }; use crate::peer::peer_actor::PeerActor; use crate::peer_manager::connection; @@ -15,8 +15,8 @@ use crate::tcp; use crate::time; use crate::types::{ ConnectedPeerInfo, GetNetworkInfo, HighestHeightPeerInfo, KnownProducer, NetworkInfo, - NetworkRequests, NetworkResponses, PeerIdOrHash, PeerManagerMessageRequest, - PeerManagerMessageResponse, PeerType, SetChainInfo, + NetworkRequests, NetworkResponses, PeerManagerMessageRequest, PeerManagerMessageResponse, + PeerType, SetChainInfo, }; use actix::fut::future::wrap_future; use actix::{Actor as _, AsyncContext as _}; @@ -25,9 +25,7 @@ use near_o11y::{handler_debug_span, handler_trace_span, OpenTelemetrySpanExt, Wi use near_performance_metrics_macros::perf; use near_primitives::block::GenesisId; use near_primitives::network::{AnnounceAccount, PeerId}; -use near_primitives::views::EdgeView; -use near_primitives::views::NetworkGraphView; -use near_primitives::views::{KnownPeerStateView, PeerStoreView}; +use near_primitives::views::{EdgeView, KnownPeerStateView, NetworkGraphView, PeerStoreView}; use rand::seq::IteratorRandom; use rand::thread_rng; use rand::Rng; @@ -78,6 +76,7 @@ pub struct PeerManagerActor { my_peer_id: PeerId, /// Flag that track whether we started attempts to establish outbound connections. started_connect_attempts: bool, + /// State that is shared between multiple threads (including PeerActors). pub(crate) state: Arc, } @@ -106,7 +105,7 @@ pub enum Event { // it is hard to pinpoint all the places when the processing of a message is // actually complete. Currently this event is reported only for some message types, // feel free to add support for more. - MessageProcessed(PeerMessage), + MessageProcessed(tcp::Tier, PeerMessage), // Reported every time a new list of proxies has been constructed. Tier1AdvertiseProxies(Vec>), // Reported when a handshake has been started. @@ -248,6 +247,19 @@ impl PeerManagerActor { } } }); + // Update TIER1 connections periodically. + arbiter.spawn({ + let clock = clock.clone(); + let state = state.clone(); + let mut interval = tokio::time::interval(cfg.connect_interval.try_into().unwrap()); + interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + async move { + loop { + interval.tick().await; + state.tier1_connect(&clock).await; + } + } + }); } } }); @@ -516,7 +528,7 @@ impl PeerManagerActor { let clock = self.clock.clone(); async move { let result = async { - let stream = tcp::Stream::connect(&peer_info).await.context("tcp::Stream::connect()")?; + let stream = tcp::Stream::connect(&peer_info, tcp::Tier::T2).await.context("tcp::Stream::connect()")?; PeerActor::spawn_and_handshake(clock.clone(),stream,None,state.clone()).await.context("PeerActor::spawn()")?; anyhow::Ok(()) }.await; @@ -571,6 +583,7 @@ impl PeerManagerActor { self.state.send_message_to_peer( &self.clock, + tcp::Tier::T2, self.state.sign_message(&self.clock, RawRoutedMessage { target, body: msg }), ) } @@ -719,6 +732,7 @@ impl PeerManagerActor { }; 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 }, @@ -776,6 +790,7 @@ impl PeerManagerActor { { if self.state.send_message_to_peer( &self.clock, + tcp::Tier::T2, self.state.sign_message( &self.clock, RawRoutedMessage { @@ -805,6 +820,7 @@ impl PeerManagerActor { NetworkRequests::PartialEncodedChunkResponse { route_back, response } => { if self.state.send_message_to_peer( &self.clock, + tcp::Tier::T2, self.state.sign_message( &self.clock, RawRoutedMessage { @@ -908,7 +924,7 @@ impl PeerManagerActor { } // TEST-ONLY PeerManagerMessageRequest::PingTo { nonce, target } => { - self.state.send_ping(&self.clock, nonce, target); + self.state.send_ping(&self.clock, tcp::Tier::T2, nonce, target); PeerManagerMessageResponse::PingTo } } diff --git a/chain/network/src/peer_manager/testonly.rs b/chain/network/src/peer_manager/testonly.rs index be51154dd98..653a0d92f73 100644 --- a/chain/network/src/peer_manager/testonly.rs +++ b/chain/network/src/peer_manager/testonly.rs @@ -15,7 +15,6 @@ use crate::testonly::fake_client; use crate::time; use crate::types::{ AccountKeys, ChainInfo, KnownPeerStatus, NetworkRequests, PeerManagerMessageRequest, - SetChainInfo, }; use crate::PeerManagerActor; use near_o11y::WithSpanContextExt; @@ -57,7 +56,10 @@ pub(crate) struct ActorHandler { pub fn unwrap_sync_accounts_data_processed(ev: Event) -> Option { match ev { - Event::PeerManager(PME::MessageProcessed(PeerMessage::SyncAccountsData(msg))) => Some(msg), + Event::PeerManager(PME::MessageProcessed( + tcp::Tier::T2, + PeerMessage::SyncAccountsData(msg), + )) => Some(msg), _ => None, } } @@ -135,12 +137,16 @@ impl ActorHandler { } } - pub fn connect_to(&self, peer_info: &PeerInfo) -> impl 'static + Send + Future { + pub fn connect_to( + &self, + peer_info: &PeerInfo, + tier: tcp::Tier, + ) -> impl 'static + Send + Future { let addr = self.actix.addr.clone(); let events = self.events.clone(); let peer_info = peer_info.clone(); async move { - let stream = tcp::Stream::connect(&peer_info).await.unwrap(); + let stream = tcp::Stream::connect(&peer_info, tier).await.unwrap(); let mut events = events.from_now(); let stream_id = stream.id(); addr.do_send(PeerManagerMessageRequest::OutboundTcpConnect(stream).with_span_context()); @@ -186,7 +192,7 @@ impl ActorHandler { // 3. establish connection. let socket = tcp::Socket::bind_v4(); let events = self.events.from_now(); - let stream = socket.connect(&self.peer_info()).await; + let stream = socket.connect(&self.peer_info(), tcp::Tier::T2).await; let stream_id = stream.id(); let conn = RawConnection { events, @@ -218,8 +224,10 @@ impl ActorHandler { &self, chain: Arc, network_cfg: config::NetworkConfig, + tier: tcp::Tier, ) -> RawConnection { - let (outbound_stream, inbound_stream) = tcp::Stream::loopback(network_cfg.node_id()).await; + let (outbound_stream, inbound_stream) = + tcp::Stream::loopback(network_cfg.node_id(), tier).await; let stream_id = outbound_stream.id(); let events = self.events.from_now(); self.actix.addr.do_send( @@ -382,6 +390,15 @@ impl ActorHandler { .await; } } + + /// Executes `NetworkState::tier1_connect` method. + pub async fn tier1_connect(&self, clock: &time::Clock) { + let clock = clock.clone(); + self.with_state(move |s| async move { + s.tier1_connect(&clock).await; + }) + .await; + } } pub(crate) async fn start( @@ -405,6 +422,6 @@ pub(crate) async fn start( let mut h = ActorHandler { cfg, actix, events: recv }; // Wait for the server to start. assert_eq!(Event::PeerManager(PME::ServerStarted), h.events.recv().await); - h.actix.addr.send(SetChainInfo(chain.get_chain_info()).with_span_context()).await.unwrap(); + h.set_chain_info(chain.get_chain_info()).await; h } diff --git a/chain/network/src/peer_manager/tests/accounts_data.rs b/chain/network/src/peer_manager/tests/accounts_data.rs index babf946666c..1a1a7b62c97 100644 --- a/chain/network/src/peer_manager/tests/accounts_data.rs +++ b/chain/network/src/peer_manager/tests/accounts_data.rs @@ -5,6 +5,7 @@ use crate::peer; use crate::peer_manager; use crate::peer_manager::peer_manager_actor::Event as PME; use crate::peer_manager::testonly; +use crate::tcp; use crate::testonly::{make_rng, AsSet as _}; use crate::time; use crate::types::PeerMessage; @@ -42,15 +43,17 @@ async fn broadcast() { .await; let take_incremental_sync = |ev| match ev { - peer::testonly::Event::Network(PME::MessageProcessed(PeerMessage::SyncAccountsData( - msg, - ))) if msg.incremental => Some(msg), + peer::testonly::Event::Network(PME::MessageProcessed( + tcp::Tier::T2, + PeerMessage::SyncAccountsData(msg), + )) if msg.incremental => Some(msg), _ => None, }; let take_full_sync = |ev| match ev { - peer::testonly::Event::Network(PME::MessageProcessed(PeerMessage::SyncAccountsData( - msg, - ))) if !msg.incremental => Some(msg), + peer::testonly::Event::Network(PME::MessageProcessed( + tcp::Tier::T2, + PeerMessage::SyncAccountsData(msg), + )) if !msg.incremental => Some(msg), _ => None, }; @@ -130,8 +133,8 @@ async fn gradual_epoch_change() { // 0 <-> 1 <-> 2 let pm1 = pms[1].peer_info(); let pm2 = pms[2].peer_info(); - pms[0].connect_to(&pm1).await; - pms[1].connect_to(&pm2).await; + pms[0].connect_to(&pm1, tcp::Tier::T2).await; + pms[1].connect_to(&pm2, tcp::Tier::T2).await; // For every order of nodes. for ids in (0..pms.len()).permutations(pms.len()) { @@ -204,7 +207,7 @@ async fn rate_limiting() { for j in 0..m { for k in 0..m { let pi = pms[(i + 1) * m + k].peer_info(); - tasks.push(tokio::spawn(pms[i * m + j].connect_to(&pi))); + tasks.push(tokio::spawn(pms[i * m + j].connect_to(&pi, tcp::Tier::T2))); connections += 1; } } diff --git a/chain/network/src/peer_manager/tests/connection_pool.rs b/chain/network/src/peer_manager/tests/connection_pool.rs index 35b006f21a2..4258425964b 100644 --- a/chain/network/src/peer_manager/tests/connection_pool.rs +++ b/chain/network/src/peer_manager/tests/connection_pool.rs @@ -75,15 +75,15 @@ async fn loop_connection() { let mut cfg = chain.make_config(rng); cfg.node_key = pm.cfg.node_key.clone(); - // Starting an outbound loop connection should be stopped without sending the handshake. - let conn = pm.start_outbound(chain.clone(), cfg).await; + // Starting an outbound loop connection on TIER2 should be stopped without sending the handshake. + let conn = pm.start_outbound(chain.clone(), cfg, tcp::Tier::T2).await; assert_eq!( ClosingReason::OutboundNotAllowed(connection::PoolError::UnexpectedLoopConnection), conn.manager_fail_handshake(&clock.clock()).await ); // An inbound connection pretending to be a loop should be rejected. - let stream = tcp::Stream::connect(&pm.peer_info()).await.unwrap(); + let stream = tcp::Stream::connect(&pm.peer_info(), tcp::Tier::T2).await.unwrap(); let stream_id = stream.id(); let port = stream.local_addr.port(); let mut events = pm.events.from_now(); @@ -141,7 +141,7 @@ async fn owned_account_mismatch() { .await; // An inbound connection pretending to be a loop should be rejected. - let stream = tcp::Stream::connect(&pm.peer_info()).await.unwrap(); + let stream = tcp::Stream::connect(&pm.peer_info(), tcp::Tier::T2).await.unwrap(); let stream_id = stream.id(); let port = stream.local_addr.port(); let mut events = pm.events.from_now(); diff --git a/chain/network/src/peer_manager/tests/mod.rs b/chain/network/src/peer_manager/tests/mod.rs index f555e7d18f6..e6254d60f42 100644 --- a/chain/network/src/peer_manager/tests/mod.rs +++ b/chain/network/src/peer_manager/tests/mod.rs @@ -2,3 +2,4 @@ mod accounts_data; mod connection_pool; mod nonce; mod routing; +mod tier1; diff --git a/chain/network/src/peer_manager/tests/nonce.rs b/chain/network/src/peer_manager/tests/nonce.rs index fe094cb10e1..d447af00416 100644 --- a/chain/network/src/peer_manager/tests/nonce.rs +++ b/chain/network/src/peer_manager/tests/nonce.rs @@ -60,7 +60,7 @@ async fn test_nonces() { ) .await; - let stream = tcp::Stream::connect(&pm.peer_info()).await.unwrap(); + let stream = tcp::Stream::connect(&pm.peer_info(), tcp::Tier::T2).await.unwrap(); let mut stream = stream::Stream::new(Some(Encoding::Proto), stream); let peer_key = data::make_secret_key(rng); let peer_id = PeerId::new(peer_key.public_key()); @@ -129,7 +129,7 @@ async fn test_nonce_refresh() { ) .await; - pm2.connect_to(&pm.peer_info()).await; + pm2.connect_to(&pm.peer_info(), tcp::Tier::T2).await; let edge = wait_for_edge(&mut pm2).await; let start_time = clock.now_utc(); diff --git a/chain/network/src/peer_manager/tests/routing.rs b/chain/network/src/peer_manager/tests/routing.rs index 797722faeba..38b34291014 100644 --- a/chain/network/src/peer_manager/tests/routing.rs +++ b/chain/network/src/peer_manager/tests/routing.rs @@ -46,7 +46,7 @@ async fn simple() { pm1.wait_for_routing_table(&[]).await; tracing::info!(target:"test", "connect the nodes"); - pm0.connect_to(&pm1.peer_info()).await; + pm0.connect_to(&pm1.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()])]).await; @@ -68,8 +68,8 @@ async fn three_nodes_path() { let pm1 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; let pm2 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; - pm0.connect_to(&pm1.peer_info()).await; - pm1.connect_to(&pm2.peer_info()).await; + pm0.connect_to(&pm1.peer_info(), tcp::Tier::T2).await; + pm1.connect_to(&pm2.peer_info(), tcp::Tier::T2).await; let id0 = pm0.cfg.node_id(); let id1 = pm1.cfg.node_id(); @@ -109,8 +109,8 @@ async fn three_nodes_star() { let pm1 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; let pm2 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; - pm0.connect_to(&pm1.peer_info()).await; - pm1.connect_to(&pm2.peer_info()).await; + pm0.connect_to(&pm1.peer_info(), tcp::Tier::T2).await; + pm1.connect_to(&pm2.peer_info(), tcp::Tier::T2).await; let id0 = pm0.cfg.node_id(); let id1 = pm1.cfg.node_id(); @@ -136,7 +136,7 @@ async fn three_nodes_star() { .await; tracing::info!(target:"test", "connect {id0} and {id2}"); - pm0.connect_to(&pm2.peer_info()).await; + pm0.connect_to(&pm2.peer_info(), tcp::Tier::T2).await; tracing::info!(target:"test", "wait for {id0} routing table"); pm0.wait_for_routing_table(&[ @@ -179,8 +179,8 @@ async fn join_components() { let id2 = pm2.cfg.node_id(); let id3 = pm3.cfg.node_id(); - pm0.connect_to(&pm1.peer_info()).await; - pm2.connect_to(&pm3.peer_info()).await; + pm0.connect_to(&pm1.peer_info(), tcp::Tier::T2).await; + pm2.connect_to(&pm3.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()])]).await; @@ -193,8 +193,8 @@ async fn join_components() { pm3.wait_for_routing_table(&[(id2.clone(), vec![id2.clone()])]).await; tracing::info!(target:"test", "join the two components into a square"); - pm0.connect_to(&pm2.peer_info()).await; - pm3.connect_to(&pm1.peer_info()).await; + pm0.connect_to(&pm2.peer_info(), tcp::Tier::T2).await; + pm3.connect_to(&pm1.peer_info(), tcp::Tier::T2).await; tracing::info!(target:"test", "wait for {id0} routing table"); pm0.wait_for_routing_table(&[ @@ -244,8 +244,8 @@ async fn simple_remove() { let pm1 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; let pm2 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; - pm0.connect_to(&pm1.peer_info()).await; - pm1.connect_to(&pm2.peer_info()).await; + pm0.connect_to(&pm1.peer_info(), tcp::Tier::T2).await; + pm1.connect_to(&pm2.peer_info(), tcp::Tier::T2).await; let id0 = pm0.cfg.node_id(); let id1 = pm1.cfg.node_id(); @@ -337,7 +337,7 @@ async fn ping_simple() { let id0 = pm0.cfg.node_id(); let id1 = pm1.cfg.node_id(); - pm0.connect_to(&pm1.peer_info()).await; + pm0.connect_to(&pm1.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()])]).await; @@ -375,8 +375,8 @@ async fn ping_jump() { let id2 = pm2.cfg.node_id(); tracing::info!(target:"test", "connect nodes in a line"); - pm0.connect_to(&pm1.peer_info()).await; - pm1.connect_to(&pm2.peer_info()).await; + pm0.connect_to(&pm1.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(&[ @@ -432,8 +432,8 @@ async fn test_dont_drop_after_ttl() { let id2 = pm2.cfg.node_id(); tracing::info!(target:"test", "connect nodes in a line"); - pm0.connect_to(&pm1.peer_info()).await; - pm1.connect_to(&pm2.peer_info()).await; + pm0.connect_to(&pm1.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(&[ @@ -489,8 +489,8 @@ async fn test_drop_after_ttl() { let id2 = pm2.cfg.node_id(); tracing::info!(target:"test", "connect nodes in a line"); - pm0.connect_to(&pm1.peer_info()).await; - pm1.connect_to(&pm2.peer_info()).await; + pm0.connect_to(&pm1.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(&[ @@ -540,8 +540,8 @@ async fn test_dropping_duplicate_messages() { let id2 = pm2.cfg.node_id(); tracing::info!(target:"test", "connect nodes in a line"); - pm0.connect_to(&pm1.peer_info()).await; - pm1.connect_to(&pm2.peer_info()).await; + pm0.connect_to(&pm1.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(&[ @@ -880,7 +880,7 @@ async fn ttl() { chain, force_encoding: Some(Encoding::Proto), }; - let stream = tcp::Stream::connect(&pm.peer_info()).await.unwrap(); + let stream = tcp::Stream::connect(&pm.peer_info(), tcp::Tier::T2).await.unwrap(); let mut peer = peer::testonly::PeerHandle::start_endpoint(clock.clock(), cfg, stream).await; peer.complete_handshake().await; pm.wait_for_routing_table(&[(peer.cfg.id(), vec![peer.cfg.id()])]).await; @@ -901,9 +901,10 @@ async fn ttl() { let got = peer .events .recv_until(|ev| match ev { - peer::testonly::Event::Network(PME::MessageProcessed(PeerMessage::Routed( - msg, - ))) => Some(msg), + peer::testonly::Event::Network(PME::MessageProcessed( + tcp::Tier::T2, + PeerMessage::Routed(msg), + )) => Some(msg), _ => None, }) .await; @@ -934,7 +935,7 @@ async fn repeated_data_in_sync_routing_table() { chain, force_encoding: Some(Encoding::Proto), }; - let stream = tcp::Stream::connect(&pm.peer_info()).await.unwrap(); + let stream = tcp::Stream::connect(&pm.peer_info(), tcp::Tier::T2).await.unwrap(); let mut peer = peer::testonly::PeerHandle::start_endpoint(clock.clock(), cfg, stream).await; peer.complete_handshake().await; @@ -955,6 +956,7 @@ async fn repeated_data_in_sync_routing_table() { while edges_got != edges_want || accounts_got != accounts_want { match peer.events.recv().await { peer::testonly::Event::Network(PME::MessageProcessed( + tcp::Tier::T2, PeerMessage::SyncRoutingTable(got), )) => { for a in got.accounts { @@ -1002,6 +1004,7 @@ async fn wait_for_edges( while &got != want { match events.recv().await { peer::testonly::Event::Network(PME::MessageProcessed( + tcp::Tier::T2, PeerMessage::SyncRoutingTable(msg), )) => { tracing::info!(target: "test", "got edges: {:?}",msg.edges.iter().map(|e|e.hash()).collect::>()); @@ -1093,10 +1096,10 @@ async fn square() { let pm1 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; let pm2 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; let pm3 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; - pm0.connect_to(&pm1.peer_info()).await; - pm1.connect_to(&pm2.peer_info()).await; - pm2.connect_to(&pm3.peer_info()).await; - pm3.connect_to(&pm0.peer_info()).await; + pm0.connect_to(&pm1.peer_info(), tcp::Tier::T2).await; + pm1.connect_to(&pm2.peer_info(), tcp::Tier::T2).await; + pm2.connect_to(&pm3.peer_info(), tcp::Tier::T2).await; + pm3.connect_to(&pm0.peer_info(), tcp::Tier::T2).await; let id0 = pm0.cfg.node_id(); let id1 = pm1.cfg.node_id(); let id2 = pm2.cfg.node_id(); @@ -1165,7 +1168,7 @@ async fn fix_local_edges() { conn.send(msg.clone()).await; events .recv_until(|ev| match ev { - Event::PeerManager(PME::MessageProcessed(got)) if got == msg => Some(()), + Event::PeerManager(PME::MessageProcessed(tcp::Tier::T2, got)) if got == msg => Some(()), _ => None, }) .await; @@ -1202,7 +1205,7 @@ async fn do_not_block_announce_account_broadcast() { tracing::info!(target:"test", "spawn 2 nodes and announce the account."); let pm0 = start_pm(clock.clock(), db0.clone(), chain.make_config(rng), chain.clone()).await; let pm1 = start_pm(clock.clock(), db1.clone(), chain.make_config(rng), chain.clone()).await; - pm1.connect_to(&pm0.peer_info()).await; + pm1.connect_to(&pm0.peer_info(), tcp::Tier::T2).await; pm1.announce_account(aa.clone()).await; assert_eq!(&aa.peer_id, &pm0.wait_for_account_owner(&aa.account_id).await); drop(pm0); @@ -1214,8 +1217,8 @@ async fn do_not_block_announce_account_broadcast() { let pm0 = start_pm(clock.clock(), db0, chain.make_config(rng), chain.clone()).await; let pm1 = start_pm(clock.clock(), db1, chain.make_config(rng), chain.clone()).await; let pm2 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; - pm1.connect_to(&pm0.peer_info()).await; - pm2.connect_to(&pm0.peer_info()).await; + pm1.connect_to(&pm0.peer_info(), tcp::Tier::T2).await; + pm2.connect_to(&pm0.peer_info(), tcp::Tier::T2).await; pm1.announce_account(aa.clone()).await; assert_eq!(&aa.peer_id, &pm2.wait_for_account_owner(&aa.account_id).await); } diff --git a/chain/network/src/peer_manager/tests/tier1.rs b/chain/network/src/peer_manager/tests/tier1.rs new file mode 100644 index 00000000000..2367fccb3e3 --- /dev/null +++ b/chain/network/src/peer_manager/tests/tier1.rs @@ -0,0 +1,343 @@ +use crate::config; +use crate::network_protocol::testonly as data; +use crate::network_protocol::{PeerAddr, PeerMessage, RoutedMessageBody}; +use crate::peer_manager; +use crate::peer_manager::peer_manager_actor::Event as PME; +use crate::peer_manager::testonly::start as start_pm; +use crate::peer_manager::testonly::Event; +use crate::tcp; +use crate::testonly::{make_rng, Rng}; +use crate::time; +use crate::types::{NetworkRequests, NetworkResponses, PeerManagerMessageRequest}; +use near_o11y::testonly::init_test_logger; +use near_o11y::WithSpanContextExt; +use near_primitives::block_header::{Approval, ApprovalInner, ApprovalMessage}; +use near_primitives::validator_signer::ValidatorSigner; +use near_store::db::TestDB; +use rand::Rng as _; +use std::collections::HashSet; +use std::sync::Arc; + +/// Constructs a random TIER1 message. +fn make_block_approval(rng: &mut Rng, signer: &dyn ValidatorSigner) -> Approval { + let inner = ApprovalInner::Endorsement(data::make_hash(rng)); + let target_height = rng.gen_range(0..100000); + Approval { + signature: signer.sign_approval(&inner, target_height), + account_id: signer.validator_id().clone(), + target_height, + inner, + } +} + +async fn establish_connections(clock: &time::Clock, pms: &[&peer_manager::testonly::ActorHandler]) { + // Make TIER1 validators connect to proxies. + let mut data = HashSet::new(); + for pm in pms { + data.extend(pm.tier1_advertise_proxies(clock).await); + } + tracing::info!(target:"test", "tier1_advertise_proxies() DONE"); + + // Wait for accounts data to propagate. + for pm in pms { + tracing::info!(target:"test", "{}: wait_for_accounts_data()",pm.cfg.node_id()); + pm.wait_for_accounts_data(&data).await; + tracing::info!(target:"test", "{}: wait_for_accounts_data() DONE",pm.cfg.node_id()); + pm.tier1_connect(clock).await; + tracing::info!(target:"test", "{}: tier1_connect() DONE",pm.cfg.node_id()); + } +} + +async fn send_tier1_message( + rng: &mut Rng, + from: &peer_manager::testonly::ActorHandler, + to: &peer_manager::testonly::ActorHandler, +) { + let from_signer = from.cfg.validator.as_ref().unwrap().signer.clone(); + let to_signer = to.cfg.validator.as_ref().unwrap().signer.clone(); + let target = to_signer.validator_id().clone(); + let want = make_block_approval(rng, from_signer.as_ref()); + let req = NetworkRequests::Approval { + approval_message: ApprovalMessage { approval: want.clone(), target }, + }; + let mut events = to.events.from_now(); + let resp = from + .actix + .addr + .send(PeerManagerMessageRequest::NetworkRequests(req).with_span_context()) + .await + .unwrap(); + assert_eq!(NetworkResponses::NoResponse, resp.as_network_response()); + let got = events + .recv_until(|ev| match ev { + Event::PeerManager(PME::MessageProcessed(tcp::Tier::T1, PeerMessage::Routed(got))) => { + Some(got) + } + _ => None, + }) + .await; + assert_eq!(from.cfg.node_id(), got.author); + assert_eq!(RoutedMessageBody::BlockApproval(want), got.body); +} + +/// Send a message over each connection. +async fn test_clique(rng: &mut Rng, pms: &[&peer_manager::testonly::ActorHandler]) { + for from in pms { + for to in pms { + if from.cfg.node_id() == to.cfg.node_id() { + continue; + } + send_tier1_message(rng, from, to).await; + } + } +} + +// In case a node is its own proxy, it should advertise its address as soon as +// it becomes a TIER1 node. +#[tokio::test] +async fn first_proxy_advertisement() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + let pm = peer_manager::testonly::start( + clock.clock(), + near_store::db::TestDB::new(), + chain.make_config(rng), + chain.clone(), + ) + .await; + let chain_info = peer_manager::testonly::make_chain_info(&chain, &[&pm]); + tracing::info!(target:"test", "set_chain_info()"); + // TODO(gprusak): The default config constructed via chain.make_config(), + // currently returns a validator config with its own server addr in the list of TIER1 proxies. + // You might want to set it explicitly within this test to not rely on defaults. + pm.set_chain_info(chain_info).await; + let got = pm.tier1_advertise_proxies(&clock.clock()).await; + tracing::info!(target:"test", "awaiting for Tier1AdvertiseProxies"); + assert_eq!( + got[0].proxies, + vec![PeerAddr { peer_id: pm.cfg.node_id(), addr: pm.cfg.node_addr.unwrap() }] + ); +} + +#[tokio::test] +async fn direct_connections() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + let mut pms = vec![]; + for _ in 0..5 { + pms.push( + peer_manager::testonly::start( + clock.clock(), + near_store::db::TestDB::new(), + chain.make_config(rng), + chain.clone(), + ) + .await, + ); + } + let pms: Vec<_> = pms.iter().collect(); + + tracing::info!(target:"test", "Connect peers serially."); + for i in 1..pms.len() { + pms[i - 1].connect_to(&pms[i].peer_info(), tcp::Tier::T2).await; + } + + tracing::info!(target:"test", "Set chain info."); + let chain_info = peer_manager::testonly::make_chain_info(&chain, &pms[..]); + for pm in &pms { + pm.set_chain_info(chain_info.clone()).await; + } + tracing::info!(target:"test", "Establish connections."); + establish_connections(&clock.clock(), &pms[..]).await; + tracing::info!(target:"test", "Test clique."); + test_clique(rng, &pms[..]).await; +} + +/// Test which spawns N validators, each with 1 proxy. +/// All the nodes are connected in TIER2 star topology. +/// Then all validators connect to the proxy of each other validator. +#[tokio::test] +async fn proxy_connections() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + const N: usize = 5; + + let mut proxies = vec![]; + for _ in 0..N { + proxies.push( + peer_manager::testonly::start( + clock.clock(), + near_store::db::TestDB::new(), + chain.make_config(rng), + chain.clone(), + ) + .await, + ); + } + let proxies: Vec<_> = proxies.iter().collect(); + + let mut validators = vec![]; + for i in 0..N { + let mut cfg = chain.make_config(rng); + cfg.validator.as_mut().unwrap().proxies = + config::ValidatorProxies::Static(vec![PeerAddr { + peer_id: proxies[i].cfg.node_id(), + addr: proxies[i].cfg.node_addr.unwrap(), + }]); + validators.push( + peer_manager::testonly::start( + clock.clock(), + near_store::db::TestDB::new(), + cfg, + chain.clone(), + ) + .await, + ); + } + let validators: Vec<_> = validators.iter().collect(); + + // Connect validators and proxies in a star topology. Any connected graph would do. + let hub = peer_manager::testonly::start( + clock.clock(), + near_store::db::TestDB::new(), + chain.make_config(rng), + chain.clone(), + ) + .await; + for pm in &validators { + pm.connect_to(&hub.peer_info(), tcp::Tier::T2).await; + } + for pm in &proxies { + pm.connect_to(&hub.peer_info(), tcp::Tier::T2).await; + } + + let mut all = vec![]; + all.extend(validators.clone()); + all.extend(proxies.clone()); + all.push(&hub); + + let chain_info = peer_manager::testonly::make_chain_info(&chain, &validators[..]); + for pm in &all { + pm.set_chain_info(chain_info.clone()).await; + } + establish_connections(&clock.clock(), &all[..]).await; + test_clique(rng, &validators[..]).await; +} + +#[tokio::test] +async fn account_keys_change() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + let v0 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let v1 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let v2 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let hub = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + hub.connect_to(&v0.peer_info(), tcp::Tier::T2).await; + hub.connect_to(&v1.peer_info(), tcp::Tier::T2).await; + hub.connect_to(&v2.peer_info(), tcp::Tier::T2).await; + + // TIER1 nodes in 1st epoch are {v0,v1}. + let chain_info = peer_manager::testonly::make_chain_info(&chain, &[&v0, &v1]); + for pm in [&v0, &v1, &v2, &hub] { + pm.set_chain_info(chain_info.clone()).await; + } + establish_connections(&clock.clock(), &[&v0, &v1, &v2, &hub]).await; + test_clique(rng, &[&v0, &v1]).await; + + // TIER1 nodes in 2nd epoch are {v0,v2}. + let chain_info = peer_manager::testonly::make_chain_info(&chain, &[&v0, &v2]); + for pm in [&v0, &v1, &v2, &hub] { + pm.set_chain_info(chain_info.clone()).await; + } + establish_connections(&clock.clock(), &[&v0, &v1, &v2, &hub]).await; + test_clique(rng, &[&v0, &v2]).await; + + drop(v0); + drop(v1); + drop(v2); + drop(hub); +} + +// Let's say that a validator has 2 proxies configured. At first proxy0 is available and proxy1 is not, +// then proxy1 is available and proxy0 is not. In both situations validator should be reachable, +// as long as it manages to advertise the currently available proxy and the TIER1 nodes connect to +// that proxy. +#[tokio::test] +async fn proxy_change() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + // v0 has proxies {p0,p1} + // v1 has no proxies. + let p0cfg = chain.make_config(rng); + let p1cfg = chain.make_config(rng); + let mut v0cfg = chain.make_config(rng); + v0cfg.validator.as_mut().unwrap().proxies = config::ValidatorProxies::Static(vec![ + PeerAddr { peer_id: p0cfg.node_id(), addr: p0cfg.node_addr.unwrap() }, + PeerAddr { peer_id: p1cfg.node_id(), addr: p1cfg.node_addr.unwrap() }, + ]); + let mut v1cfg = chain.make_config(rng); + v1cfg.validator.as_mut().unwrap().proxies = config::ValidatorProxies::Static(vec![]); + + tracing::info!(target:"test", "Start all nodes."); + let p0 = start_pm(clock.clock(), TestDB::new(), p0cfg.clone(), chain.clone()).await; + let p1 = start_pm(clock.clock(), TestDB::new(), p1cfg.clone(), chain.clone()).await; + let v0 = start_pm(clock.clock(), TestDB::new(), v0cfg.clone(), chain.clone()).await; + let v1 = start_pm(clock.clock(), TestDB::new(), v1cfg.clone(), chain.clone()).await; + let hub = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + hub.connect_to(&p0.peer_info(), tcp::Tier::T2).await; + hub.connect_to(&p1.peer_info(), tcp::Tier::T2).await; + hub.connect_to(&v0.peer_info(), tcp::Tier::T2).await; + hub.connect_to(&v1.peer_info(), tcp::Tier::T2).await; + tracing::info!(target:"dupa","p0 = {}",p0cfg.node_id()); + tracing::info!(target:"dupa","hub = {}",hub.cfg.node_id()); + + tracing::info!(target:"test", "p0 goes down"); + drop(p0); + tracing::info!(target:"test", "remaining nodes learn that [v0,v1] are TIER1 nodes"); + let chain_info = peer_manager::testonly::make_chain_info(&chain, &[&v0, &v1]); + for pm in [&v0, &v1, &p1, &hub] { + pm.set_chain_info(chain_info.clone()).await; + } + tracing::info!(target:"test", "TIER1 connections get established: v0 -> p1 <- v1."); + establish_connections(&clock.clock(), &[&v0, &v1, &p1, &hub]).await; + tracing::info!(target:"test", "Send message v1 -> v0 over TIER1."); + send_tier1_message(rng, &v1, &v0).await; + + // Advance time, so that the new AccountsData has newer timestamp. + clock.advance(time::Duration::hours(1)); + + tracing::info!(target:"test", "p1 goes down."); + drop(p1); + tracing::info!(target:"test", "p0 goes up and learns that [v0,v1] are TIER1 nodes."); + let p0 = start_pm(clock.clock(), TestDB::new(), p0cfg.clone(), chain.clone()).await; + p0.set_chain_info(chain_info).await; + hub.connect_to(&p0.peer_info(), tcp::Tier::T2).await; + tracing::info!(target:"test", "TIER1 connections get established: v0 -> p0 <- v1."); + establish_connections(&clock.clock(), &[&v0, &v1, &p0, &hub]).await; + tracing::info!(target:"test", "Send message v1 -> v0 over TIER1."); + send_tier1_message(rng, &v1, &v0).await; + + drop(hub); + drop(v0); + drop(v1); + drop(p0); +} diff --git a/chain/network/src/private_actix.rs b/chain/network/src/private_actix.rs index 89eccca7478..aa2aa08a806 100644 --- a/chain/network/src/private_actix.rs +++ b/chain/network/src/private_actix.rs @@ -11,6 +11,8 @@ pub(crate) enum RegisterPeerError { Banned, PoolError(connection::PoolError), ConnectionLimitExceeded, + NotTier1Peer, + Tier1InboundDisabled, InvalidEdge, } diff --git a/chain/network/src/tcp.rs b/chain/network/src/tcp.rs index 125fe74a5b5..8b330ebcfe0 100644 --- a/chain/network/src/tcp.rs +++ b/chain/network/src/tcp.rs @@ -2,10 +2,24 @@ use crate::network_protocol::PeerInfo; use anyhow::{anyhow, Context as _}; use near_primitives::network::PeerId; +/// TCP connections established by a node belong to different logical networks (aka tiers), +/// which serve different purpose. +// TODO(gprusak): add a link to the design on github docs (but first write those docs). +#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::AsRefStr)] +pub enum Tier { + /// Tier1 connections are established between the BFT consensus participants (or their proxies) + /// and are reserved exclusively for exchanging BFT consensus messages. + T1, + /// Tier2 connections form a P2P gossip network, which is used for everything, except the BFT + /// consensus messages. Also, Tier1 peer discovery actually happens on Tier2 network, i.e. + /// Tier2 network is necessary to bootstrap Tier1 connections. + T2, +} + #[derive(Clone, Debug)] pub(crate) enum StreamType { Inbound, - Outbound { peer_id: PeerId }, + Outbound { peer_id: PeerId, tier: Tier }, } #[derive(Debug)] @@ -42,13 +56,13 @@ impl Socket { Self(socket) } - pub async fn connect(self, peer_info: &PeerInfo) -> Stream { + pub async fn connect(self, peer_info: &PeerInfo, tier: Tier) -> Stream { // TODO(gprusak): this could replace Stream::connect, // however this means that we will have to replicate everything // that tokio::net::TcpStream sets on the socket. // As long as Socket::connect is test-only we may ignore that. let stream = self.0.connect(peer_info.addr.unwrap()).await.unwrap(); - Stream::new(stream, StreamType::Outbound { peer_id: peer_info.id.clone() }).unwrap() + Stream::new(stream, StreamType::Outbound { peer_id: peer_info.id.clone(), tier }).unwrap() } } @@ -57,7 +71,7 @@ impl Stream { Ok(Self { peer_addr: stream.peer_addr()?, local_addr: stream.local_addr()?, stream, type_ }) } - pub async fn connect(peer_info: &PeerInfo) -> anyhow::Result { + pub async fn connect(peer_info: &PeerInfo, tier: Tier) -> anyhow::Result { let addr = peer_info.addr.ok_or(anyhow!("Trying to connect to peer with no public address"))?; // The `connect` may take several minutes. This happens when the @@ -74,13 +88,13 @@ impl Stream { ) .await? .context("TcpStream::connect()")?; - Ok(Stream::new(stream, StreamType::Outbound { peer_id: peer_info.id.clone() })?) + Ok(Stream::new(stream, StreamType::Outbound { peer_id: peer_info.id.clone(), tier })?) } /// Establishes a loopback TCP connection to localhost with random ports. /// Returns a pair of streams: (outbound,inbound). #[cfg(test)] - pub async fn loopback(peer_id: PeerId) -> (Stream, Stream) { + pub async fn loopback(peer_id: PeerId, tier: Tier) -> (Stream, Stream) { let localhost = std::net::SocketAddr::new(std::net::Ipv4Addr::LOCALHOST.into(), 0); let mut listener = Listener::bind(localhost).await.unwrap(); let peer_info = PeerInfo { @@ -88,7 +102,8 @@ impl Stream { addr: Some(listener.0.local_addr().unwrap()), account_id: None, }; - let (outbound, inbound) = tokio::join!(Stream::connect(&peer_info), listener.accept(),); + let (outbound, inbound) = + tokio::join!(Stream::connect(&peer_info, tier), listener.accept()); (outbound.unwrap(), inbound.unwrap()) } diff --git a/chain/network/src/testonly/fake_client.rs b/chain/network/src/testonly/fake_client.rs index 36c41d49644..051b8c86b1f 100644 --- a/chain/network/src/testonly/fake_client.rs +++ b/chain/network/src/testonly/fake_client.rs @@ -20,6 +20,7 @@ pub enum Event { AnnounceAccount(Vec<(AnnounceAccount, Option)>), Block(Block), BlockHeaders(Vec), + BlockApproval(Approval, PeerId), BlockHeadersRequest(Vec), BlockRequest(CryptoHash), Challenge(Challenge), @@ -73,8 +74,8 @@ impl client::Client for Fake { unimplemented!(); } - async fn block_approval(&self, _approval: Approval, _peer_id: PeerId) { - unimplemented!(); + async fn block_approval(&self, approval: Approval, peer_id: PeerId) { + self.event_sink.push(Event::BlockApproval(approval, peer_id)); } async fn transaction(&self, transaction: SignedTransaction, _is_forwarded: bool) { diff --git a/chain/network/src/types.rs b/chain/network/src/types.rs index 47f7fdf4ec4..dd47517b116 100644 --- a/chain/network/src/types.rs +++ b/chain/network/src/types.rs @@ -366,7 +366,7 @@ pub struct NetworkInfo { pub tier1_accounts: Vec>, } -#[derive(Debug, actix::MessageResponse)] +#[derive(Debug, actix::MessageResponse, PartialEq, Eq)] pub enum NetworkResponses { NoResponse, PingPongInfo { pings: Vec, pongs: Vec }, @@ -488,7 +488,6 @@ mod tests { fn test_enum_size() { assert_size!(PeerType); assert_size!(RoutedMessageBody); - assert_size!(PeerIdOrHash); assert_size!(KnownPeerStatus); assert_size!(ReasonForBan); } diff --git a/integration-tests/src/tests/network/runner.rs b/integration-tests/src/tests/network/runner.rs index 72d16c3a2a4..c23a01b7d8c 100644 --- a/integration-tests/src/tests/network/runner.rs +++ b/integration-tests/src/tests/network/runner.rs @@ -223,7 +223,7 @@ impl StateMachine { debug!(target: "network", num_prev_actions, action = ?action_clone, "runner.rs: Action"); let pm = info.get_node(from)?.actix.addr.clone(); let peer_info = info.runner.test_config[to].peer_info(); - match tcp::Stream::connect(&peer_info).await { + match tcp::Stream::connect(&peer_info, tcp::Tier::T2).await { Ok(stream) => { pm.send(PeerManagerMessageRequest::OutboundTcpConnect(stream).with_span_context()).await?; }, Err(err) => tracing::debug!("tcp::Stream::connect({peer_info}): {err}"), } From 2977c775a4b55f3937c8f645a5459c3d0941e648 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Tue, 6 Dec 2022 11:23:38 +0100 Subject: [PATCH 065/188] fix for the protobuf check (#8161) master HEAD happens to be not up to date with the origin for some reason. --- .buildkite/pipeline.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 015cef0232b..4319bbbf32b 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -5,7 +5,11 @@ env: steps: - label: "protobuf backward compatibility" command: | - buf breaking --against ".git#ref=`git merge-base master HEAD`" + # We need to manually fetch the latest commit of master to + # correctly compute the commit at which we should diff the proto + # files. Apparently the pipeline doesn't do that for us. + git fetch origin + buf breaking --against ".git#ref=`git merge-base origin/master HEAD`" timeout: 30 agents: From 70fda249987e4346240631a603459dacd599feed Mon Sep 17 00:00:00 2001 From: mm-near <91919554+mm-near@users.noreply.github.com> Date: Tue, 6 Dec 2022 14:50:56 +0100 Subject: [PATCH 066/188] [Refactor] Moving KeyValueRuntime to a separate file (#8167) Co-authored-by: near-bulldozer[bot] <73298989+near-bulldozer[bot]@users.noreply.github.com> --- chain/chain/src/test_utils.rs | 1356 +--------------------- chain/chain/src/test_utils/kv_runtime.rs | 1346 +++++++++++++++++++++ 2 files changed, 1362 insertions(+), 1340 deletions(-) create mode 100644 chain/chain/src/test_utils/kv_runtime.rs diff --git a/chain/chain/src/test_utils.rs b/chain/chain/src/test_utils.rs index f959d2c2465..7e69a2736cd 100644 --- a/chain/chain/src/test_utils.rs +++ b/chain/chain/src/test_utils.rs @@ -1,71 +1,37 @@ +mod kv_runtime; mod validator_schedule; use std::cmp::Ordering; -use std::collections::{HashMap, HashSet}; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; -use borsh::{BorshDeserialize, BorshSerialize}; - -use near_epoch_manager::EpochManagerAdapter; -use near_primitives::sandbox::state_patch::SandboxStatePatch; -use near_primitives::state_part::PartId; use num_rational::Ratio; use tracing::debug; -use near_chain_configs::{ProtocolConfig, DEFAULT_GC_NUM_EPOCHS_TO_KEEP}; use near_chain_primitives::Error; -use near_client_primitives::types::StateSplitApplyingStatus; -use near_crypto::{KeyType, PublicKey, SecretKey, Signature}; -use near_pool::types::PoolIterator; -use near_primitives::account::{AccessKey, Account}; + +use near_crypto::KeyType; use near_primitives::block::Block; -use near_primitives::block_header::{Approval, ApprovalInner}; -use near_primitives::challenge::ChallengesResult; -use near_primitives::epoch_manager::block_info::BlockInfo; -use near_primitives::epoch_manager::epoch_info::EpochInfo; -use near_primitives::epoch_manager::EpochConfig; -use near_primitives::epoch_manager::ValidatorSelectionConfig; -use near_primitives::errors::{EpochError, InvalidTxError}; -use near_primitives::hash::{hash, CryptoHash}; -use near_primitives::receipt::{ActionReceipt, Receipt, ReceiptEnum}; -use near_primitives::shard_layout; -use near_primitives::shard_layout::{ShardLayout, ShardUId}; -use near_primitives::sharding::ChunkHash; -use near_primitives::transaction::{ - Action, ExecutionMetadata, ExecutionOutcome, ExecutionOutcomeWithId, ExecutionStatus, - SignedTransaction, TransferAction, -}; -use near_primitives::types::validator_stake::{ValidatorStake, ValidatorStakeIter}; -use near_primitives::types::{ - AccountId, ApprovalStake, Balance, BlockHeight, EpochHeight, EpochId, Gas, Nonce, NumBlocks, - NumShards, ShardId, StateChangesForSplitStates, StateRoot, StateRootNode, - ValidatorInfoIdentifier, -}; + +use near_primitives::hash::CryptoHash; + +use near_primitives::types::{AccountId, NumBlocks}; use near_primitives::validator_signer::InMemoryValidatorSigner; -use near_primitives::version::{ProtocolVersion, PROTOCOL_VERSION}; -use near_primitives::views::{ - AccessKeyInfoView, AccessKeyList, CallResult, ContractCodeView, EpochValidatorInfo, - QueryRequest, QueryResponse, QueryResponseKind, ViewStateResult, -}; +use near_primitives::version::PROTOCOL_VERSION; + use near_store::test_utils::create_test_store; -use near_store::{ - DBCol, PartialStorage, ShardTries, Store, StoreUpdate, Trie, TrieChanges, WrappedTrieChanges, -}; +use near_store::DBCol; use crate::block_processing_utils::BlockNotInPoolError; use crate::chain::Chain; use crate::store::ChainStoreAccess; -use crate::types::{ - AcceptedBlock, ApplySplitStateResult, ApplyTransactionResult, BlockHeaderInfo, ChainConfig, - ChainGenesis, -}; -use crate::{BlockHeader, DoomslugThresholdMode, RuntimeAdapter}; +use crate::types::{AcceptedBlock, ChainConfig, ChainGenesis}; +use crate::DoomslugThresholdMode; use crate::{BlockProcessingArtifact, Provenance}; -use near_primitives::epoch_manager::ShardConfig; use near_primitives::time::Clock; use near_primitives::utils::MaybeValidated; -use near_store::flat_state::ChainAccessForFlatStorage; -use near_store::flat_state::{FlatStorageState, FlatStorageStateStatus}; + +pub use self::kv_runtime::account_id_to_shard_id; +pub use self::kv_runtime::KeyValueRuntime; pub use self::validator_schedule::ValidatorSchedule; @@ -111,1296 +77,6 @@ pub fn process_block_sync( Ok(accepted_blocks) } -#[derive(BorshSerialize, BorshDeserialize, Hash, PartialEq, Eq, Ord, PartialOrd, Clone, Debug)] -struct AccountNonce(AccountId, Nonce); - -#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] -struct KVState { - amounts: HashMap, - receipt_nonces: HashSet, - tx_nonces: HashSet, -} - -/// Stores the validator information in an epoch. -/// Block producers are specified by `block_producers` -/// Chunk producers have two types, validators who are also block producers and chunk only producers. -/// Block producers are assigned to shards via `validator_groups`. -/// Each shard will have `block_producers.len() / validator_groups` of validators who are also block -/// producers -struct EpochValidatorSet { - block_producers: Vec, - /// index of this list is shard_id - chunk_producers: Vec>, -} - -/// Simple key value runtime for tests. -/// -/// Major differences with production `NightshadeRuntime`: -/// * Uses in-memory storage -/// * Doesn't have WASM runtime, so can only process simple transfer -/// transaction -/// * Uses hard-coded validator schedule instead of using `EpochManager` and -/// staking to assign block and chunk producers. -pub struct KeyValueRuntime { - store: Store, - tries: ShardTries, - /// A pre determined list of validator sets. We rotate validator set in this list. - /// Epoch i uses validators from `validators_by_valset[i % validators_by_valset.len()]`. - validators_by_valset: Vec, - /// Maps from account id to validator stake for all validators, both block producers and - /// chunk producers - validators: HashMap, - num_shards: NumShards, - tracks_all_shards: bool, - epoch_length: u64, - no_gc: bool, - - // A mapping state_root => {account id => amounts}, for transactions and receipts - state: RwLock>, - state_size: RwLock>, - - headers_cache: RwLock>, - hash_to_epoch: RwLock>, - hash_to_next_epoch_approvals_req: RwLock>, - hash_to_next_epoch: RwLock>, - /// Maps EpochId to index of `validators_by_valset` to determine validators for an epoch - hash_to_valset: RwLock>, - epoch_start: RwLock>, -} - -pub fn account_id_to_shard_id(account_id: &AccountId, num_shards: NumShards) -> ShardId { - let shard_layout = ShardLayout::v0(num_shards, 0); - shard_layout::account_id_to_shard_id(account_id, &shard_layout) -} - -#[derive(BorshSerialize, BorshDeserialize)] -struct ReceiptNonce { - from: AccountId, - to: AccountId, - amount: Balance, - nonce: Nonce, -} - -fn create_receipt_nonce( - from: AccountId, - to: AccountId, - amount: Balance, - nonce: Nonce, -) -> CryptoHash { - CryptoHash::hash_borsh(ReceiptNonce { from, to, amount, nonce }) -} - -impl KeyValueRuntime { - pub fn new(store: Store, epoch_length: u64) -> Self { - let vs = - ValidatorSchedule::new().block_producers_per_epoch(vec![vec!["test".parse().unwrap()]]); - Self::new_with_validators(store, vs, epoch_length) - } - - pub fn new_with_validators(store: Store, vs: ValidatorSchedule, epoch_length: u64) -> Self { - Self::new_with_validators_and_no_gc(store, vs, epoch_length, false) - } - - pub fn new_with_validators_and_no_gc( - store: Store, - vs: ValidatorSchedule, - epoch_length: u64, - no_gc: bool, - ) -> Self { - let tries = ShardTries::test(store.clone(), vs.num_shards); - let mut initial_amounts = HashMap::new(); - for (i, validator) in vs.block_producers.iter().flatten().enumerate() { - initial_amounts.insert(validator.clone(), (1000 + 100 * i) as u128); - } - - let map_with_default_hash1 = HashMap::from([(CryptoHash::default(), EpochId::default())]); - let map_with_default_hash2 = HashMap::from([(CryptoHash::default(), 0)]); - let map_with_default_hash3 = HashMap::from([(EpochId::default(), 0)]); - - let kv_state = KVState { - amounts: initial_amounts, - receipt_nonces: HashSet::default(), - tx_nonces: HashSet::default(), - }; - let data = kv_state.try_to_vec().unwrap(); - let data_len = data.len() as u64; - // StateRoot is actually faked here. - // We cannot do any reasonable validations of it in test_utils. - let state = HashMap::from([(Trie::EMPTY_ROOT, kv_state)]); - let state_size = HashMap::from([(Trie::EMPTY_ROOT, data_len)]); - - let mut validators = HashMap::new(); - #[allow(unused_mut)] - let mut validators_by_valset: Vec = vs - .block_producers - .iter() - .map(|account_ids| { - let block_producers: Vec = account_ids - .iter() - .map(|account_id| { - let stake = ValidatorStake::new( - account_id.clone(), - SecretKey::from_seed(KeyType::ED25519, account_id.as_ref()) - .public_key(), - 1_000_000, - ); - validators.insert(account_id.clone(), stake.clone()); - stake - }) - .collect(); - - let validators_per_shard = block_producers.len() as ShardId / vs.validator_groups; - let coef = block_producers.len() as ShardId / vs.num_shards; - - let chunk_producers: Vec> = (0..vs.num_shards) - .map(|shard_id| { - let offset = (shard_id * coef / validators_per_shard * validators_per_shard) - as usize; - block_producers[offset..offset + validators_per_shard as usize].to_vec() - }) - .collect(); - - EpochValidatorSet { block_producers, chunk_producers } - }) - .collect(); - - if !vs.chunk_only_producers.is_empty() { - assert_eq!(validators_by_valset.len(), vs.chunk_only_producers.len()); - for (epoch_idx, epoch_cops) in vs.chunk_only_producers.into_iter().enumerate() { - for (shard_idx, shard_cops) in epoch_cops.into_iter().enumerate() { - for account_id in shard_cops { - let stake = ValidatorStake::new( - account_id.clone(), - SecretKey::from_seed(KeyType::ED25519, account_id.as_ref()) - .public_key(), - 1_000_000, - ); - let prev = validators.insert(account_id, stake.clone()); - assert!(prev.is_none(), "chunk only produced is also a block producer"); - validators_by_valset[epoch_idx].chunk_producers[shard_idx].push(stake) - } - } - } - } - - KeyValueRuntime { - store, - tries, - validators, - validators_by_valset, - num_shards: vs.num_shards, - tracks_all_shards: false, - epoch_length, - state: RwLock::new(state), - state_size: RwLock::new(state_size), - headers_cache: RwLock::new(HashMap::new()), - hash_to_epoch: RwLock::new(HashMap::new()), - hash_to_next_epoch_approvals_req: RwLock::new(HashMap::new()), - hash_to_next_epoch: RwLock::new(map_with_default_hash1), - hash_to_valset: RwLock::new(map_with_default_hash3), - epoch_start: RwLock::new(map_with_default_hash2), - no_gc, - } - } - - pub fn set_tracks_all_shards(&mut self, tracks_all_shards: bool) { - self.tracks_all_shards = tracks_all_shards; - } - - fn get_block_header(&self, hash: &CryptoHash) -> Result, Error> { - let mut headers_cache = self.headers_cache.write().unwrap(); - if headers_cache.get(hash).is_some() { - return Ok(Some(headers_cache.get(hash).unwrap().clone())); - } - if let Some(result) = self.store.get_ser(DBCol::BlockHeader, hash.as_ref())? { - headers_cache.insert(*hash, result); - return Ok(Some(headers_cache.get(hash).unwrap().clone())); - } - Ok(None) - } - - fn get_epoch_and_valset( - &self, - prev_hash: CryptoHash, - ) -> Result<(EpochId, usize, EpochId), Error> { - if prev_hash == CryptoHash::default() { - return Ok((EpochId(prev_hash), 0, EpochId(prev_hash))); - } - let prev_block_header = self - .get_block_header(&prev_hash)? - .ok_or_else(|| Error::DBNotFoundErr(prev_hash.to_string()))?; - - let mut hash_to_epoch = self.hash_to_epoch.write().unwrap(); - let mut hash_to_next_epoch_approvals_req = - self.hash_to_next_epoch_approvals_req.write().unwrap(); - let mut hash_to_next_epoch = self.hash_to_next_epoch.write().unwrap(); - let mut hash_to_valset = self.hash_to_valset.write().unwrap(); - let mut epoch_start_map = self.epoch_start.write().unwrap(); - - let prev_prev_hash = *prev_block_header.prev_hash(); - let prev_epoch = hash_to_epoch.get(&prev_prev_hash); - let prev_next_epoch = hash_to_next_epoch.get(&prev_prev_hash).unwrap(); - let prev_valset = match prev_epoch { - Some(prev_epoch) => Some(*hash_to_valset.get(prev_epoch).unwrap()), - None => None, - }; - - let prev_epoch_start = *epoch_start_map.get(&prev_prev_hash).unwrap(); - - let last_final_height = if prev_block_header.last_final_block() == &CryptoHash::default() { - 0 - } else { - self.get_block_header(prev_block_header.last_final_block()).unwrap().unwrap().height() - }; - - let increment_epoch = prev_prev_hash == CryptoHash::default() // genesis is in its own epoch - || last_final_height + 3 >= prev_epoch_start + self.epoch_length; - - let needs_next_epoch_approvals = !increment_epoch - && last_final_height + 3 < prev_epoch_start + self.epoch_length - && prev_block_header.height() + 3 >= prev_epoch_start + self.epoch_length; - - let (epoch, next_epoch, valset, epoch_start) = if increment_epoch { - let new_valset = match prev_valset { - None => 0, - Some(prev_valset) => prev_valset + 1, - }; - ( - prev_next_epoch.clone(), - EpochId(prev_hash), - new_valset, - prev_block_header.height() + 1, - ) - } else { - ( - prev_epoch.unwrap().clone(), - prev_next_epoch.clone(), - prev_valset.unwrap(), - prev_epoch_start, - ) - }; - - hash_to_next_epoch.insert(prev_hash, next_epoch.clone()); - hash_to_epoch.insert(prev_hash, epoch.clone()); - hash_to_next_epoch_approvals_req.insert(prev_hash, needs_next_epoch_approvals); - hash_to_valset.insert(epoch.clone(), valset); - hash_to_valset.insert(next_epoch.clone(), valset + 1); - epoch_start_map.insert(prev_hash, epoch_start); - - Ok((epoch, valset as usize % self.validators_by_valset.len(), next_epoch)) - } - - fn get_block_producers(&self, valset: usize) -> &[ValidatorStake] { - &self.validators_by_valset[valset].block_producers - } - - fn get_chunk_producers(&self, valset: usize, shard_id: ShardId) -> Vec { - self.validators_by_valset[valset].chunk_producers[shard_id as usize].clone() - } - - fn get_valset_for_epoch(&self, epoch_id: &EpochId) -> Result { - // conveniently here if the prev_hash is passed mistakenly instead of the epoch_hash, - // the `unwrap` will trigger - Ok(*self - .hash_to_valset - .read() - .unwrap() - .get(epoch_id) - .ok_or_else(|| Error::EpochOutOfBounds(epoch_id.clone()))? as usize - % self.validators_by_valset.len()) - } - - pub fn get_chunk_only_producers_for_shard( - &self, - epoch_id: &EpochId, - shard_id: ShardId, - ) -> Result, Error> { - let valset = self.get_valset_for_epoch(epoch_id)?; - let block_producers = &self.validators_by_valset[valset].block_producers; - let chunk_producers = &self.validators_by_valset[valset].chunk_producers[shard_id as usize]; - Ok(chunk_producers.iter().filter(|it| !block_producers.contains(it)).collect()) - } -} - -impl EpochManagerAdapter for KeyValueRuntime { - fn epoch_exists(&self, epoch_id: &EpochId) -> bool { - self.hash_to_valset.write().unwrap().contains_key(epoch_id) - } - - fn num_shards(&self, _epoch_id: &EpochId) -> Result { - Ok(self.num_shards) - } - - fn num_total_parts(&self) -> usize { - 12 + (self.num_shards as usize + 1) % 50 - } - - fn num_data_parts(&self) -> usize { - // Same as in Nightshade Runtime - let total_parts = self.num_total_parts(); - if total_parts <= 3 { - 1 - } else { - (total_parts - 1) / 3 - } - } - - fn get_part_owner(&self, epoch_id: &EpochId, part_id: u64) -> Result { - let validators = - &self.get_epoch_block_producers_ordered(epoch_id, &CryptoHash::default())?; - // if we don't use data_parts and total_parts as part of the formula here, the part owner - // would not depend on height, and tests wouldn't catch passing wrong height here - let idx = part_id as usize + self.num_data_parts() + self.num_total_parts(); - Ok(validators[idx as usize % validators.len()].0.account_id().clone()) - } - - fn account_id_to_shard_id( - &self, - account_id: &AccountId, - _epoch_id: &EpochId, - ) -> Result { - Ok(account_id_to_shard_id(account_id, self.num_shards)) - } - - fn shard_id_to_uid(&self, shard_id: ShardId, _epoch_id: &EpochId) -> Result { - Ok(ShardUId { version: 0, shard_id: shard_id as u32 }) - } - - fn get_block_info(&self, _hash: &CryptoHash) -> Result, Error> { - Ok(Default::default()) - } - - fn get_epoch_config(&self, _epoch_id: &EpochId) -> Result { - Ok(EpochConfig { - epoch_length: 10, - num_block_producer_seats: 2, - num_block_producer_seats_per_shard: vec![1, 1], - avg_hidden_validator_seats_per_shard: vec![1, 1], - block_producer_kickout_threshold: 0, - chunk_producer_kickout_threshold: 0, - validator_max_kickout_stake_perc: 0, - online_min_threshold: Ratio::new(1i32, 4i32), - online_max_threshold: Ratio::new(3i32, 4i32), - fishermen_threshold: 1, - minimum_stake_divisor: 1, - protocol_upgrade_stake_threshold: Ratio::new(3i32, 4i32), - protocol_upgrade_num_epochs: 100, - shard_layout: ShardLayout::v1_test(), - validator_selection_config: ValidatorSelectionConfig::default(), - }) - } - - fn get_epoch_info(&self, _epoch_id: &EpochId) -> Result, Error> { - Ok(Arc::new(EpochInfo::v1_test())) - } - - fn get_shard_layout(&self, _epoch_id: &EpochId) -> Result { - Ok(ShardLayout::v0(self.num_shards, 0)) - } - - fn get_shard_config(&self, _epoch_id: &EpochId) -> Result { - panic!("get_shard_config not implemented for KeyValueRuntime"); - } - - fn is_next_block_epoch_start(&self, parent_hash: &CryptoHash) -> Result { - if parent_hash == &CryptoHash::default() { - return Ok(true); - } - let prev_block_header = self.get_block_header(parent_hash)?.ok_or_else(|| { - Error::Other(format!("Missing block {} when computing the epoch", parent_hash)) - })?; - let prev_prev_hash = *prev_block_header.prev_hash(); - Ok(self.get_epoch_and_valset(*parent_hash)?.0 - != self.get_epoch_and_valset(prev_prev_hash)?.0) - } - - fn get_epoch_id_from_prev_block(&self, parent_hash: &CryptoHash) -> Result { - Ok(self.get_epoch_and_valset(*parent_hash)?.0) - } - - fn get_epoch_height_from_prev_block( - &self, - _prev_block_hash: &CryptoHash, - ) -> Result { - Ok(0) - } - - fn get_next_epoch_id_from_prev_block( - &self, - parent_hash: &CryptoHash, - ) -> Result { - Ok(self.get_epoch_and_valset(*parent_hash)?.2) - } - - fn get_prev_shard_ids( - &self, - _prev_hash: &CryptoHash, - shard_ids: Vec, - ) -> Result, Error> { - Ok(shard_ids) - } - - fn get_shard_layout_from_prev_block( - &self, - _parent_hash: &CryptoHash, - ) -> Result { - Ok(ShardLayout::v0(self.num_shards, 0)) - } - - fn get_epoch_id(&self, block_hash: &CryptoHash) -> Result { - let (epoch_id, _, _) = self.get_epoch_and_valset(*block_hash)?; - Ok(epoch_id) - } - - fn compare_epoch_id( - &self, - epoch_id: &EpochId, - other_epoch_id: &EpochId, - ) -> Result { - if epoch_id.0 == other_epoch_id.0 { - return Ok(Ordering::Equal); - } - match (self.get_valset_for_epoch(epoch_id), self.get_valset_for_epoch(other_epoch_id)) { - (Ok(index1), Ok(index2)) => Ok(index1.cmp(&index2)), - _ => Err(Error::EpochOutOfBounds(epoch_id.clone())), - } - } - - fn get_epoch_start_height(&self, block_hash: &CryptoHash) -> Result { - let epoch_id = self.get_epoch_id(block_hash)?; - match self.get_block_header(&epoch_id.0)? { - Some(block_header) => Ok(block_header.height()), - None => Ok(0), - } - } - - fn get_prev_epoch_id_from_prev_block( - &self, - prev_block_hash: &CryptoHash, - ) -> Result { - let mut candidate_hash = *prev_block_hash; - loop { - let header = self - .get_block_header(&candidate_hash)? - .ok_or_else(|| Error::DBNotFoundErr(candidate_hash.to_string()))?; - candidate_hash = *header.prev_hash(); - if self.is_next_block_epoch_start(&candidate_hash)? { - break Ok(self.get_epoch_and_valset(candidate_hash)?.0); - } - } - } - - fn get_estimated_protocol_upgrade_block_height( - &self, - _block_hash: CryptoHash, - ) -> Result, EpochError> { - Ok(None) - } - - fn get_epoch_block_producers_ordered( - &self, - epoch_id: &EpochId, - _last_known_block_hash: &CryptoHash, - ) -> Result, Error> { - let validators = self.get_block_producers(self.get_valset_for_epoch(epoch_id)?); - Ok(validators.iter().map(|x| (x.clone(), false)).collect()) - } - - fn get_epoch_block_approvers_ordered( - &self, - parent_hash: &CryptoHash, - ) -> Result, Error> { - let (_cur_epoch, cur_valset, next_epoch) = self.get_epoch_and_valset(*parent_hash)?; - let mut validators = self - .get_block_producers(cur_valset) - .iter() - .map(|x| x.get_approval_stake(false)) - .collect::>(); - if *self.hash_to_next_epoch_approvals_req.write().unwrap().get(parent_hash).unwrap() { - let validators_copy = validators.clone(); - validators.extend( - self.get_block_producers(self.get_valset_for_epoch(&next_epoch)?) - .iter() - .filter(|x| { - !validators_copy.iter().any(|entry| &entry.account_id == x.account_id()) - }) - .map(|x| x.get_approval_stake(true)), - ); - } - let validators = validators.into_iter().map(|stake| (stake, false)).collect::>(); - Ok(validators) - } - - fn get_epoch_chunk_producers(&self, _epoch_id: &EpochId) -> Result, Error> { - tracing::warn!("not implemented, returning a dummy value"); - Ok(vec![]) - } - - fn get_block_producer( - &self, - epoch_id: &EpochId, - height: BlockHeight, - ) -> Result { - let validators = self.get_block_producers(self.get_valset_for_epoch(epoch_id)?); - Ok(validators[(height as usize) % validators.len()].account_id().clone()) - } - - fn get_chunk_producer( - &self, - epoch_id: &EpochId, - height: BlockHeight, - shard_id: ShardId, - ) -> Result { - let valset = self.get_valset_for_epoch(epoch_id)?; - let chunk_producers = self.get_chunk_producers(valset, shard_id); - let index = (shard_id + height + 1) as usize % chunk_producers.len(); - Ok(chunk_producers[index].account_id().clone()) - } - - fn get_validator_by_account_id( - &self, - epoch_id: &EpochId, - _last_known_block_hash: &CryptoHash, - account_id: &AccountId, - ) -> Result<(ValidatorStake, bool), Error> { - let validators = &self.validators_by_valset[self.get_valset_for_epoch(epoch_id)?]; - for validator_stake in validators.block_producers.iter() { - if validator_stake.account_id() == account_id { - return Ok((validator_stake.clone(), false)); - } - } - for validator_stake in validators.chunk_producers.iter().flatten() { - if validator_stake.account_id() == account_id { - return Ok((validator_stake.clone(), false)); - } - } - Err(Error::NotAValidator) - } - - fn get_fisherman_by_account_id( - &self, - _epoch_id: &EpochId, - _last_known_block_hash: &CryptoHash, - _account_id: &AccountId, - ) -> Result<(ValidatorStake, bool), Error> { - Err(Error::NotAValidator) - } - - fn get_validator_info( - &self, - _epoch_id: ValidatorInfoIdentifier, - ) -> Result { - Ok(EpochValidatorInfo { - current_validators: vec![], - next_validators: vec![], - current_fishermen: vec![], - next_fishermen: vec![], - current_proposals: vec![], - prev_epoch_kickout: vec![], - epoch_start_height: 0, - epoch_height: 1, - }) - } - - fn get_epoch_minted_amount(&self, _epoch_id: &EpochId) -> Result { - Ok(0) - } - - fn get_epoch_protocol_version(&self, _epoch_id: &EpochId) -> Result { - Ok(PROTOCOL_VERSION) - } - - fn get_epoch_sync_data( - &self, - _prev_epoch_last_block_hash: &CryptoHash, - _epoch_id: &EpochId, - _next_epoch_id: &EpochId, - ) -> Result< - ( - Arc, - Arc, - Arc, - Arc, - Arc, - Arc, - ), - Error, - > { - Ok(Default::default()) - } - - fn epoch_sync_init_epoch_manager( - &self, - _prev_epoch_first_block_info: BlockInfo, - _prev_epoch_last_block_info: BlockInfo, - _prev_epoch_prev_last_block_info: BlockInfo, - _prev_epoch_id: &EpochId, - _prev_epoch_info: EpochInfo, - _epoch_id: &EpochId, - _epoch_info: EpochInfo, - _next_epoch_id: &EpochId, - _next_epoch_info: EpochInfo, - ) -> Result<(), Error> { - Ok(()) - } - - fn verify_block_vrf( - &self, - _epoch_id: &EpochId, - _block_height: BlockHeight, - _prev_random_value: &CryptoHash, - _vrf_value: &near_crypto::vrf::Value, - _vrf_proof: &near_crypto::vrf::Proof, - ) -> Result<(), Error> { - Ok(()) - } - - fn verify_validator_signature( - &self, - _epoch_id: &EpochId, - _last_known_block_hash: &CryptoHash, - _account_id: &AccountId, - _data: &[u8], - _signature: &Signature, - ) -> Result { - Ok(true) - } - - fn verify_validator_or_fisherman_signature( - &self, - _epoch_id: &EpochId, - _last_known_block_hash: &CryptoHash, - _account_id: &AccountId, - _data: &[u8], - _signature: &Signature, - ) -> Result { - Ok(true) - } - - fn verify_header_signature(&self, header: &BlockHeader) -> Result { - let validator = self.get_block_producer(&header.epoch_id(), header.height())?; - let validator_stake = &self.validators[&validator]; - Ok(header.verify_block_producer(validator_stake.public_key())) - } - - fn verify_chunk_signature_with_header_parts( - &self, - _chunk_hash: &ChunkHash, - _signature: &Signature, - _epoch_id: &EpochId, - _last_kown_hash: &CryptoHash, - _height_created: BlockHeight, - _shard_id: ShardId, - ) -> Result { - Ok(true) - } - - fn verify_approval( - &self, - _prev_block_hash: &CryptoHash, - _prev_block_height: BlockHeight, - _block_height: BlockHeight, - _approvals: &[Option], - ) -> Result { - Ok(true) - } - - fn verify_approvals_and_threshold_orphan( - &self, - epoch_id: &EpochId, - can_approved_block_be_produced: &dyn Fn( - &[Option], - &[(Balance, Balance, bool)], - ) -> bool, - prev_block_hash: &CryptoHash, - prev_block_height: BlockHeight, - block_height: BlockHeight, - 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( - &if prev_block_height + 1 == block_height { - ApprovalInner::Endorsement(*prev_block_hash) - } else { - ApprovalInner::Skip(prev_block_height) - }, - block_height, - ); - - for (validator, may_be_signature) in validators.iter().zip(approvals.iter()) { - if let Some(signature) = may_be_signature { - if !signature.verify(message_to_sign.as_ref(), validator.public_key()) { - return Err(Error::InvalidApprovals); - } - } - } - let stakes = validators.iter().map(|stake| (stake.stake(), 0, false)).collect::>(); - if !can_approved_block_be_produced(approvals, &stakes) { - Err(Error::NotEnoughApprovals) - } else { - Ok(()) - } - } -} - -impl RuntimeAdapter for KeyValueRuntime { - fn genesis_state(&self) -> (Store, Vec) { - (self.store.clone(), ((0..self.num_shards).map(|_| Trie::EMPTY_ROOT).collect())) - } - - fn store(&self) -> &Store { - &self.store - } - - fn get_tries(&self) -> ShardTries { - self.tries.clone() - } - - fn get_trie_for_shard( - &self, - shard_id: ShardId, - _block_hash: &CryptoHash, - state_root: StateRoot, - _use_flat_storage: bool, - ) -> Result { - Ok(self - .tries - .get_trie_for_shard(ShardUId { version: 0, shard_id: shard_id as u32 }, state_root)) - } - - fn get_view_trie_for_shard( - &self, - shard_id: ShardId, - _block_hash: &CryptoHash, - state_root: StateRoot, - ) -> Result { - Ok(self.tries.get_view_trie_for_shard( - ShardUId { version: 0, shard_id: shard_id as u32 }, - state_root, - )) - } - - fn get_flat_storage_state_for_shard(&self, _shard_id: ShardId) -> Option { - None - } - - fn try_create_flat_storage_state_for_shard( - &self, - _shard_id: ShardId, - _latest_block_height: BlockHeight, - _chain_access: &dyn ChainAccessForFlatStorage, - ) -> FlatStorageStateStatus { - FlatStorageStateStatus::DontCreate - } - - fn set_flat_storage_state_for_genesis( - &self, - _genesis_block: &CryptoHash, - _genesis_epoch_id: &EpochId, - ) -> Result { - Ok(self.store.store_update()) - } - - fn cares_about_shard( - &self, - account_id: Option<&AccountId>, - parent_hash: &CryptoHash, - shard_id: ShardId, - _is_me: bool, - ) -> bool { - if self.tracks_all_shards { - return true; - } - // This `unwrap` here tests that in all code paths we check that the epoch exists before - // we check if we care about a shard. Please do not remove the unwrap, fix the logic of - // the calling function. - let epoch_valset = self.get_epoch_and_valset(*parent_hash).unwrap(); - let chunk_producers = self.get_chunk_producers(epoch_valset.1, shard_id); - if let Some(account_id) = account_id { - for validator in chunk_producers { - if validator.account_id() == account_id { - return true; - } - } - } - false - } - - fn will_care_about_shard( - &self, - account_id: Option<&AccountId>, - parent_hash: &CryptoHash, - shard_id: ShardId, - _is_me: bool, - ) -> bool { - if self.tracks_all_shards { - return true; - } - // This `unwrap` here tests that in all code paths we check that the epoch exists before - // we check if we care about a shard. Please do not remove the unwrap, fix the logic of - // the calling function. - let epoch_valset = self.get_epoch_and_valset(*parent_hash).unwrap(); - let chunk_producers = self - .get_chunk_producers((epoch_valset.1 + 1) % self.validators_by_valset.len(), shard_id); - if let Some(account_id) = account_id { - for validator in chunk_producers { - if validator.account_id() == account_id { - return true; - } - } - } - false - } - - fn validate_tx( - &self, - _gas_price: Balance, - _state_update: Option, - _transaction: &SignedTransaction, - _verify_signature: bool, - _epoch_id: &EpochId, - _current_protocol_version: ProtocolVersion, - ) -> Result, Error> { - Ok(None) - } - - fn prepare_transactions( - &self, - _gas_price: Balance, - _gas_limit: Gas, - _epoch_id: &EpochId, - _shard_id: ShardId, - _state_root: StateRoot, - _next_block_height: BlockHeight, - transactions: &mut dyn PoolIterator, - _chain_validate: &mut dyn FnMut(&SignedTransaction) -> bool, - _current_protocol_version: ProtocolVersion, - ) -> Result, Error> { - let mut res = vec![]; - while let Some(iter) = transactions.next() { - res.push(iter.next().unwrap()); - } - Ok(res) - } - - fn add_validator_proposals( - &self, - _block_header_info: BlockHeaderInfo, - ) -> Result { - Ok(self.store.store_update()) - } - - fn apply_transactions_with_optional_storage_proof( - &self, - shard_id: ShardId, - state_root: &StateRoot, - _height: BlockHeight, - _block_timestamp: u64, - _prev_block_hash: &CryptoHash, - block_hash: &CryptoHash, - receipts: &[Receipt], - transactions: &[SignedTransaction], - _last_validator_proposals: ValidatorStakeIter, - gas_price: Balance, - _gas_limit: Gas, - _challenges: &ChallengesResult, - _random_seed: CryptoHash, - generate_storage_proof: bool, - _is_new_chunk: bool, - _is_first_block_with_chunk_of_version: bool, - _state_patch: SandboxStatePatch, - _use_flat_storage: bool, - ) -> Result { - assert!(!generate_storage_proof); - let mut tx_results = vec![]; - - let mut state = self.state.read().unwrap().get(state_root).cloned().unwrap(); - - let mut balance_transfers = vec![]; - - for receipt in receipts.iter() { - if let ReceiptEnum::Action(action) = &receipt.receipt { - assert_eq!( - self.account_id_to_shard_id(&receipt.receiver_id, &EpochId::default())?, - shard_id - ); - if !state.receipt_nonces.contains(&receipt.receipt_id) { - state.receipt_nonces.insert(receipt.receipt_id); - if let Action::Transfer(TransferAction { deposit }) = action.actions[0] { - balance_transfers.push(( - receipt.get_hash(), - receipt.predecessor_id.clone(), - receipt.receiver_id.clone(), - deposit, - 0, - )); - } - } else { - panic!("receipts should never be applied twice"); - } - } else { - unreachable!(); - } - } - - for transaction in transactions { - assert_eq!( - self.account_id_to_shard_id( - &transaction.transaction.signer_id, - &EpochId::default() - )?, - shard_id - ); - if transaction.transaction.actions.is_empty() { - continue; - } - if let Action::Transfer(TransferAction { deposit }) = transaction.transaction.actions[0] - { - if !state.tx_nonces.contains(&AccountNonce( - transaction.transaction.receiver_id.clone(), - transaction.transaction.nonce, - )) { - state.tx_nonces.insert(AccountNonce( - transaction.transaction.receiver_id.clone(), - transaction.transaction.nonce, - )); - balance_transfers.push(( - transaction.get_hash(), - transaction.transaction.signer_id.clone(), - transaction.transaction.receiver_id.clone(), - deposit, - transaction.transaction.nonce, - )); - } else { - balance_transfers.push(( - transaction.get_hash(), - transaction.transaction.signer_id.clone(), - transaction.transaction.receiver_id.clone(), - 0, - transaction.transaction.nonce, - )); - } - } else { - unreachable!(); - } - } - - let mut outgoing_receipts = vec![]; - - for (hash, from, to, amount, nonce) in balance_transfers { - let mut good_to_go = false; - - if self.account_id_to_shard_id(&from, &EpochId::default())? != shard_id { - // This is a receipt, was already debited - good_to_go = true; - } else if let Some(balance) = state.amounts.get(&from) { - if *balance >= amount { - let new_balance = balance - amount; - state.amounts.insert(from.clone(), new_balance); - good_to_go = true; - } - } - - if good_to_go { - let new_receipt_hashes = if self.account_id_to_shard_id(&to, &EpochId::default())? - == shard_id - { - state.amounts.insert(to.clone(), state.amounts.get(&to).unwrap_or(&0) + amount); - vec![] - } else { - assert_ne!(nonce, 0); - let receipt = Receipt { - predecessor_id: from.clone(), - receiver_id: to.clone(), - receipt_id: create_receipt_nonce(from.clone(), to.clone(), amount, nonce), - receipt: ReceiptEnum::Action(ActionReceipt { - signer_id: from.clone(), - signer_public_key: PublicKey::empty(KeyType::ED25519), - gas_price, - output_data_receivers: vec![], - input_data_ids: vec![], - actions: vec![Action::Transfer(TransferAction { deposit: amount })], - }), - }; - let receipt_hash = receipt.get_hash(); - outgoing_receipts.push(receipt); - vec![receipt_hash] - }; - - tx_results.push(ExecutionOutcomeWithId { - id: hash, - outcome: ExecutionOutcome { - status: ExecutionStatus::SuccessValue(vec![]), - logs: vec![], - receipt_ids: new_receipt_hashes, - gas_burnt: 0, - tokens_burnt: 0, - executor_id: to.clone(), - metadata: ExecutionMetadata::V1, - }, - }); - } - } - - let data = state.try_to_vec()?; - let state_size = data.len() as u64; - let state_root = hash(&data); - self.state.write().unwrap().insert(state_root, state); - self.state_size.write().unwrap().insert(state_root, state_size); - - Ok(ApplyTransactionResult { - trie_changes: WrappedTrieChanges::new( - self.get_tries(), - ShardUId { version: 0, shard_id: shard_id as u32 }, - TrieChanges::empty(state_root), - Default::default(), - *block_hash, - ), - new_root: state_root, - outcomes: tx_results, - outgoing_receipts, - validator_proposals: vec![], - total_gas_burnt: 0, - total_balance_burnt: 0, - proof: None, - processed_delayed_receipts: vec![], - }) - } - - fn check_state_transition( - &self, - _partial_storage: PartialStorage, - _shard_id: ShardId, - _state_root: &StateRoot, - _height: BlockHeight, - _block_timestamp: u64, - _prev_block_hash: &CryptoHash, - _block_hash: &CryptoHash, - _receipts: &[Receipt], - _transactions: &[SignedTransaction], - _last_validator_proposals: ValidatorStakeIter, - _gas_price: Balance, - _gas_limit: Gas, - _challenges: &ChallengesResult, - _random_value: CryptoHash, - _is_new_chunk: bool, - _is_first_block_with_chunk_of_version: bool, - ) -> Result { - unimplemented!(); - } - - fn query( - &self, - _shard_id: ShardUId, - state_root: &StateRoot, - block_height: BlockHeight, - _block_timestamp: u64, - _prev_block_hash: &CryptoHash, - block_hash: &CryptoHash, - _epoch_id: &EpochId, - request: &QueryRequest, - ) -> Result { - match request { - QueryRequest::ViewAccount { account_id, .. } => Ok(QueryResponse { - kind: QueryResponseKind::ViewAccount( - Account::new( - self.state.read().unwrap().get(state_root).map_or_else( - || 0, - |state| *state.amounts.get(account_id).unwrap_or(&0), - ), - 0, - CryptoHash::default(), - 0, - ) - .into(), - ), - block_height, - block_hash: *block_hash, - }), - QueryRequest::ViewCode { .. } => Ok(QueryResponse { - kind: QueryResponseKind::ViewCode(ContractCodeView { - code: vec![], - hash: CryptoHash::default(), - }), - block_height, - block_hash: *block_hash, - }), - QueryRequest::ViewAccessKeyList { .. } => Ok(QueryResponse { - kind: QueryResponseKind::AccessKeyList(AccessKeyList { - keys: vec![AccessKeyInfoView { - public_key: PublicKey::empty(KeyType::ED25519), - access_key: AccessKey::full_access().into(), - }], - }), - block_height, - block_hash: *block_hash, - }), - QueryRequest::ViewAccessKey { .. } => Ok(QueryResponse { - kind: QueryResponseKind::AccessKey(AccessKey::full_access().into()), - block_height, - block_hash: *block_hash, - }), - QueryRequest::ViewState { .. } => Ok(QueryResponse { - kind: QueryResponseKind::ViewState(ViewStateResult { - values: Default::default(), - proof: vec![], - }), - block_height, - block_hash: *block_hash, - }), - QueryRequest::CallFunction { .. } => Ok(QueryResponse { - kind: QueryResponseKind::CallResult(CallResult { - result: Default::default(), - logs: Default::default(), - }), - block_height, - block_hash: *block_hash, - }), - } - } - - fn obtain_state_part( - &self, - _shard_id: ShardId, - _block_hash: &CryptoHash, - state_root: &StateRoot, - part_id: PartId, - ) -> Result, Error> { - if part_id.idx != 0 { - return Ok(vec![]); - } - let state = self.state.read().unwrap().get(state_root).unwrap().clone(); - let data = state.try_to_vec().expect("should never fall"); - Ok(data) - } - - fn validate_state_part(&self, _state_root: &StateRoot, _part_id: PartId, _data: &[u8]) -> bool { - // We do not care about deeper validation in test_utils - true - } - - fn apply_state_part( - &self, - _shard_id: ShardId, - state_root: &StateRoot, - part_id: PartId, - data: &[u8], - _epoch_id: &EpochId, - ) -> Result<(), Error> { - if part_id.idx != 0 { - return Ok(()); - } - let state = KVState::try_from_slice(data).unwrap(); - self.state.write().unwrap().insert(*state_root, state.clone()); - let data = state.try_to_vec()?; - let state_size = data.len() as u64; - self.state_size.write().unwrap().insert(*state_root, state_size); - Ok(()) - } - - fn get_state_root_node( - &self, - _shard_id: ShardId, - _block_hash: &CryptoHash, - state_root: &StateRoot, - ) -> Result { - let data = self - .state - .read() - .unwrap() - .get(state_root) - .unwrap() - .clone() - .try_to_vec() - .expect("should never fall") - .into(); - let memory_usage = *self.state_size.read().unwrap().get(state_root).unwrap(); - Ok(StateRootNode { data, memory_usage }) - } - - fn validate_state_root_node( - &self, - _state_root_node: &StateRootNode, - _state_root: &StateRoot, - ) -> bool { - // We do not care about deeper validation in test_utils - true - } - - fn get_gc_stop_height(&self, block_hash: &CryptoHash) -> BlockHeight { - if !self.no_gc { - // This code is 'incorrect' - as production one is always setting the GC to the - // first block of the epoch. - // Unfortunately many tests are depending on this and not setting epochs when - // they produce blocks. - let block_height = self - .get_block_header(block_hash) - .unwrap_or_default() - .map(|h| h.height()) - .unwrap_or_default(); - block_height.saturating_sub(DEFAULT_GC_NUM_EPOCHS_TO_KEEP * self.epoch_length) - /* // TODO: use this version of the code instead - after we fix the block creation - // issue in multiple tests. - // We have to return the first block of the epoch T-DEFAULT_GC_NUM_EPOCHS_TO_KEEP. - let mut current_header = self.get_block_header(block_hash).unwrap().unwrap(); - for _ in 0..DEFAULT_GC_NUM_EPOCHS_TO_KEEP { - let last_block_of_prev_epoch = current_header.next_epoch_id(); - current_header = - self.get_block_header(&last_block_of_prev_epoch.0).unwrap().unwrap(); - } - loop { - if current_header.next_epoch_id().0 == *current_header.prev_hash() { - break; - } - current_header = - self.get_block_header(current_header.prev_hash()).unwrap().unwrap(); - } - current_header.height()*/ - } else { - 0 - } - } - - fn chunk_needs_to_be_fetched_from_archival( - &self, - _chunk_prev_block_hash: &CryptoHash, - _header_head: &CryptoHash, - ) -> Result { - Ok(false) - } - - fn get_protocol_config(&self, _epoch_id: &EpochId) -> Result { - unreachable!("get_protocol_config should not be called in KeyValueRuntime"); - } - - fn will_shard_layout_change_next_epoch( - &self, - _parent_hash: &CryptoHash, - ) -> Result { - Ok(false) - } - - fn apply_update_to_split_states( - &self, - _block_hash: &CryptoHash, - _state_roots: HashMap, - _next_shard_layout: &ShardLayout, - _state_changes: StateChangesForSplitStates, - ) -> Result, Error> { - Ok(vec![]) - } - - fn build_state_for_split_shards( - &self, - _shard_uid: ShardUId, - _state_root: &StateRoot, - _next_epoch_shard_layout: &ShardLayout, - _state_split_status: Arc, - ) -> Result, Error> { - Ok(HashMap::new()) - } -} - pub fn setup() -> (Chain, Arc, Arc) { setup_with_tx_validity_period(100) } diff --git a/chain/chain/src/test_utils/kv_runtime.rs b/chain/chain/src/test_utils/kv_runtime.rs new file mode 100644 index 00000000000..a9770b9f1bb --- /dev/null +++ b/chain/chain/src/test_utils/kv_runtime.rs @@ -0,0 +1,1346 @@ +use std::cmp::Ordering; +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, RwLock}; + +use borsh::{BorshDeserialize, BorshSerialize}; + +use near_epoch_manager::EpochManagerAdapter; +use near_primitives::sandbox::state_patch::SandboxStatePatch; +use near_primitives::state_part::PartId; +use num_rational::Ratio; + +use near_chain_configs::{ProtocolConfig, DEFAULT_GC_NUM_EPOCHS_TO_KEEP}; +use near_chain_primitives::Error; +use near_client_primitives::types::StateSplitApplyingStatus; +use near_crypto::{KeyType, PublicKey, SecretKey, Signature}; +use near_pool::types::PoolIterator; +use near_primitives::account::{AccessKey, Account}; +use near_primitives::block_header::{Approval, ApprovalInner}; +use near_primitives::challenge::ChallengesResult; +use near_primitives::epoch_manager::block_info::BlockInfo; +use near_primitives::epoch_manager::epoch_info::EpochInfo; +use near_primitives::epoch_manager::EpochConfig; +use near_primitives::epoch_manager::ValidatorSelectionConfig; +use near_primitives::errors::{EpochError, InvalidTxError}; +use near_primitives::hash::{hash, CryptoHash}; +use near_primitives::receipt::{ActionReceipt, Receipt, ReceiptEnum}; +use near_primitives::shard_layout; +use near_primitives::shard_layout::{ShardLayout, ShardUId}; +use near_primitives::sharding::ChunkHash; +use near_primitives::transaction::{ + Action, ExecutionMetadata, ExecutionOutcome, ExecutionOutcomeWithId, ExecutionStatus, + SignedTransaction, TransferAction, +}; +use near_primitives::types::validator_stake::{ValidatorStake, ValidatorStakeIter}; +use near_primitives::types::{ + AccountId, ApprovalStake, Balance, BlockHeight, EpochHeight, EpochId, Gas, Nonce, NumShards, + ShardId, StateChangesForSplitStates, StateRoot, StateRootNode, ValidatorInfoIdentifier, +}; +use near_primitives::version::{ProtocolVersion, PROTOCOL_VERSION}; +use near_primitives::views::{ + AccessKeyInfoView, AccessKeyList, CallResult, ContractCodeView, EpochValidatorInfo, + QueryRequest, QueryResponse, QueryResponseKind, ViewStateResult, +}; +use near_store::{ + DBCol, PartialStorage, ShardTries, Store, StoreUpdate, Trie, TrieChanges, WrappedTrieChanges, +}; + +use crate::types::{ApplySplitStateResult, ApplyTransactionResult, BlockHeaderInfo}; +use crate::{BlockHeader, RuntimeAdapter}; + +use near_primitives::epoch_manager::ShardConfig; + +use near_store::flat_state::ChainAccessForFlatStorage; +use near_store::flat_state::{FlatStorageState, FlatStorageStateStatus}; + +use super::ValidatorSchedule; + +/// Simple key value runtime for tests. +/// +/// Major differences with production `NightshadeRuntime`: +/// * Uses in-memory storage +/// * Doesn't have WASM runtime, so can only process simple transfer +/// transaction +/// * Uses hard-coded validator schedule instead of using `EpochManager` and +/// staking to assign block and chunk producers. +pub struct KeyValueRuntime { + store: Store, + tries: ShardTries, + /// A pre determined list of validator sets. We rotate validator set in this list. + /// Epoch i uses validators from `validators_by_valset[i % validators_by_valset.len()]`. + validators_by_valset: Vec, + /// Maps from account id to validator stake for all validators, both block producers and + /// chunk producers + validators: HashMap, + num_shards: NumShards, + tracks_all_shards: bool, + epoch_length: u64, + no_gc: bool, + + // A mapping state_root => {account id => amounts}, for transactions and receipts + state: RwLock>, + state_size: RwLock>, + + headers_cache: RwLock>, + hash_to_epoch: RwLock>, + hash_to_next_epoch_approvals_req: RwLock>, + hash_to_next_epoch: RwLock>, + /// Maps EpochId to index of `validators_by_valset` to determine validators for an epoch + hash_to_valset: RwLock>, + epoch_start: RwLock>, +} + +/// Stores the validator information in an epoch. +/// Block producers are specified by `block_producers` +/// Chunk producers have two types, validators who are also block producers and chunk only producers. +/// Block producers are assigned to shards via `validator_groups`. +/// Each shard will have `block_producers.len() / validator_groups` of validators who are also block +/// producers +struct EpochValidatorSet { + block_producers: Vec, + /// index of this list is shard_id + chunk_producers: Vec>, +} + +#[derive(BorshSerialize, BorshDeserialize, Hash, PartialEq, Eq, Ord, PartialOrd, Clone, Debug)] +struct AccountNonce(AccountId, Nonce); + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +struct KVState { + amounts: HashMap, + receipt_nonces: HashSet, + tx_nonces: HashSet, +} + +impl KeyValueRuntime { + pub fn new(store: Store, epoch_length: u64) -> Self { + let vs = + ValidatorSchedule::new().block_producers_per_epoch(vec![vec!["test".parse().unwrap()]]); + Self::new_with_validators(store, vs, epoch_length) + } + + pub fn new_with_validators(store: Store, vs: ValidatorSchedule, epoch_length: u64) -> Self { + Self::new_with_validators_and_no_gc(store, vs, epoch_length, false) + } + + pub fn new_with_validators_and_no_gc( + store: Store, + vs: ValidatorSchedule, + epoch_length: u64, + no_gc: bool, + ) -> Self { + let tries = ShardTries::test(store.clone(), vs.num_shards); + let mut initial_amounts = HashMap::new(); + for (i, validator) in vs.block_producers.iter().flatten().enumerate() { + initial_amounts.insert(validator.clone(), (1000 + 100 * i) as u128); + } + + let map_with_default_hash1 = HashMap::from([(CryptoHash::default(), EpochId::default())]); + let map_with_default_hash2 = HashMap::from([(CryptoHash::default(), 0)]); + let map_with_default_hash3 = HashMap::from([(EpochId::default(), 0)]); + + let kv_state = KVState { + amounts: initial_amounts, + receipt_nonces: HashSet::default(), + tx_nonces: HashSet::default(), + }; + let data = kv_state.try_to_vec().unwrap(); + let data_len = data.len() as u64; + // StateRoot is actually faked here. + // We cannot do any reasonable validations of it in test_utils. + let state = HashMap::from([(Trie::EMPTY_ROOT, kv_state)]); + let state_size = HashMap::from([(Trie::EMPTY_ROOT, data_len)]); + + let mut validators = HashMap::new(); + #[allow(unused_mut)] + let mut validators_by_valset: Vec = vs + .block_producers + .iter() + .map(|account_ids| { + let block_producers: Vec = account_ids + .iter() + .map(|account_id| { + let stake = ValidatorStake::new( + account_id.clone(), + SecretKey::from_seed(KeyType::ED25519, account_id.as_ref()) + .public_key(), + 1_000_000, + ); + validators.insert(account_id.clone(), stake.clone()); + stake + }) + .collect(); + + let validators_per_shard = block_producers.len() as ShardId / vs.validator_groups; + let coef = block_producers.len() as ShardId / vs.num_shards; + + let chunk_producers: Vec> = (0..vs.num_shards) + .map(|shard_id| { + let offset = (shard_id * coef / validators_per_shard * validators_per_shard) + as usize; + block_producers[offset..offset + validators_per_shard as usize].to_vec() + }) + .collect(); + + EpochValidatorSet { block_producers, chunk_producers } + }) + .collect(); + + if !vs.chunk_only_producers.is_empty() { + assert_eq!(validators_by_valset.len(), vs.chunk_only_producers.len()); + for (epoch_idx, epoch_cops) in vs.chunk_only_producers.into_iter().enumerate() { + for (shard_idx, shard_cops) in epoch_cops.into_iter().enumerate() { + for account_id in shard_cops { + let stake = ValidatorStake::new( + account_id.clone(), + SecretKey::from_seed(KeyType::ED25519, account_id.as_ref()) + .public_key(), + 1_000_000, + ); + let prev = validators.insert(account_id, stake.clone()); + assert!(prev.is_none(), "chunk only produced is also a block producer"); + validators_by_valset[epoch_idx].chunk_producers[shard_idx].push(stake) + } + } + } + } + + KeyValueRuntime { + store, + tries, + validators, + validators_by_valset, + num_shards: vs.num_shards, + tracks_all_shards: false, + epoch_length, + state: RwLock::new(state), + state_size: RwLock::new(state_size), + headers_cache: RwLock::new(HashMap::new()), + hash_to_epoch: RwLock::new(HashMap::new()), + hash_to_next_epoch_approvals_req: RwLock::new(HashMap::new()), + hash_to_next_epoch: RwLock::new(map_with_default_hash1), + hash_to_valset: RwLock::new(map_with_default_hash3), + epoch_start: RwLock::new(map_with_default_hash2), + no_gc, + } + } + + pub fn set_tracks_all_shards(&mut self, tracks_all_shards: bool) { + self.tracks_all_shards = tracks_all_shards; + } + + fn get_block_header(&self, hash: &CryptoHash) -> Result, Error> { + let mut headers_cache = self.headers_cache.write().unwrap(); + if headers_cache.get(hash).is_some() { + return Ok(Some(headers_cache.get(hash).unwrap().clone())); + } + if let Some(result) = self.store.get_ser(DBCol::BlockHeader, hash.as_ref())? { + headers_cache.insert(*hash, result); + return Ok(Some(headers_cache.get(hash).unwrap().clone())); + } + Ok(None) + } + + fn get_epoch_and_valset( + &self, + prev_hash: CryptoHash, + ) -> Result<(EpochId, usize, EpochId), Error> { + if prev_hash == CryptoHash::default() { + return Ok((EpochId(prev_hash), 0, EpochId(prev_hash))); + } + let prev_block_header = self + .get_block_header(&prev_hash)? + .ok_or_else(|| Error::DBNotFoundErr(prev_hash.to_string()))?; + + let mut hash_to_epoch = self.hash_to_epoch.write().unwrap(); + let mut hash_to_next_epoch_approvals_req = + self.hash_to_next_epoch_approvals_req.write().unwrap(); + let mut hash_to_next_epoch = self.hash_to_next_epoch.write().unwrap(); + let mut hash_to_valset = self.hash_to_valset.write().unwrap(); + let mut epoch_start_map = self.epoch_start.write().unwrap(); + + let prev_prev_hash = *prev_block_header.prev_hash(); + let prev_epoch = hash_to_epoch.get(&prev_prev_hash); + let prev_next_epoch = hash_to_next_epoch.get(&prev_prev_hash).unwrap(); + let prev_valset = match prev_epoch { + Some(prev_epoch) => Some(*hash_to_valset.get(prev_epoch).unwrap()), + None => None, + }; + + let prev_epoch_start = *epoch_start_map.get(&prev_prev_hash).unwrap(); + + let last_final_height = if prev_block_header.last_final_block() == &CryptoHash::default() { + 0 + } else { + self.get_block_header(prev_block_header.last_final_block()).unwrap().unwrap().height() + }; + + let increment_epoch = prev_prev_hash == CryptoHash::default() // genesis is in its own epoch + || last_final_height + 3 >= prev_epoch_start + self.epoch_length; + + let needs_next_epoch_approvals = !increment_epoch + && last_final_height + 3 < prev_epoch_start + self.epoch_length + && prev_block_header.height() + 3 >= prev_epoch_start + self.epoch_length; + + let (epoch, next_epoch, valset, epoch_start) = if increment_epoch { + let new_valset = match prev_valset { + None => 0, + Some(prev_valset) => prev_valset + 1, + }; + ( + prev_next_epoch.clone(), + EpochId(prev_hash), + new_valset, + prev_block_header.height() + 1, + ) + } else { + ( + prev_epoch.unwrap().clone(), + prev_next_epoch.clone(), + prev_valset.unwrap(), + prev_epoch_start, + ) + }; + + hash_to_next_epoch.insert(prev_hash, next_epoch.clone()); + hash_to_epoch.insert(prev_hash, epoch.clone()); + hash_to_next_epoch_approvals_req.insert(prev_hash, needs_next_epoch_approvals); + hash_to_valset.insert(epoch.clone(), valset); + hash_to_valset.insert(next_epoch.clone(), valset + 1); + epoch_start_map.insert(prev_hash, epoch_start); + + Ok((epoch, valset as usize % self.validators_by_valset.len(), next_epoch)) + } + + fn get_block_producers(&self, valset: usize) -> &[ValidatorStake] { + &self.validators_by_valset[valset].block_producers + } + + fn get_chunk_producers(&self, valset: usize, shard_id: ShardId) -> Vec { + self.validators_by_valset[valset].chunk_producers[shard_id as usize].clone() + } + + fn get_valset_for_epoch(&self, epoch_id: &EpochId) -> Result { + // conveniently here if the prev_hash is passed mistakenly instead of the epoch_hash, + // the `unwrap` will trigger + Ok(*self + .hash_to_valset + .read() + .unwrap() + .get(epoch_id) + .ok_or_else(|| Error::EpochOutOfBounds(epoch_id.clone()))? as usize + % self.validators_by_valset.len()) + } + + pub fn get_chunk_only_producers_for_shard( + &self, + epoch_id: &EpochId, + shard_id: ShardId, + ) -> Result, Error> { + let valset = self.get_valset_for_epoch(epoch_id)?; + let block_producers = &self.validators_by_valset[valset].block_producers; + let chunk_producers = &self.validators_by_valset[valset].chunk_producers[shard_id as usize]; + Ok(chunk_producers.iter().filter(|it| !block_producers.contains(it)).collect()) + } +} + +pub fn account_id_to_shard_id(account_id: &AccountId, num_shards: NumShards) -> ShardId { + let shard_layout = ShardLayout::v0(num_shards, 0); + shard_layout::account_id_to_shard_id(account_id, &shard_layout) +} + +#[derive(BorshSerialize, BorshDeserialize)] +struct ReceiptNonce { + from: AccountId, + to: AccountId, + amount: Balance, + nonce: Nonce, +} + +fn create_receipt_nonce( + from: AccountId, + to: AccountId, + amount: Balance, + nonce: Nonce, +) -> CryptoHash { + CryptoHash::hash_borsh(ReceiptNonce { from, to, amount, nonce }) +} + +impl EpochManagerAdapter for KeyValueRuntime { + fn epoch_exists(&self, epoch_id: &EpochId) -> bool { + self.hash_to_valset.write().unwrap().contains_key(epoch_id) + } + + fn num_shards(&self, _epoch_id: &EpochId) -> Result { + Ok(self.num_shards) + } + + fn num_total_parts(&self) -> usize { + 12 + (self.num_shards as usize + 1) % 50 + } + + fn num_data_parts(&self) -> usize { + // Same as in Nightshade Runtime + let total_parts = self.num_total_parts(); + if total_parts <= 3 { + 1 + } else { + (total_parts - 1) / 3 + } + } + + fn get_part_owner(&self, epoch_id: &EpochId, part_id: u64) -> Result { + let validators = + &self.get_epoch_block_producers_ordered(epoch_id, &CryptoHash::default())?; + // if we don't use data_parts and total_parts as part of the formula here, the part owner + // would not depend on height, and tests wouldn't catch passing wrong height here + let idx = part_id as usize + self.num_data_parts() + self.num_total_parts(); + Ok(validators[idx as usize % validators.len()].0.account_id().clone()) + } + + fn account_id_to_shard_id( + &self, + account_id: &AccountId, + _epoch_id: &EpochId, + ) -> Result { + Ok(account_id_to_shard_id(account_id, self.num_shards)) + } + + fn shard_id_to_uid(&self, shard_id: ShardId, _epoch_id: &EpochId) -> Result { + Ok(ShardUId { version: 0, shard_id: shard_id as u32 }) + } + + fn get_block_info(&self, _hash: &CryptoHash) -> Result, Error> { + Ok(Default::default()) + } + + fn get_epoch_config(&self, _epoch_id: &EpochId) -> Result { + Ok(EpochConfig { + epoch_length: 10, + num_block_producer_seats: 2, + num_block_producer_seats_per_shard: vec![1, 1], + avg_hidden_validator_seats_per_shard: vec![1, 1], + block_producer_kickout_threshold: 0, + chunk_producer_kickout_threshold: 0, + validator_max_kickout_stake_perc: 0, + online_min_threshold: Ratio::new(1i32, 4i32), + online_max_threshold: Ratio::new(3i32, 4i32), + fishermen_threshold: 1, + minimum_stake_divisor: 1, + protocol_upgrade_stake_threshold: Ratio::new(3i32, 4i32), + protocol_upgrade_num_epochs: 100, + shard_layout: ShardLayout::v1_test(), + validator_selection_config: ValidatorSelectionConfig::default(), + }) + } + + fn get_epoch_info(&self, _epoch_id: &EpochId) -> Result, Error> { + Ok(Arc::new(EpochInfo::v1_test())) + } + + fn get_shard_layout(&self, _epoch_id: &EpochId) -> Result { + Ok(ShardLayout::v0(self.num_shards, 0)) + } + + fn get_shard_config(&self, _epoch_id: &EpochId) -> Result { + panic!("get_shard_config not implemented for KeyValueRuntime"); + } + + fn is_next_block_epoch_start(&self, parent_hash: &CryptoHash) -> Result { + if parent_hash == &CryptoHash::default() { + return Ok(true); + } + let prev_block_header = self.get_block_header(parent_hash)?.ok_or_else(|| { + Error::Other(format!("Missing block {} when computing the epoch", parent_hash)) + })?; + let prev_prev_hash = *prev_block_header.prev_hash(); + Ok(self.get_epoch_and_valset(*parent_hash)?.0 + != self.get_epoch_and_valset(prev_prev_hash)?.0) + } + + fn get_epoch_id_from_prev_block(&self, parent_hash: &CryptoHash) -> Result { + Ok(self.get_epoch_and_valset(*parent_hash)?.0) + } + + fn get_epoch_height_from_prev_block( + &self, + _prev_block_hash: &CryptoHash, + ) -> Result { + Ok(0) + } + + fn get_next_epoch_id_from_prev_block( + &self, + parent_hash: &CryptoHash, + ) -> Result { + Ok(self.get_epoch_and_valset(*parent_hash)?.2) + } + + fn get_prev_shard_ids( + &self, + _prev_hash: &CryptoHash, + shard_ids: Vec, + ) -> Result, Error> { + Ok(shard_ids) + } + + fn get_shard_layout_from_prev_block( + &self, + _parent_hash: &CryptoHash, + ) -> Result { + Ok(ShardLayout::v0(self.num_shards, 0)) + } + + fn get_epoch_id(&self, block_hash: &CryptoHash) -> Result { + let (epoch_id, _, _) = self.get_epoch_and_valset(*block_hash)?; + Ok(epoch_id) + } + + fn compare_epoch_id( + &self, + epoch_id: &EpochId, + other_epoch_id: &EpochId, + ) -> Result { + if epoch_id.0 == other_epoch_id.0 { + return Ok(Ordering::Equal); + } + match (self.get_valset_for_epoch(epoch_id), self.get_valset_for_epoch(other_epoch_id)) { + (Ok(index1), Ok(index2)) => Ok(index1.cmp(&index2)), + _ => Err(Error::EpochOutOfBounds(epoch_id.clone())), + } + } + + fn get_epoch_start_height(&self, block_hash: &CryptoHash) -> Result { + let epoch_id = self.get_epoch_id(block_hash)?; + match self.get_block_header(&epoch_id.0)? { + Some(block_header) => Ok(block_header.height()), + None => Ok(0), + } + } + + fn get_prev_epoch_id_from_prev_block( + &self, + prev_block_hash: &CryptoHash, + ) -> Result { + let mut candidate_hash = *prev_block_hash; + loop { + let header = self + .get_block_header(&candidate_hash)? + .ok_or_else(|| Error::DBNotFoundErr(candidate_hash.to_string()))?; + candidate_hash = *header.prev_hash(); + if self.is_next_block_epoch_start(&candidate_hash)? { + break Ok(self.get_epoch_and_valset(candidate_hash)?.0); + } + } + } + + fn get_estimated_protocol_upgrade_block_height( + &self, + _block_hash: CryptoHash, + ) -> Result, EpochError> { + Ok(None) + } + + fn get_epoch_block_producers_ordered( + &self, + epoch_id: &EpochId, + _last_known_block_hash: &CryptoHash, + ) -> Result, Error> { + let validators = self.get_block_producers(self.get_valset_for_epoch(epoch_id)?); + Ok(validators.iter().map(|x| (x.clone(), false)).collect()) + } + + fn get_epoch_block_approvers_ordered( + &self, + parent_hash: &CryptoHash, + ) -> Result, Error> { + let (_cur_epoch, cur_valset, next_epoch) = self.get_epoch_and_valset(*parent_hash)?; + let mut validators = self + .get_block_producers(cur_valset) + .iter() + .map(|x| x.get_approval_stake(false)) + .collect::>(); + if *self.hash_to_next_epoch_approvals_req.write().unwrap().get(parent_hash).unwrap() { + let validators_copy = validators.clone(); + validators.extend( + self.get_block_producers(self.get_valset_for_epoch(&next_epoch)?) + .iter() + .filter(|x| { + !validators_copy.iter().any(|entry| &entry.account_id == x.account_id()) + }) + .map(|x| x.get_approval_stake(true)), + ); + } + let validators = validators.into_iter().map(|stake| (stake, false)).collect::>(); + Ok(validators) + } + + fn get_epoch_chunk_producers(&self, _epoch_id: &EpochId) -> Result, Error> { + tracing::warn!("not implemented, returning a dummy value"); + Ok(vec![]) + } + + fn get_block_producer( + &self, + epoch_id: &EpochId, + height: BlockHeight, + ) -> Result { + let validators = self.get_block_producers(self.get_valset_for_epoch(epoch_id)?); + Ok(validators[(height as usize) % validators.len()].account_id().clone()) + } + + fn get_chunk_producer( + &self, + epoch_id: &EpochId, + height: BlockHeight, + shard_id: ShardId, + ) -> Result { + let valset = self.get_valset_for_epoch(epoch_id)?; + let chunk_producers = self.get_chunk_producers(valset, shard_id); + let index = (shard_id + height + 1) as usize % chunk_producers.len(); + Ok(chunk_producers[index].account_id().clone()) + } + + fn get_validator_by_account_id( + &self, + epoch_id: &EpochId, + _last_known_block_hash: &CryptoHash, + account_id: &AccountId, + ) -> Result<(ValidatorStake, bool), Error> { + let validators = &self.validators_by_valset[self.get_valset_for_epoch(epoch_id)?]; + for validator_stake in validators.block_producers.iter() { + if validator_stake.account_id() == account_id { + return Ok((validator_stake.clone(), false)); + } + } + for validator_stake in validators.chunk_producers.iter().flatten() { + if validator_stake.account_id() == account_id { + return Ok((validator_stake.clone(), false)); + } + } + Err(Error::NotAValidator) + } + + fn get_fisherman_by_account_id( + &self, + _epoch_id: &EpochId, + _last_known_block_hash: &CryptoHash, + _account_id: &AccountId, + ) -> Result<(ValidatorStake, bool), Error> { + Err(Error::NotAValidator) + } + + fn get_validator_info( + &self, + _epoch_id: ValidatorInfoIdentifier, + ) -> Result { + Ok(EpochValidatorInfo { + current_validators: vec![], + next_validators: vec![], + current_fishermen: vec![], + next_fishermen: vec![], + current_proposals: vec![], + prev_epoch_kickout: vec![], + epoch_start_height: 0, + epoch_height: 1, + }) + } + + fn get_epoch_minted_amount(&self, _epoch_id: &EpochId) -> Result { + Ok(0) + } + + fn get_epoch_protocol_version(&self, _epoch_id: &EpochId) -> Result { + Ok(PROTOCOL_VERSION) + } + + fn get_epoch_sync_data( + &self, + _prev_epoch_last_block_hash: &CryptoHash, + _epoch_id: &EpochId, + _next_epoch_id: &EpochId, + ) -> Result< + ( + Arc, + Arc, + Arc, + Arc, + Arc, + Arc, + ), + Error, + > { + Ok(Default::default()) + } + + fn epoch_sync_init_epoch_manager( + &self, + _prev_epoch_first_block_info: BlockInfo, + _prev_epoch_last_block_info: BlockInfo, + _prev_epoch_prev_last_block_info: BlockInfo, + _prev_epoch_id: &EpochId, + _prev_epoch_info: EpochInfo, + _epoch_id: &EpochId, + _epoch_info: EpochInfo, + _next_epoch_id: &EpochId, + _next_epoch_info: EpochInfo, + ) -> Result<(), Error> { + Ok(()) + } + + fn verify_block_vrf( + &self, + _epoch_id: &EpochId, + _block_height: BlockHeight, + _prev_random_value: &CryptoHash, + _vrf_value: &near_crypto::vrf::Value, + _vrf_proof: &near_crypto::vrf::Proof, + ) -> Result<(), Error> { + Ok(()) + } + + fn verify_validator_signature( + &self, + _epoch_id: &EpochId, + _last_known_block_hash: &CryptoHash, + _account_id: &AccountId, + _data: &[u8], + _signature: &Signature, + ) -> Result { + Ok(true) + } + + fn verify_validator_or_fisherman_signature( + &self, + _epoch_id: &EpochId, + _last_known_block_hash: &CryptoHash, + _account_id: &AccountId, + _data: &[u8], + _signature: &Signature, + ) -> Result { + Ok(true) + } + + fn verify_header_signature(&self, header: &BlockHeader) -> Result { + let validator = self.get_block_producer(&header.epoch_id(), header.height())?; + let validator_stake = &self.validators[&validator]; + Ok(header.verify_block_producer(validator_stake.public_key())) + } + + fn verify_chunk_signature_with_header_parts( + &self, + _chunk_hash: &ChunkHash, + _signature: &Signature, + _epoch_id: &EpochId, + _last_kown_hash: &CryptoHash, + _height_created: BlockHeight, + _shard_id: ShardId, + ) -> Result { + Ok(true) + } + + fn verify_approval( + &self, + _prev_block_hash: &CryptoHash, + _prev_block_height: BlockHeight, + _block_height: BlockHeight, + _approvals: &[Option], + ) -> Result { + Ok(true) + } + + fn verify_approvals_and_threshold_orphan( + &self, + epoch_id: &EpochId, + can_approved_block_be_produced: &dyn Fn( + &[Option], + &[(Balance, Balance, bool)], + ) -> bool, + prev_block_hash: &CryptoHash, + prev_block_height: BlockHeight, + block_height: BlockHeight, + 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( + &if prev_block_height + 1 == block_height { + ApprovalInner::Endorsement(*prev_block_hash) + } else { + ApprovalInner::Skip(prev_block_height) + }, + block_height, + ); + + for (validator, may_be_signature) in validators.iter().zip(approvals.iter()) { + if let Some(signature) = may_be_signature { + if !signature.verify(message_to_sign.as_ref(), validator.public_key()) { + return Err(Error::InvalidApprovals); + } + } + } + let stakes = validators.iter().map(|stake| (stake.stake(), 0, false)).collect::>(); + if !can_approved_block_be_produced(approvals, &stakes) { + Err(Error::NotEnoughApprovals) + } else { + Ok(()) + } + } +} + +impl RuntimeAdapter for KeyValueRuntime { + fn genesis_state(&self) -> (Store, Vec) { + (self.store.clone(), ((0..self.num_shards).map(|_| Trie::EMPTY_ROOT).collect())) + } + + fn store(&self) -> &Store { + &self.store + } + + fn get_tries(&self) -> ShardTries { + self.tries.clone() + } + + fn get_trie_for_shard( + &self, + shard_id: ShardId, + _block_hash: &CryptoHash, + state_root: StateRoot, + _use_flat_storage: bool, + ) -> Result { + Ok(self + .tries + .get_trie_for_shard(ShardUId { version: 0, shard_id: shard_id as u32 }, state_root)) + } + + fn get_view_trie_for_shard( + &self, + shard_id: ShardId, + _block_hash: &CryptoHash, + state_root: StateRoot, + ) -> Result { + Ok(self.tries.get_view_trie_for_shard( + ShardUId { version: 0, shard_id: shard_id as u32 }, + state_root, + )) + } + + fn get_flat_storage_state_for_shard(&self, _shard_id: ShardId) -> Option { + None + } + + fn try_create_flat_storage_state_for_shard( + &self, + _shard_id: ShardId, + _latest_block_height: BlockHeight, + _chain_access: &dyn ChainAccessForFlatStorage, + ) -> FlatStorageStateStatus { + FlatStorageStateStatus::DontCreate + } + + fn set_flat_storage_state_for_genesis( + &self, + _genesis_block: &CryptoHash, + _genesis_epoch_id: &EpochId, + ) -> Result { + Ok(self.store.store_update()) + } + + fn cares_about_shard( + &self, + account_id: Option<&AccountId>, + parent_hash: &CryptoHash, + shard_id: ShardId, + _is_me: bool, + ) -> bool { + if self.tracks_all_shards { + return true; + } + // This `unwrap` here tests that in all code paths we check that the epoch exists before + // we check if we care about a shard. Please do not remove the unwrap, fix the logic of + // the calling function. + let epoch_valset = self.get_epoch_and_valset(*parent_hash).unwrap(); + let chunk_producers = self.get_chunk_producers(epoch_valset.1, shard_id); + if let Some(account_id) = account_id { + for validator in chunk_producers { + if validator.account_id() == account_id { + return true; + } + } + } + false + } + + fn will_care_about_shard( + &self, + account_id: Option<&AccountId>, + parent_hash: &CryptoHash, + shard_id: ShardId, + _is_me: bool, + ) -> bool { + if self.tracks_all_shards { + return true; + } + // This `unwrap` here tests that in all code paths we check that the epoch exists before + // we check if we care about a shard. Please do not remove the unwrap, fix the logic of + // the calling function. + let epoch_valset = self.get_epoch_and_valset(*parent_hash).unwrap(); + let chunk_producers = self + .get_chunk_producers((epoch_valset.1 + 1) % self.validators_by_valset.len(), shard_id); + if let Some(account_id) = account_id { + for validator in chunk_producers { + if validator.account_id() == account_id { + return true; + } + } + } + false + } + + fn validate_tx( + &self, + _gas_price: Balance, + _state_update: Option, + _transaction: &SignedTransaction, + _verify_signature: bool, + _epoch_id: &EpochId, + _current_protocol_version: ProtocolVersion, + ) -> Result, Error> { + Ok(None) + } + + fn prepare_transactions( + &self, + _gas_price: Balance, + _gas_limit: Gas, + _epoch_id: &EpochId, + _shard_id: ShardId, + _state_root: StateRoot, + _next_block_height: BlockHeight, + transactions: &mut dyn PoolIterator, + _chain_validate: &mut dyn FnMut(&SignedTransaction) -> bool, + _current_protocol_version: ProtocolVersion, + ) -> Result, Error> { + let mut res = vec![]; + while let Some(iter) = transactions.next() { + res.push(iter.next().unwrap()); + } + Ok(res) + } + + fn add_validator_proposals( + &self, + _block_header_info: BlockHeaderInfo, + ) -> Result { + Ok(self.store.store_update()) + } + + fn apply_transactions_with_optional_storage_proof( + &self, + shard_id: ShardId, + state_root: &StateRoot, + _height: BlockHeight, + _block_timestamp: u64, + _prev_block_hash: &CryptoHash, + block_hash: &CryptoHash, + receipts: &[Receipt], + transactions: &[SignedTransaction], + _last_validator_proposals: ValidatorStakeIter, + gas_price: Balance, + _gas_limit: Gas, + _challenges: &ChallengesResult, + _random_seed: CryptoHash, + generate_storage_proof: bool, + _is_new_chunk: bool, + _is_first_block_with_chunk_of_version: bool, + _state_patch: SandboxStatePatch, + _use_flat_storage: bool, + ) -> Result { + assert!(!generate_storage_proof); + let mut tx_results = vec![]; + + let mut state = self.state.read().unwrap().get(state_root).cloned().unwrap(); + + let mut balance_transfers = vec![]; + + for receipt in receipts.iter() { + if let ReceiptEnum::Action(action) = &receipt.receipt { + assert_eq!( + self.account_id_to_shard_id(&receipt.receiver_id, &EpochId::default())?, + shard_id + ); + if !state.receipt_nonces.contains(&receipt.receipt_id) { + state.receipt_nonces.insert(receipt.receipt_id); + if let Action::Transfer(TransferAction { deposit }) = action.actions[0] { + balance_transfers.push(( + receipt.get_hash(), + receipt.predecessor_id.clone(), + receipt.receiver_id.clone(), + deposit, + 0, + )); + } + } else { + panic!("receipts should never be applied twice"); + } + } else { + unreachable!(); + } + } + + for transaction in transactions { + assert_eq!( + self.account_id_to_shard_id( + &transaction.transaction.signer_id, + &EpochId::default() + )?, + shard_id + ); + if transaction.transaction.actions.is_empty() { + continue; + } + if let Action::Transfer(TransferAction { deposit }) = transaction.transaction.actions[0] + { + if !state.tx_nonces.contains(&AccountNonce( + transaction.transaction.receiver_id.clone(), + transaction.transaction.nonce, + )) { + state.tx_nonces.insert(AccountNonce( + transaction.transaction.receiver_id.clone(), + transaction.transaction.nonce, + )); + balance_transfers.push(( + transaction.get_hash(), + transaction.transaction.signer_id.clone(), + transaction.transaction.receiver_id.clone(), + deposit, + transaction.transaction.nonce, + )); + } else { + balance_transfers.push(( + transaction.get_hash(), + transaction.transaction.signer_id.clone(), + transaction.transaction.receiver_id.clone(), + 0, + transaction.transaction.nonce, + )); + } + } else { + unreachable!(); + } + } + + let mut outgoing_receipts = vec![]; + + for (hash, from, to, amount, nonce) in balance_transfers { + let mut good_to_go = false; + + if self.account_id_to_shard_id(&from, &EpochId::default())? != shard_id { + // This is a receipt, was already debited + good_to_go = true; + } else if let Some(balance) = state.amounts.get(&from) { + if *balance >= amount { + let new_balance = balance - amount; + state.amounts.insert(from.clone(), new_balance); + good_to_go = true; + } + } + + if good_to_go { + let new_receipt_hashes = if self.account_id_to_shard_id(&to, &EpochId::default())? + == shard_id + { + state.amounts.insert(to.clone(), state.amounts.get(&to).unwrap_or(&0) + amount); + vec![] + } else { + assert_ne!(nonce, 0); + let receipt = Receipt { + predecessor_id: from.clone(), + receiver_id: to.clone(), + receipt_id: create_receipt_nonce(from.clone(), to.clone(), amount, nonce), + receipt: ReceiptEnum::Action(ActionReceipt { + signer_id: from.clone(), + signer_public_key: PublicKey::empty(KeyType::ED25519), + gas_price, + output_data_receivers: vec![], + input_data_ids: vec![], + actions: vec![Action::Transfer(TransferAction { deposit: amount })], + }), + }; + let receipt_hash = receipt.get_hash(); + outgoing_receipts.push(receipt); + vec![receipt_hash] + }; + + tx_results.push(ExecutionOutcomeWithId { + id: hash, + outcome: ExecutionOutcome { + status: ExecutionStatus::SuccessValue(vec![]), + logs: vec![], + receipt_ids: new_receipt_hashes, + gas_burnt: 0, + tokens_burnt: 0, + executor_id: to.clone(), + metadata: ExecutionMetadata::V1, + }, + }); + } + } + + let data = state.try_to_vec()?; + let state_size = data.len() as u64; + let state_root = hash(&data); + self.state.write().unwrap().insert(state_root, state); + self.state_size.write().unwrap().insert(state_root, state_size); + + Ok(ApplyTransactionResult { + trie_changes: WrappedTrieChanges::new( + self.get_tries(), + ShardUId { version: 0, shard_id: shard_id as u32 }, + TrieChanges::empty(state_root), + Default::default(), + *block_hash, + ), + new_root: state_root, + outcomes: tx_results, + outgoing_receipts, + validator_proposals: vec![], + total_gas_burnt: 0, + total_balance_burnt: 0, + proof: None, + processed_delayed_receipts: vec![], + }) + } + + fn check_state_transition( + &self, + _partial_storage: PartialStorage, + _shard_id: ShardId, + _state_root: &StateRoot, + _height: BlockHeight, + _block_timestamp: u64, + _prev_block_hash: &CryptoHash, + _block_hash: &CryptoHash, + _receipts: &[Receipt], + _transactions: &[SignedTransaction], + _last_validator_proposals: ValidatorStakeIter, + _gas_price: Balance, + _gas_limit: Gas, + _challenges: &ChallengesResult, + _random_value: CryptoHash, + _is_new_chunk: bool, + _is_first_block_with_chunk_of_version: bool, + ) -> Result { + unimplemented!(); + } + + fn query( + &self, + _shard_id: ShardUId, + state_root: &StateRoot, + block_height: BlockHeight, + _block_timestamp: u64, + _prev_block_hash: &CryptoHash, + block_hash: &CryptoHash, + _epoch_id: &EpochId, + request: &QueryRequest, + ) -> Result { + match request { + QueryRequest::ViewAccount { account_id, .. } => Ok(QueryResponse { + kind: QueryResponseKind::ViewAccount( + Account::new( + self.state.read().unwrap().get(state_root).map_or_else( + || 0, + |state| *state.amounts.get(account_id).unwrap_or(&0), + ), + 0, + CryptoHash::default(), + 0, + ) + .into(), + ), + block_height, + block_hash: *block_hash, + }), + QueryRequest::ViewCode { .. } => Ok(QueryResponse { + kind: QueryResponseKind::ViewCode(ContractCodeView { + code: vec![], + hash: CryptoHash::default(), + }), + block_height, + block_hash: *block_hash, + }), + QueryRequest::ViewAccessKeyList { .. } => Ok(QueryResponse { + kind: QueryResponseKind::AccessKeyList(AccessKeyList { + keys: vec![AccessKeyInfoView { + public_key: PublicKey::empty(KeyType::ED25519), + access_key: AccessKey::full_access().into(), + }], + }), + block_height, + block_hash: *block_hash, + }), + QueryRequest::ViewAccessKey { .. } => Ok(QueryResponse { + kind: QueryResponseKind::AccessKey(AccessKey::full_access().into()), + block_height, + block_hash: *block_hash, + }), + QueryRequest::ViewState { .. } => Ok(QueryResponse { + kind: QueryResponseKind::ViewState(ViewStateResult { + values: Default::default(), + proof: vec![], + }), + block_height, + block_hash: *block_hash, + }), + QueryRequest::CallFunction { .. } => Ok(QueryResponse { + kind: QueryResponseKind::CallResult(CallResult { + result: Default::default(), + logs: Default::default(), + }), + block_height, + block_hash: *block_hash, + }), + } + } + + fn obtain_state_part( + &self, + _shard_id: ShardId, + _block_hash: &CryptoHash, + state_root: &StateRoot, + part_id: PartId, + ) -> Result, Error> { + if part_id.idx != 0 { + return Ok(vec![]); + } + let state = self.state.read().unwrap().get(state_root).unwrap().clone(); + let data = state.try_to_vec().expect("should never fall"); + Ok(data) + } + + fn validate_state_part(&self, _state_root: &StateRoot, _part_id: PartId, _data: &[u8]) -> bool { + // We do not care about deeper validation in test_utils + true + } + + fn apply_state_part( + &self, + _shard_id: ShardId, + state_root: &StateRoot, + part_id: PartId, + data: &[u8], + _epoch_id: &EpochId, + ) -> Result<(), Error> { + if part_id.idx != 0 { + return Ok(()); + } + let state = KVState::try_from_slice(data).unwrap(); + self.state.write().unwrap().insert(*state_root, state.clone()); + let data = state.try_to_vec()?; + let state_size = data.len() as u64; + self.state_size.write().unwrap().insert(*state_root, state_size); + Ok(()) + } + + fn get_state_root_node( + &self, + _shard_id: ShardId, + _block_hash: &CryptoHash, + state_root: &StateRoot, + ) -> Result { + let data = self + .state + .read() + .unwrap() + .get(state_root) + .unwrap() + .clone() + .try_to_vec() + .expect("should never fall") + .into(); + let memory_usage = *self.state_size.read().unwrap().get(state_root).unwrap(); + Ok(StateRootNode { data, memory_usage }) + } + + fn validate_state_root_node( + &self, + _state_root_node: &StateRootNode, + _state_root: &StateRoot, + ) -> bool { + // We do not care about deeper validation in test_utils + true + } + + fn get_gc_stop_height(&self, block_hash: &CryptoHash) -> BlockHeight { + if !self.no_gc { + // This code is 'incorrect' - as production one is always setting the GC to the + // first block of the epoch. + // Unfortunately many tests are depending on this and not setting epochs when + // they produce blocks. + let block_height = self + .get_block_header(block_hash) + .unwrap_or_default() + .map(|h| h.height()) + .unwrap_or_default(); + block_height.saturating_sub(DEFAULT_GC_NUM_EPOCHS_TO_KEEP * self.epoch_length) + /* // TODO: use this version of the code instead - after we fix the block creation + // issue in multiple tests. + // We have to return the first block of the epoch T-DEFAULT_GC_NUM_EPOCHS_TO_KEEP. + let mut current_header = self.get_block_header(block_hash).unwrap().unwrap(); + for _ in 0..DEFAULT_GC_NUM_EPOCHS_TO_KEEP { + let last_block_of_prev_epoch = current_header.next_epoch_id(); + current_header = + self.get_block_header(&last_block_of_prev_epoch.0).unwrap().unwrap(); + } + loop { + if current_header.next_epoch_id().0 == *current_header.prev_hash() { + break; + } + current_header = + self.get_block_header(current_header.prev_hash()).unwrap().unwrap(); + } + current_header.height()*/ + } else { + 0 + } + } + + fn chunk_needs_to_be_fetched_from_archival( + &self, + _chunk_prev_block_hash: &CryptoHash, + _header_head: &CryptoHash, + ) -> Result { + Ok(false) + } + + fn get_protocol_config(&self, _epoch_id: &EpochId) -> Result { + unreachable!("get_protocol_config should not be called in KeyValueRuntime"); + } + + fn will_shard_layout_change_next_epoch( + &self, + _parent_hash: &CryptoHash, + ) -> Result { + Ok(false) + } + + fn apply_update_to_split_states( + &self, + _block_hash: &CryptoHash, + _state_roots: HashMap, + _next_shard_layout: &ShardLayout, + _state_changes: StateChangesForSplitStates, + ) -> Result, Error> { + Ok(vec![]) + } + + fn build_state_for_split_shards( + &self, + _shard_uid: ShardUId, + _state_root: &StateRoot, + _next_epoch_shard_layout: &ShardLayout, + _state_split_status: Arc, + ) -> Result, Error> { + Ok(HashMap::new()) + } +} From fc862f60ef28c484f311ba99d51629d3649ce3ec Mon Sep 17 00:00:00 2001 From: mm-near <91919554+mm-near@users.noreply.github.com> Date: Tue, 6 Dec 2022 15:13:47 +0100 Subject: [PATCH 067/188] [Test utils] Added TestBlockBuilder (#8163) Created TestBlockBuilder to make it easier to create blocks in tests. It allows you to overwrite just the fields that you need. For example to create a block with custom approvals: ```rust TestBlockBuilder::new(&b1, signer.clone()).approvals(approvals).build(); ``` the PR contains just a couple changed tests - if it gets approved, I'll go ahead and change all the other tests to use this pattern too. --- chain/chain/src/tests/gc.rs | 15 ++--- chain/chain/src/tests/simple_chain.rs | 7 +- chain/chain/src/tests/sync_chain.rs | 12 ++-- chain/chain/src/types.rs | 21 +++--- core/primitives/src/test_utils.rs | 93 +++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 31 deletions(-) diff --git a/chain/chain/src/tests/gc.rs b/chain/chain/src/tests/gc.rs index cdddacec50c..2b1adea914f 100644 --- a/chain/chain/src/tests/gc.rs +++ b/chain/chain/src/tests/gc.rs @@ -10,6 +10,7 @@ use near_crypto::KeyType; use near_primitives::block::Block; use near_primitives::merkle::PartialMerkleTree; use near_primitives::shard_layout::ShardUId; +use near_primitives::test_utils::TestBlockBuilder; use near_primitives::types::{NumBlocks, NumShards, StateRoot}; use near_primitives::validator_signer::InMemoryValidatorSigner; use near_store::test_utils::{create_test_store, gen_changes}; @@ -90,15 +91,11 @@ fn do_fork( &prev_hash, ) .unwrap(); - Block::empty_with_epoch( - &prev_block, - prev_block.header().height() + 1, - epoch_id, - next_epoch_id, - next_bp_hash, - &*signer, - &mut PartialMerkleTree::default(), - ) + TestBlockBuilder::new(&prev_block, signer.clone()) + .epoch_id(epoch_id) + .next_epoch_id(next_epoch_id) + .next_bp_hash(next_bp_hash) + .build() }; if verbose { diff --git a/chain/chain/src/tests/simple_chain.rs b/chain/chain/src/tests/simple_chain.rs index b2008c7b301..25d3110e8e6 100644 --- a/chain/chain/src/tests/simple_chain.rs +++ b/chain/chain/src/tests/simple_chain.rs @@ -6,6 +6,7 @@ use chrono; use chrono::TimeZone; use near_o11y::testonly::init_test_logger; use near_primitives::hash::CryptoHash; +use near_primitives::test_utils::TestBlockBuilder; use near_primitives::time::MockClockGuard; use near_primitives::version::PROTOCOL_VERSION; use num_rational::Ratio; @@ -141,9 +142,9 @@ fn build_chain_with_skips_and_forks() { init_test_logger(); let (mut chain, _, signer) = setup(); let genesis = chain.get_block(&chain.genesis().hash().clone()).unwrap(); - let b1 = Block::empty(&genesis, &*signer); - let b2 = Block::empty_with_height(&genesis, 2, &*signer); - let b3 = Block::empty_with_height(&b1, 3, &*signer); + let b1 = TestBlockBuilder::new(&genesis, signer.clone()).build(); + let b2 = TestBlockBuilder::new(&genesis, signer.clone()).height(2).build(); + let b3 = TestBlockBuilder::new(&b1, signer.clone()).height(3).build(); let b4 = Block::empty_with_height(&b2, 4, &*signer); let b5 = Block::empty(&b4, &*signer); let b6 = Block::empty(&b5, &*signer); diff --git a/chain/chain/src/tests/sync_chain.rs b/chain/chain/src/tests/sync_chain.rs index 6179501dacf..b6ac46d8122 100644 --- a/chain/chain/src/tests/sync_chain.rs +++ b/chain/chain/src/tests/sync_chain.rs @@ -1,7 +1,7 @@ use crate::test_utils::setup; -use crate::Block; use near_o11y::testonly::init_test_logger; use near_primitives::merkle::PartialMerkleTree; +use near_primitives::test_utils::TestBlockBuilder; #[test] fn chain_sync_headers() { @@ -11,11 +11,11 @@ fn chain_sync_headers() { let mut blocks = vec![chain.get_block(&chain.genesis().hash().clone()).unwrap()]; let mut block_merkle_tree = PartialMerkleTree::default(); for i in 0..4 { - blocks.push(Block::empty_with_block_merkle_tree( - &blocks[i], - &*bls_signer, - &mut block_merkle_tree, - )); + blocks.push( + TestBlockBuilder::new(&blocks[i], bls_signer.clone()) + .block_merkle_tree(&mut block_merkle_tree) + .build(), + ) } let mut challenges = vec![]; diff --git a/chain/chain/src/types.rs b/chain/chain/src/types.rs index df929e38a49..d786886e7ab 100644 --- a/chain/chain/src/types.rs +++ b/chain/chain/src/types.rs @@ -577,6 +577,7 @@ pub struct LatestKnown { #[cfg(test)] mod tests { + use near_primitives::test_utils::TestBlockBuilder; use near_primitives::time::Utc; use near_crypto::KeyType; @@ -604,9 +605,12 @@ mod tests { 1_000_000_000, CryptoHash::hash_borsh(genesis_bps), ); - let signer = - InMemoryValidatorSigner::from_seed("other".parse().unwrap(), KeyType::ED25519, "other"); - let b1 = Block::empty(&genesis, &signer); + let signer = Arc::new(InMemoryValidatorSigner::from_seed( + "other".parse().unwrap(), + KeyType::ED25519, + "other", + )); + let b1 = TestBlockBuilder::new(&genesis, signer.clone()).build(); assert!(b1.header().verify_block_producer(&signer.public_key())); let other_signer = InMemoryValidatorSigner::from_seed( "other2".parse().unwrap(), @@ -614,16 +618,7 @@ mod tests { "other2", ); let approvals = vec![Some(Approval::new(*b1.hash(), 1, 2, &other_signer).signature)]; - let b2 = Block::empty_with_approvals( - &b1, - 2, - b1.header().epoch_id().clone(), - EpochId(*genesis.hash()), - approvals, - &signer, - *genesis.header().next_bp_hash(), - CryptoHash::default(), - ); + let b2 = TestBlockBuilder::new(&b1, signer.clone()).approvals(approvals).build(); b2.header().verify_block_producer(&signer.public_key()); } diff --git a/core/primitives/src/test_utils.rs b/core/primitives/src/test_utils.rs index 7090277a078..302f997cd07 100644 --- a/core/primitives/src/test_utils.rs +++ b/core/primitives/src/test_utils.rs @@ -306,6 +306,99 @@ impl BlockHeader { } } +/// Builder class for blocks to make testing easier. +/// # Examples +/// +/// // TODO(mm-near): change it to doc-tested code once we have easy way to create a genesis block. +/// let signer = EmptyValidatorSigner::default(); +/// let test_block = test_utils::TestBlockBuilder::new(prev, signer).height(33).build(); +/// + +pub struct TestBlockBuilder { + prev: Block, + signer: Arc, + height: u64, + epoch_id: EpochId, + next_epoch_id: EpochId, + next_bp_hash: CryptoHash, + approvals: Vec>, + block_merkle_root: CryptoHash, +} + +impl TestBlockBuilder { + pub fn new(prev: &Block, signer: Arc) -> Self { + let mut tree = PartialMerkleTree::default(); + tree.insert(prev.hash().clone()); + + Self { + prev: prev.clone(), + signer: signer.clone(), + height: prev.header().height() + 1, + epoch_id: prev.header().epoch_id().clone(), + next_epoch_id: if prev.header().prev_hash() == &CryptoHash::default() { + EpochId(*prev.hash()) + } else { + prev.header().next_epoch_id().clone() + }, + next_bp_hash: *prev.header().next_bp_hash(), + approvals: vec![], + block_merkle_root: tree.root(), + } + } + pub fn height(mut self, height: u64) -> Self { + self.height = height; + self + } + pub fn epoch_id(mut self, epoch_id: EpochId) -> Self { + self.epoch_id = epoch_id; + self + } + pub fn next_epoch_id(mut self, next_epoch_id: EpochId) -> Self { + self.next_epoch_id = next_epoch_id; + self + } + pub fn next_bp_hash(mut self, next_bp_hash: CryptoHash) -> Self { + self.next_bp_hash = next_bp_hash; + self + } + pub fn approvals(mut self, approvals: Vec>) -> Self { + self.approvals = approvals; + self + } + + /// Updates the merkle tree by adding the previous hash, and updates the new block's merkle_root. + pub fn block_merkle_tree(mut self, block_merkle_tree: &mut PartialMerkleTree) -> Self { + block_merkle_tree.insert(self.prev.hash().clone()); + self.block_merkle_root = block_merkle_tree.root(); + self + } + + pub fn build(self) -> Block { + Block::produce( + PROTOCOL_VERSION, + PROTOCOL_VERSION, + self.prev.header(), + self.height, + self.prev.header().block_ordinal() + 1, + self.prev.chunks().iter().cloned().collect(), + self.epoch_id, + self.next_epoch_id, + None, + self.approvals, + Ratio::new(0, 1), + 0, + 0, + Some(0), + vec![], + vec![], + self.signer.as_ref(), + self.next_bp_hash, + self.block_merkle_root, + None, + ) + } +} + impl Block { pub fn mut_header(&mut self) -> &mut BlockHeader { match self { From 2bf5b031c5490a5887a69935a7a117a3c23da483 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Tue, 6 Dec 2022 15:54:03 +0100 Subject: [PATCH 068/188] added monitoring of TIER1 performance (#8170) Added a metric to check whether TIER1 or TIER2 message arrived first and what is the latency. Added TIER1 connections status to NetworkInfo and NetworkInfoView, which is served on debug/api/status endpoint. --- chain/client/src/client_actor.rs | 3 +- chain/client/src/debug.rs | 29 ++++++++++++++- chain/client/src/info.rs | 3 +- chain/client/src/test_utils.rs | 3 +- chain/network/src/peer/peer_actor.rs | 12 ++++-- .../src/peer_manager/network_state/mod.rs | 12 ++++++ .../src/peer_manager/peer_manager_actor.rs | 4 +- .../src/routing/routing_table_view/tests.rs | 2 +- chain/network/src/stats/metrics.rs | 37 +++++++++++++++---- chain/network/src/time.rs | 3 +- chain/network/src/types.rs | 10 +++-- core/primitives/src/views.rs | 16 ++++++++ .../src/tests/client/process_blocks.rs | 3 +- integration-tests/src/tests/network/runner.rs | 24 ++++++------ tools/chainsync-loadtest/src/network.rs | 3 +- tools/mock-node/src/lib.rs | 3 +- 16 files changed, 132 insertions(+), 35 deletions(-) diff --git a/chain/client/src/client_actor.rs b/chain/client/src/client_actor.rs index 2fb434c686f..06c20358f31 100644 --- a/chain/client/src/client_actor.rs +++ b/chain/client/src/client_actor.rs @@ -192,13 +192,14 @@ impl ClientActor { node_id, network_info: NetworkInfo { connected_peers: vec![], + tier1_connections: vec![], num_connected_peers: 0, peer_max_count: 0, highest_height_peers: vec![], received_bytes_per_sec: 0, sent_bytes_per_sec: 0, known_producers: vec![], - tier1_accounts: vec![], + tier1_accounts_data: vec![], }, last_validator_announce_time: None, info_helper, diff --git a/chain/client/src/debug.rs b/chain/client/src/debug.rs index cbf6aa47ed5..b98b4686d63 100644 --- a/chain/client/src/debug.rs +++ b/chain/client/src/debug.rs @@ -33,7 +33,9 @@ use near_client_primitives::debug::{DebugBlockStatus, DebugChunkStatus}; use near_network::types::{ConnectedPeerInfo, NetworkInfo, PeerType}; use near_primitives::sharding::ShardChunkHeader; use near_primitives::time::Clock; -use near_primitives::views::{KnownProducerView, NetworkInfoView, PeerInfoView}; +use near_primitives::views::{ + AccountDataView, KnownProducerView, NetworkInfoView, PeerInfoView, Tier1ProxyView, +}; // Constants for debug requests. const DEBUG_BLOCKS_TO_FETCH: u32 = 50; @@ -687,5 +689,30 @@ pub(crate) fn new_network_info_view(chain: &Chain, network_info: &NetworkInfo) - .map(|it| it.iter().map(|peer_id| peer_id.public_key().clone()).collect()), }) .collect(), + tier1_accounts_data: network_info + .tier1_accounts_data + .iter() + .map(|d| AccountDataView { + peer_id: d.peer_id.public_key().clone(), + proxies: d + .proxies + .iter() + .map(|p| Tier1ProxyView { + addr: p.addr, + peer_id: p.peer_id.public_key().clone(), + }) + .collect(), + account_key: d.account_key.clone(), + timestamp: chrono::DateTime::from_utc( + chrono::NaiveDateTime::from_timestamp(d.timestamp.unix_timestamp(), 0), + chrono::Utc, + ), + }) + .collect(), + tier1_connections: network_info + .tier1_connections + .iter() + .map(|full_peer_info| new_peer_info_view(chain, full_peer_info)) + .collect::>(), } } diff --git a/chain/client/src/info.rs b/chain/client/src/info.rs index 69bb8384e6c..48688379284 100644 --- a/chain/client/src/info.rs +++ b/chain/client/src/info.rs @@ -560,7 +560,8 @@ mod tests { sent_bytes_per_sec: 0, received_bytes_per_sec: 0, known_producers: vec![], - tier1_accounts: vec![], + tier1_connections: vec![], + tier1_accounts_data: vec![], }, &config, 0.0, diff --git a/chain/client/src/test_utils.rs b/chain/client/src/test_utils.rs index 974072a10a6..2776fbb6f2d 100644 --- a/chain/client/src/test_utils.rs +++ b/chain/client/src/test_utils.rs @@ -690,13 +690,14 @@ pub fn setup_mock_all_validators( .collect(); let info = NetworkInfo { connected_peers: peers, + tier1_connections: vec![], num_connected_peers: key_pairs1.len(), peer_max_count: key_pairs1.len() as u32, highest_height_peers: peers2, sent_bytes_per_sec: 0, received_bytes_per_sec: 0, known_producers: vec![], - tier1_accounts: vec![], + tier1_accounts_data: vec![], }; client_addr.do_send(SetNetworkInfo(info).with_span_context()); } diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index dea5d2c8407..04ead509189 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -1209,7 +1209,15 @@ impl PeerActor { msg.target); let for_me = self.network_state.message_for_me(&msg.target); if for_me { - metrics::record_routed_msg_metrics(&self.clock, &msg); + // Check if we have already received this message. + let fastest = self + .network_state + .recent_routed_messages + .lock() + .put(CryptoHash::hash_borsh(&msg.body), ()) + .is_none(); + // Register that the message has been received. + metrics::record_routed_msg_metrics(&self.clock, &msg, conn.tier, fastest); } // Drop duplicated messages routed within DROP_DUPLICATED_MESSAGES_PERIOD ms @@ -1447,8 +1455,6 @@ impl actix::Handler for PeerActor { msg_len = msg.len(), peer = %self.peer_info) .entered(); - // TODO(#5155) We should change our code to track size of messages received from Peer - // as long as it travels to PeerManager, etc. if self.closing_reason.is_some() { tracing::warn!(target: "network", "Received message from closing connection {:?}. Ignoring", self.peer_type); diff --git a/chain/network/src/peer_manager/network_state/mod.rs b/chain/network/src/peer_manager/network_state/mod.rs index 67a2cfd3d38..c4f75e7869e 100644 --- a/chain/network/src/peer_manager/network_state/mod.rs +++ b/chain/network/src/peer_manager/network_state/mod.rs @@ -39,6 +39,11 @@ pub(crate) const LIMIT_PENDING_PEERS: usize = 60; /// We send these messages multiple times to reduce the chance that they are lost const IMPORTANT_MESSAGE_RESENT_COUNT: usize = 3; +/// Size of LRU cache size of recent routed messages. +/// It should be large enough to detect duplicates (i.e. all messages received during +/// production of 1 block should fit). +const RECENT_ROUTED_MESSAGES_CACHE_SIZE: usize = 10000; + /// How long a peer has to be unreachable, until we prune it from the in-memory graph. const PRUNE_UNREACHABLE_PEERS_AFTER: time::Duration = time::Duration::hours(1); @@ -98,6 +103,10 @@ pub(crate) struct NetworkState { /// A graph of the whole NEAR network. pub graph: Arc, + /// Hashes of the body of recently received routed messages. + /// It allows us to determine whether messages arrived faster over TIER1 or TIER2 network. + pub recent_routed_messages: Mutex>, + /// Hash of messages that requires routing back to respective previous hop. /// Currently unused, as TIER1 messages do not require a response. /// Also TIER1 connections are direct by design (except for proxies), @@ -160,6 +169,9 @@ impl NetworkState { peer_store, accounts_data: Arc::new(accounts_data::Cache::new()), tier1_route_back: Mutex::new(RouteBackCache::default()), + recent_routed_messages: Mutex::new(lru::LruCache::new( + RECENT_ROUTED_MESSAGES_CACHE_SIZE, + )), txns_since_last_block: AtomicUsize::new(0), whitelist_nodes, max_num_peers: AtomicU32::new(config.max_num_peers), diff --git a/chain/network/src/peer_manager/peer_manager_actor.rs b/chain/network/src/peer_manager/peer_manager_actor.rs index 351705fa8be..86d8725a840 100644 --- a/chain/network/src/peer_manager/peer_manager_actor.rs +++ b/chain/network/src/peer_manager/peer_manager_actor.rs @@ -589,6 +589,7 @@ impl PeerManagerActor { } pub(crate) fn get_network_info(&self) -> NetworkInfo { + let tier1 = self.state.tier1.load(); let tier2 = self.state.tier2.load(); let now = self.clock.now(); let connected_peer = |cp: &Arc| ConnectedPeerInfo { @@ -603,6 +604,7 @@ impl PeerManagerActor { }; NetworkInfo { connected_peers: tier2.ready.values().map(connected_peer).collect(), + tier1_connections: tier1.ready.values().map(connected_peer).collect(), num_connected_peers: tier2.ready.len(), peer_max_count: self.state.max_num_peers.load(Ordering::Relaxed), highest_height_peers: self.highest_height_peers(), @@ -630,7 +632,7 @@ impl PeerManagerActor { next_hops: self.state.graph.routing_table.view_route(&announce_account.peer_id), }) .collect(), - tier1_accounts: self.state.accounts_data.load().data.values().cloned().collect(), + tier1_accounts_data: self.state.accounts_data.load().data.values().cloned().collect(), } } diff --git a/chain/network/src/routing/routing_table_view/tests.rs b/chain/network/src/routing/routing_table_view/tests.rs index f2db55bd5c8..4a125c1c7dc 100644 --- a/chain/network/src/routing/routing_table_view/tests.rs +++ b/chain/network/src/routing/routing_table_view/tests.rs @@ -1,10 +1,10 @@ use crate::network_protocol::testonly as data; +use crate::network_protocol::PeerIdOrHash; use crate::routing; use crate::routing::routing_table_view::*; use crate::test_utils::{random_epoch_id, random_peer_id}; use crate::testonly::make_rng; use crate::time; -use crate::types::PeerIdOrHash; use near_crypto::Signature; use near_primitives::network::AnnounceAccount; use rand::seq::SliceRandom; diff --git a/chain/network/src/stats/metrics.rs b/chain/network/src/stats/metrics.rs index c64a18d48dd..c54842b3764 100644 --- a/chain/network/src/stats/metrics.rs +++ b/chain/network/src/stats/metrics.rs @@ -1,5 +1,6 @@ use crate::network_protocol::Encoding; use crate::network_protocol::{RoutedMessageBody, RoutedMessageV2}; +use crate::tcp; use crate::time; use crate::types::PeerType; use near_o11y::metrics::prometheus; @@ -70,7 +71,7 @@ impl Labels for Connection { pub(crate) struct MetricGuard { metric: M, - drop: Option>, + drop: Option>, } impl MetricGuard { @@ -317,8 +318,8 @@ pub(crate) static BROADCAST_MESSAGES: Lazy = Lazy::new(|| { static NETWORK_ROUTED_MSG_LATENCY: Lazy = Lazy::new(|| { try_create_histogram_vec( "near_network_routed_msg_latency", - "Latency of network messages, assuming clocks are perfectly synchronized", - &["routed"], + "Latency of network messages, assuming clocks are perfectly synchronized. 'tier' indicates what is the tier of the connection on which the message arrived (TIER1 is expected to be faster than TIER2) and 'fastest' indicates whether this was the first copy of the message to arrive.", + &["routed","tier","fastest"], Some(exponential_buckets(0.0001, 1.6, 20).unwrap()), ) .unwrap() @@ -348,19 +349,41 @@ pub(crate) static ALREADY_CONNECTED_ACCOUNT: Lazy = Lazy::new(|| { .unwrap() }); -pub(crate) fn record_routed_msg_metrics(clock: &time::Clock, msg: &RoutedMessageV2) { - record_routed_msg_latency(clock, msg); +/// Updated the prometheus metrics about the received routed message `msg`. +/// `tier` indicates the network over which the message was transmitted. +/// `fastest` indicates whether this message is the first copy of `msg` received - +/// important messages are sent multiple times over different routing paths +/// simultaneously to improve the chance that the message will be delivered on time. +pub(crate) fn record_routed_msg_metrics( + clock: &time::Clock, + msg: &RoutedMessageV2, + tier: tcp::Tier, + fastest: bool, +) { + record_routed_msg_latency(clock, msg, tier, fastest); record_routed_msg_hops(msg); } // The routed message reached its destination. If the timestamp of creation of this message is // known, then update the corresponding latency metric histogram. -fn record_routed_msg_latency(clock: &time::Clock, msg: &RoutedMessageV2) { +fn record_routed_msg_latency( + clock: &time::Clock, + msg: &RoutedMessageV2, + tier: tcp::Tier, + fastest: bool, +) { if let Some(created_at) = msg.created_at { let now = clock.now_utc(); let duration = now - created_at; NETWORK_ROUTED_MSG_LATENCY - .with_label_values(&[msg.body_variant()]) + .with_label_values(&[ + msg.body_variant(), + tier.as_ref(), + match fastest { + true => "true", + false => "false", + }, + ]) .observe(duration.as_seconds_f64()); } } diff --git a/chain/network/src/time.rs b/chain/network/src/time.rs index 5c6a6a4110c..d5b79e38f07 100644 --- a/chain/network/src/time.rs +++ b/chain/network/src/time.rs @@ -149,7 +149,7 @@ pub struct FakeClock(Arc>); impl FakeClock { /// Constructor of a fake clock. Use it in tests. - /// It support manually moving time forward (via advance()). + /// It supports manually moving time forward (via advance()). /// You can also arbitrarly set the UTC time in runtime. /// Use FakeClock::clock() when calling prod code from tests. pub fn new(utc: Utc) -> Self { @@ -158,6 +158,7 @@ impl FakeClock { pub fn now(&self) -> Instant { self.0.lock().unwrap().now() } + pub fn now_utc(&self) -> Utc { self.0.lock().unwrap().now_utc() } diff --git a/chain/network/src/types.rs b/chain/network/src/types.rs index dd47517b116..9a10152a077 100644 --- a/chain/network/src/types.rs +++ b/chain/network/src/types.rs @@ -26,8 +26,8 @@ use std::sync::Arc; /// Exported types, which are part of network protocol. pub use crate::network_protocol::{ Edge, PartialEdgeInfo, PartialEncodedChunkForwardMsg, PartialEncodedChunkRequestMsg, - PartialEncodedChunkResponseMsg, PeerChainInfoV2, PeerIdOrHash, PeerInfo, Ping, Pong, - StateResponseInfo, StateResponseInfoV1, StateResponseInfoV2, + PartialEncodedChunkResponseMsg, PeerChainInfoV2, PeerInfo, Ping, Pong, StateResponseInfo, + StateResponseInfoV1, StateResponseInfoV2, }; /// Number of hops a message is allowed to travel before being dropped. @@ -355,6 +355,7 @@ pub struct ConnectedPeerInfo { #[derive(Debug, Clone, actix::MessageResponse)] pub struct NetworkInfo { + /// TIER2 connections. pub connected_peers: Vec, pub num_connected_peers: usize, pub peer_max_count: u32, @@ -363,7 +364,10 @@ pub struct NetworkInfo { pub received_bytes_per_sec: u64, /// Accounts of known block and chunk producers from routing table. pub known_producers: Vec, - pub tier1_accounts: Vec>, + /// Collected data about the current TIER1 accounts. + pub tier1_accounts_data: Vec>, + /// TIER1 connections. + pub tier1_connections: Vec, } #[derive(Debug, actix::MessageResponse, PartialEq, Eq)] diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index 4627ed71a4d..ef1bf9dc328 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -363,12 +363,28 @@ pub struct KnownProducerView { pub next_hops: Option>, } +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct Tier1ProxyView { + pub addr: std::net::SocketAddr, + pub peer_id: PublicKey, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct AccountDataView { + pub peer_id: PublicKey, + pub proxies: Vec, + pub account_key: PublicKey, + pub timestamp: DateTime, +} + #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct NetworkInfoView { pub peer_max_count: u32, pub num_connected_peers: usize, pub connected_peers: Vec, pub known_producers: Vec, + pub tier1_accounts_data: Vec, + pub tier1_connections: Vec, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] diff --git a/integration-tests/src/tests/client/process_blocks.rs b/integration-tests/src/tests/client/process_blocks.rs index 91415830ad0..74d525f961f 100644 --- a/integration-tests/src/tests/client/process_blocks.rs +++ b/integration-tests/src/tests/client/process_blocks.rs @@ -1059,7 +1059,8 @@ fn client_sync_headers() { sent_bytes_per_sec: 0, received_bytes_per_sec: 0, known_producers: vec![], - tier1_accounts: vec![], + tier1_connections: vec![], + tier1_accounts_data: vec![], }) .with_span_context(), ); diff --git a/integration-tests/src/tests/network/runner.rs b/integration-tests/src/tests/network/runner.rs index c23a01b7d8c..df0a54fea49 100644 --- a/integration-tests/src/tests/network/runner.rs +++ b/integration-tests/src/tests/network/runner.rs @@ -211,7 +211,7 @@ impl StateMachine { #[allow(unused_variables)] Action::SetOptions { target, max_num_peers } => { self.actions.push(Box::new(move |info:&mut RunningInfo| Box::pin(async move { - debug!(target: "network", num_prev_actions, action = ?action_clone, "runner.rs: Action"); + debug!(target: "test", num_prev_actions, action = ?action_clone, "runner.rs: Action"); info.get_node(target)?.actix.addr.send(PeerManagerMessageRequest::SetAdvOptions(near_network::test_utils::SetAdvOptions { set_max_peers: max_num_peers, }).with_span_context()).await?; @@ -220,7 +220,7 @@ impl StateMachine { } Action::AddEdge { from, to, force } => { self.actions.push(Box::new(move |info: &mut RunningInfo| Box::pin(async move { - debug!(target: "network", num_prev_actions, action = ?action_clone, "runner.rs: Action"); + debug!(target: "test", num_prev_actions, action = ?action_clone, "runner.rs: Action"); let pm = info.get_node(from)?.actix.addr.clone(); let peer_info = info.runner.test_config[to].peer_info(); match tcp::Stream::connect(&peer_info, tcp::Tier::T2).await { @@ -252,14 +252,14 @@ impl StateMachine { } Action::Stop(source) => { self.actions.push(Box::new(move |info: &mut RunningInfo| Box::pin(async move { - debug!(target: "network", num_prev_actions, action = ?action_clone, "runner.rs: Action"); + debug!(target: "test", num_prev_actions, action = ?action_clone, "runner.rs: Action"); info.stop_node(source); Ok(ControlFlow::Break(())) }))); } Action::Wait(t) => { self.actions.push(Box::new(move |_info: &mut RunningInfo| Box::pin(async move { - debug!(target: "network", num_prev_actions, action = ?action_clone, "runner.rs: Action"); + debug!(target: "test", num_prev_actions, action = ?action_clone, "runner.rs: Action"); tokio::time::sleep(t.try_into().unwrap()).await; Ok(ControlFlow::Break(())) }))); @@ -507,7 +507,7 @@ pub fn start_test(runner: Runner) -> anyhow::Result<()> { let step = tokio::time::Duration::from_millis(10); let start = tokio::time::Instant::now(); for (i, a) in actions.into_iter().enumerate() { - tracing::debug!("[starting action {i}]"); + tracing::debug!(target: "test", "[starting action {i}]"); loop { let done = tokio::time::timeout_at(start + timeout, a(&mut info)).await.with_context( @@ -578,7 +578,7 @@ pub fn check_expected_connections( ) -> ActionFn { Box::new(move |info: &mut RunningInfo| { Box::pin(async move { - debug!(target: "network", node_id, expected_connections_lo, ?expected_connections_hi, "runner.rs: check_expected_connections"); + debug!(target: "test", node_id, expected_connections_lo, ?expected_connections_hi, "runner.rs: check_expected_connections"); let pm = &info.get_node(node_id)?.actix.addr; let res = pm.send(GetInfo {}.with_span_context()).await?; if expected_connections_lo.map_or(false, |l| l > res.num_connected_peers) { @@ -598,7 +598,7 @@ async fn check_direct_connection_inner( target_id: usize, ) -> anyhow::Result { let target_peer_id = info.runner.test_config[target_id].peer_id(); - debug!(target: "network", node_id, ?target_id, "runner.rs: check_direct_connection"); + debug!(target: "test", node_id, ?target_id, "runner.rs: check_direct_connection"); let pm = &info.get_node(node_id)?.actix.addr; let rt = match pm.send(PeerManagerMessageRequest::FetchRoutingTable.with_span_context()).await? { @@ -608,12 +608,12 @@ async fn check_direct_connection_inner( let routes = if let Some(routes) = rt.next_hops.get(&target_peer_id) { routes } else { - debug!(target: "network", ?target_peer_id, node_id, target_id, + debug!(target: "test", ?target_peer_id, node_id, target_id, "runner.rs: check_direct_connection NO ROUTES!", ); return Ok(ControlFlow::Continue(())); }; - debug!(target: "network", ?target_peer_id, ?routes, node_id, target_id, + debug!(target: "test", ?target_peer_id, ?routes, node_id, target_id, "runner.rs: check_direct_connection", ); if !routes.contains(&target_peer_id) { @@ -631,7 +631,7 @@ pub fn check_direct_connection(node_id: usize, target_id: usize) -> ActionFn { pub fn restart(node_id: usize) -> ActionFn { Box::new(move |info: &mut RunningInfo| { Box::pin(async move { - debug!(target: "network", ?node_id, "runner.rs: restart"); + debug!(target: "test", ?node_id, "runner.rs: restart"); info.start_node(node_id).await?; Ok(ControlFlow::Break(())) }) @@ -643,7 +643,7 @@ async fn ban_peer_inner( target_peer: usize, banned_peer: usize, ) -> anyhow::Result { - debug!(target: "network", target_peer, banned_peer, "runner.rs: ban_peer"); + debug!(target: "test", target_peer, banned_peer, "runner.rs: ban_peer"); let banned_peer_id = info.runner.test_config[banned_peer].peer_id(); let pm = &info.get_node(target_peer)?.actix.addr; pm.send(BanPeerSignal::new(banned_peer_id).with_span_context()).await?; @@ -677,7 +677,7 @@ where Box::new(move |_info: &mut RunningInfo| { let predicate = predicate.clone(); Box::pin(async move { - debug!(target: "network", "runner.rs: wait_for predicate"); + debug!(target: "test", "runner.rs: wait_for predicate"); if predicate() { return Ok(ControlFlow::Break(())); } diff --git a/tools/chainsync-loadtest/src/network.rs b/tools/chainsync-loadtest/src/network.rs index 923976a4c63..7a765489dcd 100644 --- a/tools/chainsync-loadtest/src/network.rs +++ b/tools/chainsync-loadtest/src/network.rs @@ -86,7 +86,8 @@ impl Network { sent_bytes_per_sec: 0, received_bytes_per_sec: 0, known_producers: vec![], - tier1_accounts: vec![], + tier1_connections: vec![], + tier1_accounts_data: vec![], }), info_futures: Default::default(), }), diff --git a/tools/mock-node/src/lib.rs b/tools/mock-node/src/lib.rs index e8ffd60b320..6be34eba38f 100644 --- a/tools/mock-node/src/lib.rs +++ b/tools/mock-node/src/lib.rs @@ -255,7 +255,8 @@ impl MockPeerManagerActor { sent_bytes_per_sec: 0, received_bytes_per_sec: 0, known_producers: vec![], - tier1_accounts: vec![], + tier1_connections: vec![], + tier1_accounts_data: vec![], }; let incoming_requests = IncomingRequests::new( &network_config.incoming_requests, From 3cd22ff8ae31d3e37903c546a372366e71680fde Mon Sep 17 00:00:00 2001 From: Marcelo Diop-Gonzalez Date: Tue, 6 Dec 2022 10:08:52 -0500 Subject: [PATCH 069/188] feat(mocknet): add a script for running mocknet tests based on `tools/mirror` (#8162) this assumes that there are instances named a certain way with records prepared with `neard mirror prepare` in ~/.near/setup for nodes and in ~/.near/output/ for the traffic generator. It sets up new state with the number of validators specified and starts `neard mirror run` on the traffic generator instance to start the test see the help text of the cmd for more info --- pytest/lib/mocknet.py | 5 +- pytest/tests/mocknet/mirror.py | 486 +++++++++++++++++++++++++++++++++ 2 files changed, 489 insertions(+), 2 deletions(-) create mode 100755 pytest/tests/mocknet/mirror.py diff --git a/pytest/lib/mocknet.py b/pytest/lib/mocknet.py index b718e87481a..36fb82d87e0 100644 --- a/pytest/lib/mocknet.py +++ b/pytest/lib/mocknet.py @@ -428,8 +428,9 @@ def get_near_pid(machine): def stop_node(node): m = node.machine logger.info(f'Stopping node {m.name}') - pid = get_near_pid(m) - if pid != '': + pids = get_near_pid(m).split() + + for pid in pids: m.run('bash', input=kill_proccess_script(pid)) m.run('sudo -u ubuntu -i', input=TMUX_STOP_SCRIPT) diff --git a/pytest/tests/mocknet/mirror.py b/pytest/tests/mocknet/mirror.py new file mode 100755 index 00000000000..f604b2a8a89 --- /dev/null +++ b/pytest/tests/mocknet/mirror.py @@ -0,0 +1,486 @@ +#!/usr/bin/env python3 +""" + +""" +import argparse +import pathlib +import random +from rc import pmap, run +import requests +import sys +import time + +sys.path.append(str(pathlib.Path(__file__).resolve().parents[2] / 'lib')) + +import mocknet + +from configured_logger import logger + + +def get_nodes(args): + pattern = args.chain_id + '-' + str( + args.start_height) + '-' + args.unique_id + all_nodes = mocknet.get_nodes(pattern=pattern) + if len(all_nodes) < 1: + sys.exit(f'no known nodes matching {pattern}') + + traffic_generator = None + nodes = [] + for n in all_nodes: + if n.instance_name.endswith('traffic'): + if traffic_generator is not None: + sys.exit( + f'more than one traffic generator instance found. {traffic_generator.instance_name} and {n.instance_name}' + ) + traffic_generator = n + else: + nodes.append(n) + + if traffic_generator is None: + sys.exit(f'no traffic generator instance found') + 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**30), + }) + + return validators + + +def run_cmd(node, cmd): + r = node.machine.run(cmd) + if r.exitcode != 0: + sys.exit( + f'failed running {cmd} on {node.instance_name}:\nstdout: {r.stdout}\nstderr: {r.stderr}' + ) + return r + + +LOG_DIR = '/home/ubuntu/.near/logs' +STATUS_DIR = '/home/ubuntu/.near/logs/status' + + +def run_in_background(node, cmd, log_filename, env=''): + setup_cmd = f'truncate --size 0 {STATUS_DIR}/{log_filename} ' + setup_cmd += f'&& for i in {{8..0}}; do if [ -f {LOG_DIR}/{log_filename}.$i ]; then mv {LOG_DIR}/{log_filename}.$i {LOG_DIR}/{log_filename}.$((i+1)); fi done' + run_cmd( + node, + f'( {setup_cmd} && {env} nohup {cmd} > {LOG_DIR}/{log_filename}.0 2>&1; nohup echo "$?" ) > {STATUS_DIR}/{log_filename} 2>&1 &' + ) + + +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_in = '/home/ubuntu/.near/setup/genesis.json' + records_file_in = '/home/ubuntu/.near/setup/records.json' + 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_in = '/home/ubuntu/.near/output/genesis.json' + records_file_in = '/home/ubuntu/.near/output/mirror-records.json' + 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', + genesis_file_in, + '--records-file-in', + records_file_in, + '--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): + while True: + try: + res = node.get_validators() + if 'error' not in res: + assert 'result' in res + logger.info(f'Node {node.instance_name} is up') + return + 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]' + ) + if sys.stdin.readline().strip() != 'yes': + sys.exit() + + if args.epoch_length is None: + print('epoch length for the initialized genesis file?: ') + args.epoch_length = int(sys.stdin.readline().strip()) + + if args.num_validators is None: + print('number of validators?: ') + args.num_validators = int(sys.stdin.readline().strip()) + + if args.num_seats is None: + print('number of block producer seats?: ') + args.num_seats = int(sys.stdin.readline().strip()) + + +def setup(args, traffic_generator, nodes): + prompt_setup_flags(args) + + if args.epoch_length <= 0: + sys.exit(f'--epoch-length should be positive') + if args.num_validators <= 0: + sys.exit(f'--num-validators should be positive') + if len(nodes) < args.num_validators: + sys.exit( + f'--num-validators is {args.num_validators} but only found {len(nodes)} under test' + ) + + all_nodes = nodes + [traffic_generator] + mocknet.stop_nodes(all_nodes) + + 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]) + + 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)') + 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') + + logger.info( + f'starting neard nodes then waiting for them to be ready. This may take a very long time (> 12 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): + 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) + + +def reset_data_dirs(args, traffic_generator, nodes): + all_nodes = nodes + [traffic_generator] + mocknet.stop_nodes(all_nodes) + if not all(pmap(check_backup, all_nodes)): + logger.warning('Not continuing with backup restoration') + return + + 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(args, traffic_generator, nodes): + mocknet.stop_nodes(nodes + [traffic_generator]) + + +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' + ) + return + # TODO: handle upgrading to a different binary mid-test + pmap(lambda node: start_neard(node, 'neard.txt'), nodes) + logger.info("waiting for validators to be up") + pmap(lambda node: wait_node_up(node, 'neard.txt'), nodes) + logger.info( + "waiting a bit after validators started before starting traffic") + time.sleep(10) + start_mirror(traffic_generator, 'mirror.txt') + logger.info( + f'test running. to check the traffic sent, try running "curl http://{traffic_generator.machine.ip}:3030/metrics | grep mirror"' + ) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Run a load test') + parser.add_argument('--chain-id', type=str, required=True) + 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) + + subparsers = parser.add_subparsers(title='subcommands', + description='valid subcommands', + help='additional help') + + setup_parser = subparsers.add_parser('setup', + 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. + ''') + setup_parser.add_argument('--start-traffic', + default=False, + action='store_true') + setup_parser.set_defaults(func=setup) + + start_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) + + stop_parser = subparsers.add_parser('stop-nodes', + help='kill all neard processes') + stop_parser.set_defaults(func=stop_nodes) + + 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) + + reset_parser = subparsers.add_parser('reset', + help=''' + The setup 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) + + args = parser.parse_args() + + traffic_generator, nodes = get_nodes(args) + args.func(args, traffic_generator, nodes) From 78ec412a846e8bb8182bcd58f08c003504ea1877 Mon Sep 17 00:00:00 2001 From: mm-near <91919554+mm-near@users.noreply.github.com> Date: Tue, 6 Dec 2022 16:25:38 +0100 Subject: [PATCH 070/188] Adding test to state sync (#8164) Currently the test is covering only the basic header-fetching functionality --- chain/chain/src/chain.rs | 4 +- chain/client-primitives/src/types.rs | 2 +- chain/client/src/sync/state.rs | 142 ++++++++++++++++++++++++++- chain/network/src/test_utils.rs | 3 + 4 files changed, 146 insertions(+), 5 deletions(-) diff --git a/chain/chain/src/chain.rs b/chain/chain/src/chain.rs index dc5cb605b78..e15e75170b9 100644 --- a/chain/chain/src/chain.rs +++ b/chain/chain/src/chain.rs @@ -3113,7 +3113,7 @@ impl Chain { } pub fn schedule_apply_state_parts( - &mut self, + &self, shard_id: ShardId, sync_hash: CryptoHash, num_parts: u64, @@ -3168,7 +3168,7 @@ impl Chain { } pub fn build_state_for_split_shards_preprocessing( - &mut self, + &self, sync_hash: &CryptoHash, shard_id: ShardId, state_split_scheduler: &dyn Fn(StateSplitRequest), diff --git a/chain/client-primitives/src/types.rs b/chain/client-primitives/src/types.rs index 8a27b5ac246..6395829bc9e 100644 --- a/chain/client-primitives/src/types.rs +++ b/chain/client-primitives/src/types.rs @@ -41,7 +41,7 @@ pub enum Error { Other(String), } -#[derive(Clone, Debug, serde::Serialize)] +#[derive(Clone, Debug, serde::Serialize, PartialEq)] pub enum AccountOrPeerIdOrHash { AccountId(AccountId), PeerId(PeerId), diff --git a/chain/client/src/sync/state.rs b/chain/client/src/sync/state.rs index 53f22d93fdf..cf46fda3818 100644 --- a/chain/client/src/sync/state.rs +++ b/chain/client/src/sync/state.rs @@ -198,8 +198,8 @@ impl StateSync { let prev_hash = *chain.get_block_header(&sync_hash)?.prev_hash(); let prev_epoch_id = chain.get_block_header(&prev_hash)?.epoch_id().clone(); let epoch_id = chain.get_block_header(&sync_hash)?.epoch_id().clone(); - if chain.runtime_adapter.get_shard_layout(&prev_epoch_id)? - != chain.runtime_adapter.get_shard_layout(&epoch_id)? + if runtime_adapter.get_shard_layout(&prev_epoch_id)? + != runtime_adapter.get_shard_layout(&epoch_id)? { error!("cannot sync to the first epoch after sharding upgrade"); panic!("cannot sync to the first epoch after sharding upgrade. Please wait for the next epoch or find peers that are more up to date"); @@ -886,3 +886,141 @@ impl Iterator for SamplerLimited { } } } + +#[cfg(test)] +mod test { + + use actix::System; + use near_actix_test_utils::run_actix; + use near_chain::{test_utils::process_block_sync, Block, BlockProcessingArtifact, Provenance}; + + use near_epoch_manager::EpochManagerAdapter; + use near_network::test_utils::MockPeerManagerAdapter; + use near_primitives::{ + merkle::PartialMerkleTree, + syncing::{ShardStateSyncResponseHeader, ShardStateSyncResponseV2}, + types::EpochId, + }; + + use near_chain::test_utils; + + use super::*; + + #[test] + // Start a new state sync - and check that it asks for a header. + fn test_ask_for_header() { + let mock_peer_manager = Arc::new(MockPeerManagerAdapter::default()); + let mut state_sync = StateSync::new(mock_peer_manager.clone(), TimeDuration::from_secs(1)); + let mut new_shard_sync = HashMap::new(); + + let (mut chain, kv, signer) = test_utils::setup(); + + // TODO: lower the epoch length + for _ in 0..(chain.epoch_length + 1) { + let prev = chain.get_block(&chain.head().unwrap().last_block_hash).unwrap(); + let block = if kv.is_next_block_epoch_start(prev.hash()).unwrap() { + Block::empty_with_epoch( + &prev, + prev.header().height() + 1, + prev.header().next_epoch_id().clone(), + EpochId { 0: *prev.hash() }, + *prev.header().next_bp_hash(), + &*signer, + &mut PartialMerkleTree::default(), + ) + } else { + Block::empty(&prev, &*signer) + }; + + process_block_sync( + &mut chain, + &None, + block.into(), + Provenance::PRODUCED, + &mut BlockProcessingArtifact::default(), + ) + .unwrap(); + } + + let request_hash = &chain.head().unwrap().last_block_hash; + let state_sync_header = chain.get_state_response_header(0, *request_hash).unwrap(); + let state_sync_header = match state_sync_header { + ShardStateSyncResponseHeader::V1(_) => panic!("Invalid header"), + ShardStateSyncResponseHeader::V2(internal) => internal, + }; + + let apply_parts_fn = move |_: ApplyStatePartsRequest| {}; + let state_split_fn = move |_: StateSplitRequest| {}; + + run_actix(async { + state_sync + .run( + &None, + *request_hash, + &mut new_shard_sync, + &mut chain, + &(kv as Arc), + &[], + vec![0], + &apply_parts_fn, + &state_split_fn, + ) + .unwrap(); + + // Wait for the message that is sent to peer manager. + mock_peer_manager.notify.notified().await; + let request = mock_peer_manager.pop().unwrap(); + + assert_eq!( + NetworkRequests::StateRequestHeader { + shard_id: 0, + sync_hash: *request_hash, + target: AccountOrPeerIdOrHash::AccountId("test".parse().unwrap()) + }, + request.as_network_requests() + ); + + assert_eq!(1, new_shard_sync.len()); + let download = new_shard_sync.get(&0).unwrap(); + + assert_eq!(download.status, ShardSyncStatus::StateDownloadHeader); + + assert_eq!(download.downloads.len(), 1); + let download_status = &download.downloads[0]; + + // 'run me' is false - as we've just executed this peer manager request. + assert_eq!(download_status.run_me.load(Ordering::SeqCst), false); + assert_eq!(download_status.error, false); + assert_eq!(download_status.done, false); + assert_eq!(download_status.state_requests_count, 1); + assert_eq!( + download_status.last_target, + Some(near_client_primitives::types::AccountOrPeerIdOrHash::AccountId( + "test".parse().unwrap() + )) + ); + + // Now let's simulate header return message. + + let state_response = ShardStateSyncResponse::V2(ShardStateSyncResponseV2 { + header: Some(state_sync_header), + part: None, + }); + + state_sync.update_download_on_state_response_message( + &mut new_shard_sync.get_mut(&0).unwrap(), + *request_hash, + 0, + state_response, + &mut chain, + ); + + let download = new_shard_sync.get(&0).unwrap(); + assert_eq!(download.status, ShardSyncStatus::StateDownloadHeader); + // Download should be marked as done. + assert_eq!(download.downloads[0].done, true); + + System::current().stop() + }); + } +} diff --git a/chain/network/src/test_utils.rs b/chain/network/src/test_utils.rs index ef1d65f394c..64f5d00943d 100644 --- a/chain/network/src/test_utils.rs +++ b/chain/network/src/test_utils.rs @@ -20,6 +20,7 @@ use std::collections::{HashMap, HashSet, VecDeque}; use std::net::TcpListener; use std::ops::ControlFlow; use std::sync::{Arc, Mutex, RwLock}; +use tokio::sync::Notify; use tracing::debug; static OPENED_PORTS: Lazy>> = Lazy::new(|| Mutex::new(HashSet::new())); @@ -286,6 +287,7 @@ impl Handler> for PeerManagerActor { #[derive(Default)] pub struct MockPeerManagerAdapter { pub requests: Arc>>, + pub notify: Notify, } impl MsgRecipient> for MockPeerManagerAdapter { @@ -300,6 +302,7 @@ impl MsgRecipient> for MockPeerManage fn do_send(&self, msg: WithSpanContext) { self.requests.write().unwrap().push_back(msg.msg); + self.notify.notify_one(); } } From 80e9960de2af81b61089a20949718662977ef72a Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Tue, 6 Dec 2022 22:40:13 +0100 Subject: [PATCH 071/188] refactor: Fix near-stdx clippy warnings (#8169)
Warnings fixed ``` warning: this let-binding has unit value --> utils/stdx/src/lib.rs:11:5 | 11 | let () = AssertEqSum::::OK; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: omit the `let` binding: `AssertEqSum::::OK;` | = note: `#[warn(clippy::let_unit_value)]` on by default = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#let_unit_value warning: this let-binding has unit value --> utils/stdx/src/lib.rs:90:5 | 90 | let () = AssertNonZero::::OK; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: omit the `let` binding: `AssertNonZero::::OK;` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#let_unit_value warning: this returns a `Result<_, ()>` --> utils/stdx/src/lib.rs:102:1 | 102 | pub fn as_chunks_exact(slice: &[T]) -> Result<&[[T; N]], ()> { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: `#[warn(clippy::result_unit_err)]` on by default = help: use a custom `Error` type instead = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#result_unit_err warning: boolean to int conversion using if --> utils/stdx/src/lib.rs:127:25 | 127 | const OK: () = [()][if N == 0 { 1 } else { 0 }]; | ^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace with from: `usize::from(N == 0)` | = note: `#[warn(clippy::bool_to_int_with_if)]` on by default = note: `(N == 0) as usize` or `(N == 0).into()` can also be valid options = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#bool_to_int_with_if ```
`let_unit_value` is suppressed since this is explicitly what we want in that case. Please note that `_ = AssertEqSum::::OK;` produces the same clippy warning and having just `AssertEqSum::::OK;` results in `path statement with no effect` rustc warning. Also we can't use `static_assertions` crate here since `near-stdx` should not have external dependencies as stated in `utils/stdx/Cargo.toml`. This also includes refactor around static assertions to use std `assert!` which still works on compile time:
Assertion failure example ``` error[E0080]: evaluation of `AssertEqSum::<5, 1, 3>::OK` failed --> utils/stdx/src/lib.rs:125:20 | 125 | const OK: () = assert!(S == A + B); | ^^^^^^^^^^^^^^^^^^^ the evaluated program panicked at 'assertion failed: S == A + B', utils/stdx/src/lib.rs:125:20 | = note: this error originates in the macro `assert` (in Nightly builds, run with -Z macro-backtrace for more info) note: the above error was encountered while instantiating `fn split_array::<5, 1, 3>` --> utils/stdx/src/lib.rs:61:36 | 61 | assert_eq!((&[0], &[2, 3, 4]), split_array(&[0, 1, 2, 3, 4])); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For more information about this error, try `rustc --explain E0080`. error: could not compile `near-stdx` due to previous error ```
Part of #8145 --- runtime/near-vm-logic/src/alt_bn128.rs | 4 +-- runtime/near-vm-logic/src/tests/alt_bn128.rs | 6 ++-- utils/stdx/src/lib.rs | 33 +++++++++++++++++--- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/runtime/near-vm-logic/src/alt_bn128.rs b/runtime/near-vm-logic/src/alt_bn128.rs index 0836905223e..e99d14e6c24 100644 --- a/runtime/near-vm-logic/src/alt_bn128.rs +++ b/runtime/near-vm-logic/src/alt_bn128.rs @@ -25,9 +25,7 @@ impl From for VMLogicError { pub(crate) fn split_elements( data: &[u8], ) -> Result<&[[u8; ELEMENT_SIZE]], InvalidInput> { - stdx::as_chunks_exact(data).map_err(|()| InvalidInput { - msg: format!("invalid array, byte length {}, element size {}", data.len(), ELEMENT_SIZE), - }) + stdx::as_chunks_exact(data).map_err(|e| InvalidInput { msg: e.to_string() }) } const G1_MULTIEXP_ELEMENT_SIZE: usize = POINT_SIZE + SCALAR_SIZE; diff --git a/runtime/near-vm-logic/src/tests/alt_bn128.rs b/runtime/near-vm-logic/src/tests/alt_bn128.rs index 280ac487ed0..cccc3ff6208 100644 --- a/runtime/near-vm-logic/src/tests/alt_bn128.rs +++ b/runtime/near-vm-logic/src/tests/alt_bn128.rs @@ -136,7 +136,7 @@ fn test_alt_bn128_g1_multiexp() { ], ); - check_err(b"XXXX", "invalid array, byte length 4, element size 96"); + check_err(b"XXXX", "slice of size 4 cannot be precisely split into chunks of size 96"); check_err( &le_bytes![0x92 0x2944829dcfa7dd72bb04d12e46869e6a6c8162698f9a6c35724f91f597e25fc4 0x112b450c0769c7cd80ffa552aaab2153adb5646664ee091639784a7f887411f7], "invalid g1", @@ -201,7 +201,7 @@ fn test_alt_bn128_g1_sum() { ], ); - check_err(&[92], "invalid array, byte length 1, element size 65"); + check_err(&[92], "slice of size 1 cannot be precisely split into chunks of size 65"); check_err( &le_bytes![ 0u8 0x111 0x222 @@ -267,6 +267,6 @@ fn test_alt_bn128_pairing_check() { 0, ); - check_err(b"XXXX", "invalid array, byte length 4, element size 192"); + check_err(b"XXXX", "slice of size 4 cannot be precisely split into chunks of size 192"); check_err(&le_bytes![0x0 0x0 0x0 0x0 0x0 0x0, 0x0 0x0 0x0 0x0 0x0 0x111], "invalid g2"); } diff --git a/utils/stdx/src/lib.rs b/utils/stdx/src/lib.rs index 97091e31681..aa36f3eec83 100644 --- a/utils/stdx/src/lib.rs +++ b/utils/stdx/src/lib.rs @@ -8,6 +8,7 @@ pub fn split_array( xs: &[u8; N], ) -> (&[u8; L], &[u8; R]) { + #[allow(clippy::let_unit_value)] let () = AssertEqSum::::OK; let (left, right) = xs.split_at(L); @@ -18,6 +19,7 @@ pub fn split_array( pub fn split_array_mut( xs: &mut [u8; N], ) -> (&mut [u8; L], &mut [u8; R]) { + #[allow(clippy::let_unit_value)] let () = AssertEqSum::::OK; let (left, right) = xs.split_at_mut(L); @@ -70,6 +72,7 @@ pub fn join_array( left: [u8; L], right: [u8; R], ) -> [u8; N] { + #[allow(clippy::let_unit_value)] let () = AssertEqSum::::OK; let mut res = [0; N]; @@ -87,6 +90,7 @@ fn test_join() { /// Splits a slice into a slice of N-element arrays. // TODO(mina86): Replace with [T]::as_chunks once that’s stabilised. pub fn as_chunks(slice: &[T]) -> (&[[T; N]], &[T]) { + #[allow(clippy::let_unit_value)] let () = AssertNonZero::::OK; let len = slice.len() / N; @@ -98,13 +102,29 @@ pub fn as_chunks(slice: &[T]) -> (&[[T; N]], &[T]) { (head, tail) } +#[derive(Debug, Eq, PartialEq)] +pub struct InexactChunkingError { + slice_len: usize, + chunk_size: usize, +} +impl std::error::Error for InexactChunkingError {} +impl std::fmt::Display for InexactChunkingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "slice of size {} cannot be precisely split into chunks of size {}", + self.slice_len, self.chunk_size + ) + } +} + /// Like `as_chunks` but returns an error if there’s a remainder. -pub fn as_chunks_exact(slice: &[T]) -> Result<&[[T; N]], ()> { +pub fn as_chunks_exact(slice: &[T]) -> Result<&[[T; N]], InexactChunkingError> { let (chunks, remainder) = as_chunks(slice); if remainder.is_empty() { Ok(chunks) } else { - Err(()) + Err(InexactChunkingError { slice_len: slice.len(), chunk_size: N }) } } @@ -112,17 +132,20 @@ pub fn as_chunks_exact(slice: &[T]) -> Result<&[[T; N]], ()> fn test_as_chunks() { assert_eq!((&[[0, 1], [2, 3]][..], &[4][..]), as_chunks::<2, _>(&[0, 1, 2, 3, 4])); assert_eq!(Ok(&[[0, 1], [2, 3]][..]), as_chunks_exact::<2, _>(&[0, 1, 2, 3])); - assert_eq!(Err(()), as_chunks_exact::<2, _>(&[0, 1, 2, 3, 4])); + assert_eq!( + Err(InexactChunkingError { slice_len: 5, chunk_size: 2 }), + as_chunks_exact::<2, _>(&[0, 1, 2, 3, 4]) + ); } /// Asserts, at compile time, that `S == A + B`. struct AssertEqSum; impl AssertEqSum { - const OK: () = [()][A + B - S]; + const OK: () = assert!(S == A + B); } /// Asserts, at compile time, that `N` is non-zero. struct AssertNonZero; impl AssertNonZero { - const OK: () = [()][if N == 0 { 1 } else { 0 }]; + const OK: () = assert!(N != 0); } From 8c83455a604ec259c7f3b2d2e06392eff6743efe Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Wed, 7 Dec 2022 08:44:53 +0000 Subject: [PATCH 072/188] refactor: ExtCostsConfig as simple map of costs (#8115) This is the next step in the refactoring steps for changing gas profiles to track gas by parameter (#8033). Here we make `ExtCostsConfig` opaque and look up parameters by ```rust pub fn cost(&self, param: ExtCosts) -> Gas ``` instead of using a specific field inside. There are side-effects for this in 1. parameter definition 2. JSON RPC 3. parameter estimator 1) We no longer load the parameters through "parameter table -> JSON -> serde_deser" steps because `ExtCostsConfig` no longer has serde derives. Instead each `ExtCosts` maps to a `Parameter` that allows looking up the value directly from `ParameterTable`. This explicit mapping also replaces the `Parameter::ext_costs()` iterator previously used to find all parameters that are ext costs. We used to define `wasm_read_cached_trie_node` in `53.txt` and fill old values with serde default. Serde was removed here, so I changed it to define the parameter in the base files. This is equivalent to the old behavior, only it is less clear when we added the parameter. 2) JSON RPC must keep the old format. Thus, I added `ExtCostsConfigView` and `VMConfigView` there. It is a direct copy-paste of the old structs in the old format but without serde magic to fill in missing values. 3) The estimator generates a `ExtCostsConfig` from estimations. This is now done through a mapping from estimated costs to `ExtCosts`. # Testing The exact JSON output is checked in existing tests `test_json_unchanged`. --- core/primitives-core/src/config.rs | 526 +++++------------- core/primitives-core/src/parameter.rs | 71 --- core/primitives/res/runtime_configs/53.txt | 1 - .../res/runtime_configs/parameters.txt | 1 + .../runtime_configs/parameters_testnet.txt | 1 + core/primitives/src/runtime/config_store.rs | 6 +- .../primitives/src/runtime/parameter_table.rs | 9 +- core/primitives/src/views.rs | 365 +++++++++++- .../src/tests/client/process_blocks.rs | 6 +- .../src/tests/runtime/sanity_checks.rs | 4 +- .../src/tests/standard_cases/mod.rs | 10 +- nearcore/src/runtime/mod.rs | 5 +- .../src/costs_to_runtime_config.rs | 151 ++--- 13 files changed, 612 insertions(+), 544 deletions(-) diff --git a/core/primitives-core/src/config.rs b/core/primitives-core/src/config.rs index b8f41f858f7..25cba9c1299 100644 --- a/core/primitives-core/src/config.rs +++ b/core/primitives-core/src/config.rs @@ -1,9 +1,11 @@ +use crate::parameter::Parameter; use crate::types::Gas; +use enum_map::{enum_map, EnumMap}; use serde::{Deserialize, Serialize}; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; -use strum::{Display, EnumCount}; +use strum::Display; /// Dynamic configuration parameters required for the WASM runtime to /// execute a smart contract. @@ -12,7 +14,7 @@ use strum::{Display, EnumCount}; /// 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, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct VMConfig { /// Costs for runtime externals pub ext_costs: ExtCostsConfig, @@ -256,183 +258,9 @@ pub struct ViewConfig { pub max_gas_burnt: Gas, } -#[derive(Debug, Serialize, Deserialize, Clone, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct ExtCostsConfig { - /// Base cost for calling a host function. - pub base: Gas, - - /// Base cost of loading a pre-compiled contract - pub contract_loading_base: Gas, - /// Cost per byte of loading a pre-compiled contract - pub contract_loading_bytes: Gas, - - /// Base cost for guest memory read - pub read_memory_base: Gas, - /// Cost for guest memory read - pub read_memory_byte: Gas, - - /// Base cost for guest memory write - pub write_memory_base: Gas, - /// Cost for guest memory write per byte - pub write_memory_byte: Gas, - - /// Base cost for reading from register - pub read_register_base: Gas, - /// Cost for reading byte from register - pub read_register_byte: Gas, - - /// Base cost for writing into register - pub write_register_base: Gas, - /// Cost for writing byte into register - pub write_register_byte: Gas, - - /// Base cost of decoding utf8. It's used for `log_utf8` and `panic_utf8`. - pub utf8_decoding_base: Gas, - /// Cost per byte of decoding utf8. It's used for `log_utf8` and `panic_utf8`. - pub utf8_decoding_byte: Gas, - - /// Base cost of decoding utf16. It's used for `log_utf16`. - pub utf16_decoding_base: Gas, - /// Cost per byte of decoding utf16. It's used for `log_utf16`. - pub utf16_decoding_byte: Gas, - - /// Cost of getting sha256 base - pub sha256_base: Gas, - /// Cost of getting sha256 per byte - pub sha256_byte: Gas, - - /// Cost of getting sha256 base - pub keccak256_base: Gas, - /// Cost of getting sha256 per byte - pub keccak256_byte: Gas, - - /// Cost of getting sha256 base - pub keccak512_base: Gas, - /// Cost of getting sha256 per byte - pub keccak512_byte: Gas, - - /// Cost of getting ripemd160 base - pub ripemd160_base: Gas, - /// Cost of getting ripemd160 per message block - pub ripemd160_block: Gas, - - /// Cost of getting ed25519 base - #[cfg(feature = "protocol_feature_ed25519_verify")] - pub ed25519_verify_base: Gas, - /// Cost of getting ed25519 per byte - #[cfg(feature = "protocol_feature_ed25519_verify")] - pub ed25519_verify_byte: Gas, - - /// Cost of calling ecrecover - pub ecrecover_base: Gas, - - /// Cost for calling logging. - pub log_base: Gas, - /// Cost for logging per byte - pub log_byte: Gas, - - // ############### - // # Storage API # - // ############### - /// Storage trie write key base cost - pub storage_write_base: Gas, - /// Storage trie write key per byte cost - pub storage_write_key_byte: Gas, - /// Storage trie write value per byte cost - pub storage_write_value_byte: Gas, - /// Storage trie write cost per byte of evicted value. - pub storage_write_evicted_byte: Gas, - - /// Storage trie read key base cost - pub storage_read_base: Gas, - /// Storage trie read key per byte cost - pub storage_read_key_byte: Gas, - /// Storage trie read value cost per byte cost - pub storage_read_value_byte: Gas, - - /// Remove key from trie base cost - pub storage_remove_base: Gas, - /// Remove key from trie per byte cost - pub storage_remove_key_byte: Gas, - /// Remove key from trie ret value byte cost - pub storage_remove_ret_value_byte: Gas, - - /// Storage trie check for key existence cost base - pub storage_has_key_base: Gas, - /// Storage trie check for key existence per key byte - pub storage_has_key_byte: Gas, - - /// Create trie prefix iterator cost base - pub storage_iter_create_prefix_base: Gas, - /// Create trie prefix iterator cost per byte. - pub storage_iter_create_prefix_byte: Gas, - - /// Create trie range iterator cost base - pub storage_iter_create_range_base: Gas, - /// Create trie range iterator cost per byte of from key. - pub storage_iter_create_from_byte: Gas, - /// Create trie range iterator cost per byte of to key. - pub storage_iter_create_to_byte: Gas, - - /// Trie iterator per key base cost - pub storage_iter_next_base: Gas, - /// Trie iterator next key byte cost - pub storage_iter_next_key_byte: Gas, - /// Trie iterator next key byte cost - pub storage_iter_next_value_byte: Gas, - - /// Cost per reading trie node from DB - pub touching_trie_node: Gas, - /// Cost for reading trie node from memory - #[serde(default = "default_read_cached_trie_node")] - pub read_cached_trie_node: Gas, - - // ############### - // # Promise API # - // ############### - /// Cost for calling `promise_and` - pub promise_and_base: Gas, - /// Cost for calling `promise_and` for each promise - pub promise_and_per_promise: Gas, - /// Cost for calling `promise_return` - pub promise_return: Gas, - - // ############### - // # Validator API # - // ############### - /// Cost of calling `validator_stake`. - pub validator_stake_base: Gas, - /// Cost of calling `validator_total_stake`. - pub validator_total_stake_base: Gas, - - // Workaround to keep JSON serialization backwards-compatible - // . - // - // Remove once #5516 is fixed. - #[serde(default, rename = "contract_compile_base")] - pub _unused1: Gas, - #[serde(default, rename = "contract_compile_bytes")] - pub _unused2: Gas, - - // ############# - // # Alt BN128 # - // ############# - /// Base cost for multiexp - pub alt_bn128_g1_multiexp_base: Gas, - /// Per element cost for multiexp - pub alt_bn128_g1_multiexp_element: Gas, - /// Base cost for sum - pub alt_bn128_g1_sum_base: Gas, - /// Per element cost for sum - pub alt_bn128_g1_sum_element: Gas, - /// Base cost for pairing check - pub alt_bn128_pairing_check_base: Gas, - /// Per element cost for pairing check - pub alt_bn128_pairing_check_element: Gas, -} - -fn default_read_cached_trie_node() -> Gas { - SAFETY_MULTIPLIER * 760_000_000 + pub costs: EnumMap, } // We multiply the actual computed costs by the fixed factor to ensure we @@ -440,153 +268,103 @@ fn default_read_cached_trie_node() -> Gas { const SAFETY_MULTIPLIER: u64 = 3; impl ExtCostsConfig { + pub fn cost(&self, param: ExtCosts) -> Gas { + self.costs[param] + } + /// Convenience constructor to use in tests where the exact gas cost does /// not need to correspond to a specific protocol version. pub fn test() -> ExtCostsConfig { - ExtCostsConfig { - base: SAFETY_MULTIPLIER * 88256037, - contract_loading_base: SAFETY_MULTIPLIER * 11815321, - contract_loading_bytes: SAFETY_MULTIPLIER * 72250, - read_memory_base: SAFETY_MULTIPLIER * 869954400, - read_memory_byte: SAFETY_MULTIPLIER * 1267111, - write_memory_base: SAFETY_MULTIPLIER * 934598287, - write_memory_byte: SAFETY_MULTIPLIER * 907924, - read_register_base: SAFETY_MULTIPLIER * 839055062, - read_register_byte: SAFETY_MULTIPLIER * 32854, - write_register_base: SAFETY_MULTIPLIER * 955174162, - write_register_byte: SAFETY_MULTIPLIER * 1267188, - utf8_decoding_base: SAFETY_MULTIPLIER * 1037259687, - utf8_decoding_byte: SAFETY_MULTIPLIER * 97193493, - utf16_decoding_base: SAFETY_MULTIPLIER * 1181104350, - utf16_decoding_byte: SAFETY_MULTIPLIER * 54525831, - sha256_base: SAFETY_MULTIPLIER * 1513656750, - sha256_byte: SAFETY_MULTIPLIER * 8039117, - keccak256_base: SAFETY_MULTIPLIER * 1959830425, - keccak256_byte: SAFETY_MULTIPLIER * 7157035, - keccak512_base: SAFETY_MULTIPLIER * 1937129412, - keccak512_byte: SAFETY_MULTIPLIER * 12216567, - ripemd160_base: SAFETY_MULTIPLIER * 284558362, + let costs = enum_map! { + ExtCosts::base => SAFETY_MULTIPLIER * 88256037, + ExtCosts::contract_loading_base => SAFETY_MULTIPLIER * 11815321, + ExtCosts::contract_loading_bytes => SAFETY_MULTIPLIER * 72250, + ExtCosts::read_memory_base => SAFETY_MULTIPLIER * 869954400, + ExtCosts::read_memory_byte => SAFETY_MULTIPLIER * 1267111, + ExtCosts::write_memory_base => SAFETY_MULTIPLIER * 934598287, + ExtCosts::write_memory_byte => SAFETY_MULTIPLIER * 907924, + ExtCosts::read_register_base => SAFETY_MULTIPLIER * 839055062, + ExtCosts::read_register_byte => SAFETY_MULTIPLIER * 32854, + ExtCosts::write_register_base => SAFETY_MULTIPLIER * 955174162, + ExtCosts::write_register_byte => SAFETY_MULTIPLIER * 1267188, + ExtCosts::utf8_decoding_base => SAFETY_MULTIPLIER * 1037259687, + ExtCosts::utf8_decoding_byte => SAFETY_MULTIPLIER * 97193493, + ExtCosts::utf16_decoding_base => SAFETY_MULTIPLIER * 1181104350, + ExtCosts::utf16_decoding_byte => SAFETY_MULTIPLIER * 54525831, + ExtCosts::sha256_base => SAFETY_MULTIPLIER * 1513656750, + ExtCosts::sha256_byte => SAFETY_MULTIPLIER * 8039117, + ExtCosts::keccak256_base => SAFETY_MULTIPLIER * 1959830425, + ExtCosts::keccak256_byte => SAFETY_MULTIPLIER * 7157035, + ExtCosts::keccak512_base => SAFETY_MULTIPLIER * 1937129412, + ExtCosts::keccak512_byte => SAFETY_MULTIPLIER * 12216567, + ExtCosts::ripemd160_base => SAFETY_MULTIPLIER * 284558362, #[cfg(feature = "protocol_feature_ed25519_verify")] - ed25519_verify_base: SAFETY_MULTIPLIER * 1513656750, + ExtCosts::ed25519_verify_base => SAFETY_MULTIPLIER * 1513656750, #[cfg(feature = "protocol_feature_ed25519_verify")] - ed25519_verify_byte: SAFETY_MULTIPLIER * 7157035, - // Cost per byte is 3542227. There are 64 bytes in a block. - ripemd160_block: SAFETY_MULTIPLIER * 226702528, - ecrecover_base: SAFETY_MULTIPLIER * 1121789875000, - log_base: SAFETY_MULTIPLIER * 1181104350, - log_byte: SAFETY_MULTIPLIER * 4399597, - storage_write_base: SAFETY_MULTIPLIER * 21398912000, - storage_write_key_byte: SAFETY_MULTIPLIER * 23494289, - storage_write_value_byte: SAFETY_MULTIPLIER * 10339513, - storage_write_evicted_byte: SAFETY_MULTIPLIER * 10705769, - storage_read_base: SAFETY_MULTIPLIER * 18785615250, - storage_read_key_byte: SAFETY_MULTIPLIER * 10317511, - storage_read_value_byte: SAFETY_MULTIPLIER * 1870335, - storage_remove_base: SAFETY_MULTIPLIER * 17824343500, - storage_remove_key_byte: SAFETY_MULTIPLIER * 12740128, - storage_remove_ret_value_byte: SAFETY_MULTIPLIER * 3843852, - storage_has_key_base: SAFETY_MULTIPLIER * 18013298875, - storage_has_key_byte: SAFETY_MULTIPLIER * 10263615, - storage_iter_create_prefix_base: SAFETY_MULTIPLIER * 0, - storage_iter_create_prefix_byte: SAFETY_MULTIPLIER * 0, - storage_iter_create_range_base: SAFETY_MULTIPLIER * 0, - storage_iter_create_from_byte: SAFETY_MULTIPLIER * 0, - storage_iter_create_to_byte: SAFETY_MULTIPLIER * 0, - storage_iter_next_base: SAFETY_MULTIPLIER * 0, - storage_iter_next_key_byte: SAFETY_MULTIPLIER * 0, - storage_iter_next_value_byte: SAFETY_MULTIPLIER * 0, - touching_trie_node: SAFETY_MULTIPLIER * 5367318642, - read_cached_trie_node: default_read_cached_trie_node(), - promise_and_base: SAFETY_MULTIPLIER * 488337800, - promise_and_per_promise: SAFETY_MULTIPLIER * 1817392, - promise_return: SAFETY_MULTIPLIER * 186717462, - validator_stake_base: SAFETY_MULTIPLIER * 303944908800, - validator_total_stake_base: SAFETY_MULTIPLIER * 303944908800, - _unused1: 0, - _unused2: 0, - alt_bn128_g1_multiexp_base: 713_000_000_000, - alt_bn128_g1_multiexp_element: 320_000_000_000, - alt_bn128_pairing_check_base: 9_686_000_000_000, - alt_bn128_pairing_check_element: 5_102_000_000_000, - alt_bn128_g1_sum_base: 3_000_000_000, - alt_bn128_g1_sum_element: 5_000_000_000, - } + ExtCosts::ed25519_verify_byte => SAFETY_MULTIPLIER * 7157035, + ExtCosts::ripemd160_block => SAFETY_MULTIPLIER * 226702528, + ExtCosts::ecrecover_base => SAFETY_MULTIPLIER * 1121789875000, + ExtCosts::log_base => SAFETY_MULTIPLIER * 1181104350, + ExtCosts::log_byte => SAFETY_MULTIPLIER * 4399597, + ExtCosts::storage_write_base => SAFETY_MULTIPLIER * 21398912000, + ExtCosts::storage_write_key_byte => SAFETY_MULTIPLIER * 23494289, + ExtCosts::storage_write_value_byte => SAFETY_MULTIPLIER * 10339513, + ExtCosts::storage_write_evicted_byte => SAFETY_MULTIPLIER * 10705769, + ExtCosts::storage_read_base => SAFETY_MULTIPLIER * 18785615250, + ExtCosts::storage_read_key_byte => SAFETY_MULTIPLIER * 10317511, + ExtCosts::storage_read_value_byte => SAFETY_MULTIPLIER * 1870335, + ExtCosts::storage_remove_base => SAFETY_MULTIPLIER * 17824343500, + ExtCosts::storage_remove_key_byte => SAFETY_MULTIPLIER * 12740128, + ExtCosts::storage_remove_ret_value_byte => SAFETY_MULTIPLIER * 3843852, + ExtCosts::storage_has_key_base => SAFETY_MULTIPLIER * 18013298875, + ExtCosts::storage_has_key_byte => SAFETY_MULTIPLIER * 10263615, + ExtCosts::storage_iter_create_prefix_base => SAFETY_MULTIPLIER * 0, + ExtCosts::storage_iter_create_prefix_byte => SAFETY_MULTIPLIER * 0, + ExtCosts::storage_iter_create_range_base => SAFETY_MULTIPLIER * 0, + ExtCosts::storage_iter_create_from_byte => SAFETY_MULTIPLIER * 0, + ExtCosts::storage_iter_create_to_byte => SAFETY_MULTIPLIER * 0, + ExtCosts::storage_iter_next_base => SAFETY_MULTIPLIER * 0, + ExtCosts::storage_iter_next_key_byte => SAFETY_MULTIPLIER * 0, + ExtCosts::storage_iter_next_value_byte => SAFETY_MULTIPLIER * 0, + ExtCosts::touching_trie_node => SAFETY_MULTIPLIER * 5367318642, + ExtCosts::read_cached_trie_node => SAFETY_MULTIPLIER * 760_000_000, + ExtCosts::promise_and_base => SAFETY_MULTIPLIER * 488337800, + ExtCosts::promise_and_per_promise => SAFETY_MULTIPLIER * 1817392, + ExtCosts::promise_return => SAFETY_MULTIPLIER * 186717462, + ExtCosts::validator_stake_base => SAFETY_MULTIPLIER * 303944908800, + ExtCosts::validator_total_stake_base => SAFETY_MULTIPLIER * 303944908800, + ExtCosts::alt_bn128_g1_multiexp_base => 713_000_000_000, + ExtCosts::alt_bn128_g1_multiexp_element => 320_000_000_000, + ExtCosts::alt_bn128_pairing_check_base => 9_686_000_000_000, + ExtCosts::alt_bn128_pairing_check_element => 5_102_000_000_000, + ExtCosts::alt_bn128_g1_sum_base => 3_000_000_000, + ExtCosts::alt_bn128_g1_sum_element => 5_000_000_000, + }; + ExtCostsConfig { costs } } fn free() -> ExtCostsConfig { ExtCostsConfig { - base: 0, - contract_loading_base: 0, - contract_loading_bytes: 0, - read_memory_base: 0, - read_memory_byte: 0, - write_memory_base: 0, - write_memory_byte: 0, - read_register_base: 0, - read_register_byte: 0, - write_register_base: 0, - write_register_byte: 0, - utf8_decoding_base: 0, - utf8_decoding_byte: 0, - utf16_decoding_base: 0, - utf16_decoding_byte: 0, - sha256_base: 0, - sha256_byte: 0, - keccak256_base: 0, - keccak256_byte: 0, - keccak512_base: 0, - keccak512_byte: 0, - ripemd160_base: 0, - ripemd160_block: 0, - #[cfg(feature = "protocol_feature_ed25519_verify")] - ed25519_verify_base: 0, - #[cfg(feature = "protocol_feature_ed25519_verify")] - ed25519_verify_byte: 0, - ecrecover_base: 0, - log_base: 0, - log_byte: 0, - storage_write_base: 0, - storage_write_key_byte: 0, - storage_write_value_byte: 0, - storage_write_evicted_byte: 0, - storage_read_base: 0, - storage_read_key_byte: 0, - storage_read_value_byte: 0, - storage_remove_base: 0, - storage_remove_key_byte: 0, - storage_remove_ret_value_byte: 0, - storage_has_key_base: 0, - storage_has_key_byte: 0, - 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: 0, - read_cached_trie_node: 0, - promise_and_base: 0, - promise_and_per_promise: 0, - promise_return: 0, - validator_stake_base: 0, - validator_total_stake_base: 0, - _unused1: 0, - _unused2: 0, - alt_bn128_g1_multiexp_base: 0, - alt_bn128_g1_multiexp_element: 0, - alt_bn128_pairing_check_base: 0, - alt_bn128_pairing_check_element: 0, - alt_bn128_g1_sum_base: 0, - alt_bn128_g1_sum_element: 0, + costs: enum_map! { + _ => 0 + }, } } } /// Strongly-typed representation of the fees for counting. #[derive( - Copy, Clone, Hash, PartialEq, Eq, Debug, PartialOrd, Ord, EnumCount, Display, strum::EnumIter, + Copy, + Clone, + Hash, + PartialEq, + Eq, + Debug, + PartialOrd, + Ord, + Display, + strum::EnumIter, + enum_map::Enum, )] #[allow(non_camel_case_types)] pub enum ExtCosts { @@ -665,7 +443,6 @@ pub enum ExtCosts { Debug, PartialOrd, Ord, - EnumCount, Display, strum::EnumIter, enum_map::Enum, @@ -691,71 +468,74 @@ pub enum ActionCosts { impl ExtCosts { pub fn value(self, config: &ExtCostsConfig) -> Gas { - use ExtCosts::*; + config.cost(self) + } + + pub fn param(&self) -> Parameter { match self { - base => config.base, - contract_loading_base => config.contract_loading_base, - contract_loading_bytes => config.contract_loading_bytes, - read_memory_base => config.read_memory_base, - read_memory_byte => config.read_memory_byte, - write_memory_base => config.write_memory_base, - write_memory_byte => config.write_memory_byte, - read_register_base => config.read_register_base, - read_register_byte => config.read_register_byte, - write_register_base => config.write_register_base, - write_register_byte => config.write_register_byte, - utf8_decoding_base => config.utf8_decoding_base, - utf8_decoding_byte => config.utf8_decoding_byte, - utf16_decoding_base => config.utf16_decoding_base, - utf16_decoding_byte => config.utf16_decoding_byte, - sha256_base => config.sha256_base, - sha256_byte => config.sha256_byte, - keccak256_base => config.keccak256_base, - keccak256_byte => config.keccak256_byte, - keccak512_base => config.keccak512_base, - keccak512_byte => config.keccak512_byte, - ripemd160_base => config.ripemd160_base, - ripemd160_block => config.ripemd160_block, + ExtCosts::base => Parameter::WasmBase, + ExtCosts::contract_loading_base => Parameter::WasmContractLoadingBase, + ExtCosts::contract_loading_bytes => Parameter::WasmContractLoadingBytes, + ExtCosts::read_memory_base => Parameter::WasmReadMemoryBase, + ExtCosts::read_memory_byte => Parameter::WasmReadMemoryByte, + ExtCosts::write_memory_base => Parameter::WasmWriteMemoryBase, + ExtCosts::write_memory_byte => Parameter::WasmWriteMemoryByte, + ExtCosts::read_register_base => Parameter::WasmReadRegisterBase, + ExtCosts::read_register_byte => Parameter::WasmReadRegisterByte, + ExtCosts::write_register_base => Parameter::WasmWriteRegisterBase, + ExtCosts::write_register_byte => Parameter::WasmWriteRegisterByte, + ExtCosts::utf8_decoding_base => Parameter::WasmUtf8DecodingBase, + ExtCosts::utf8_decoding_byte => Parameter::WasmUtf8DecodingByte, + ExtCosts::utf16_decoding_base => Parameter::WasmUtf16DecodingBase, + ExtCosts::utf16_decoding_byte => Parameter::WasmUtf16DecodingByte, + ExtCosts::sha256_base => Parameter::WasmSha256Base, + ExtCosts::sha256_byte => Parameter::WasmSha256Byte, + ExtCosts::keccak256_base => Parameter::WasmKeccak256Base, + ExtCosts::keccak256_byte => Parameter::WasmKeccak256Byte, + ExtCosts::keccak512_base => Parameter::WasmKeccak512Base, + ExtCosts::keccak512_byte => Parameter::WasmKeccak512Byte, + ExtCosts::ripemd160_base => Parameter::WasmRipemd160Base, + ExtCosts::ripemd160_block => Parameter::WasmRipemd160Block, + ExtCosts::ecrecover_base => Parameter::WasmEcrecoverBase, #[cfg(feature = "protocol_feature_ed25519_verify")] - ed25519_verify_base => config.ed25519_verify_base, + ExtCosts::ed25519_verify_base => Parameter::WasmEd25519VerifyBase, #[cfg(feature = "protocol_feature_ed25519_verify")] - ed25519_verify_byte => config.ed25519_verify_byte, - ecrecover_base => config.ecrecover_base, - log_base => config.log_base, - log_byte => config.log_byte, - storage_write_base => config.storage_write_base, - storage_write_key_byte => config.storage_write_key_byte, - storage_write_value_byte => config.storage_write_value_byte, - storage_write_evicted_byte => config.storage_write_evicted_byte, - storage_read_base => config.storage_read_base, - storage_read_key_byte => config.storage_read_key_byte, - storage_read_value_byte => config.storage_read_value_byte, - storage_remove_base => config.storage_remove_base, - storage_remove_key_byte => config.storage_remove_key_byte, - storage_remove_ret_value_byte => config.storage_remove_ret_value_byte, - storage_has_key_base => config.storage_has_key_base, - storage_has_key_byte => config.storage_has_key_byte, - storage_iter_create_prefix_base => config.storage_iter_create_prefix_base, - storage_iter_create_prefix_byte => config.storage_iter_create_prefix_byte, - storage_iter_create_range_base => config.storage_iter_create_range_base, - storage_iter_create_from_byte => config.storage_iter_create_from_byte, - storage_iter_create_to_byte => config.storage_iter_create_to_byte, - storage_iter_next_base => config.storage_iter_next_base, - storage_iter_next_key_byte => config.storage_iter_next_key_byte, - storage_iter_next_value_byte => config.storage_iter_next_value_byte, - touching_trie_node => config.touching_trie_node, - read_cached_trie_node => config.read_cached_trie_node, - promise_and_base => config.promise_and_base, - promise_and_per_promise => config.promise_and_per_promise, - promise_return => config.promise_return, - validator_stake_base => config.validator_stake_base, - validator_total_stake_base => config.validator_total_stake_base, - alt_bn128_g1_multiexp_base => config.alt_bn128_g1_multiexp_base, - alt_bn128_g1_multiexp_element => config.alt_bn128_g1_multiexp_element, - alt_bn128_pairing_check_base => config.alt_bn128_pairing_check_base, - alt_bn128_pairing_check_element => config.alt_bn128_pairing_check_element, - alt_bn128_g1_sum_base => config.alt_bn128_g1_sum_base, - alt_bn128_g1_sum_element => config.alt_bn128_g1_sum_element, + ExtCosts::ed25519_verify_byte => Parameter::WasmEd25519VerifyByte, + ExtCosts::log_base => Parameter::WasmLogBase, + ExtCosts::log_byte => Parameter::WasmLogByte, + ExtCosts::storage_write_base => Parameter::WasmStorageWriteBase, + ExtCosts::storage_write_key_byte => Parameter::WasmStorageWriteKeyByte, + ExtCosts::storage_write_value_byte => Parameter::WasmStorageWriteValueByte, + ExtCosts::storage_write_evicted_byte => Parameter::WasmStorageWriteEvictedByte, + ExtCosts::storage_read_base => Parameter::WasmStorageReadBase, + ExtCosts::storage_read_key_byte => Parameter::WasmStorageReadKeyByte, + ExtCosts::storage_read_value_byte => Parameter::WasmStorageReadValueByte, + ExtCosts::storage_remove_base => Parameter::WasmStorageRemoveBase, + ExtCosts::storage_remove_key_byte => Parameter::WasmStorageRemoveKeyByte, + ExtCosts::storage_remove_ret_value_byte => Parameter::WasmStorageRemoveRetValueByte, + ExtCosts::storage_has_key_base => Parameter::WasmStorageHasKeyBase, + ExtCosts::storage_has_key_byte => Parameter::WasmStorageHasKeyByte, + ExtCosts::storage_iter_create_prefix_base => Parameter::WasmStorageIterCreatePrefixBase, + ExtCosts::storage_iter_create_prefix_byte => Parameter::WasmStorageIterCreatePrefixByte, + ExtCosts::storage_iter_create_range_base => Parameter::WasmStorageIterCreateRangeBase, + ExtCosts::storage_iter_create_from_byte => Parameter::WasmStorageIterCreateFromByte, + ExtCosts::storage_iter_create_to_byte => Parameter::WasmStorageIterCreateToByte, + ExtCosts::storage_iter_next_base => Parameter::WasmStorageIterNextBase, + ExtCosts::storage_iter_next_key_byte => Parameter::WasmStorageIterNextKeyByte, + ExtCosts::storage_iter_next_value_byte => Parameter::WasmStorageIterNextValueByte, + ExtCosts::touching_trie_node => Parameter::WasmTouchingTrieNode, + ExtCosts::read_cached_trie_node => Parameter::WasmReadCachedTrieNode, + ExtCosts::promise_and_base => Parameter::WasmPromiseAndBase, + ExtCosts::promise_and_per_promise => Parameter::WasmPromiseAndPerPromise, + ExtCosts::promise_return => Parameter::WasmPromiseReturn, + ExtCosts::validator_stake_base => Parameter::WasmValidatorStakeBase, + ExtCosts::validator_total_stake_base => Parameter::WasmValidatorTotalStakeBase, + ExtCosts::alt_bn128_g1_multiexp_base => Parameter::WasmAltBn128G1MultiexpBase, + ExtCosts::alt_bn128_g1_multiexp_element => Parameter::WasmAltBn128G1MultiexpElement, + ExtCosts::alt_bn128_pairing_check_base => Parameter::WasmAltBn128PairingCheckBase, + ExtCosts::alt_bn128_pairing_check_element => Parameter::WasmAltBn128PairingCheckElement, + ExtCosts::alt_bn128_g1_sum_base => Parameter::WasmAltBn128G1SumBase, + ExtCosts::alt_bn128_g1_sum_element => Parameter::WasmAltBn128G1SumElement, } } } diff --git a/core/primitives-core/src/parameter.rs b/core/primitives-core/src/parameter.rs index 872f3575fec..547dbe4900b 100644 --- a/core/primitives-core/src/parameter.rs +++ b/core/primitives-core/src/parameter.rs @@ -210,77 +210,6 @@ pub enum FeeParameter { } impl Parameter { - /// Iterate through all parameters that define external gas costs that may - /// be charged during WASM execution. These are essentially all costs from - /// host function calls. Note that the gas cost for regular WASM operation - /// is treated separately and therefore not included in this list. - pub fn ext_costs() -> slice::Iter<'static, Parameter> { - [ - Parameter::WasmBase, - Parameter::WasmContractLoadingBase, - Parameter::WasmContractLoadingBytes, - Parameter::WasmReadMemoryBase, - Parameter::WasmReadMemoryByte, - Parameter::WasmWriteMemoryBase, - Parameter::WasmWriteMemoryByte, - Parameter::WasmReadRegisterBase, - Parameter::WasmReadRegisterByte, - Parameter::WasmWriteRegisterBase, - Parameter::WasmWriteRegisterByte, - Parameter::WasmUtf8DecodingBase, - Parameter::WasmUtf8DecodingByte, - Parameter::WasmUtf16DecodingBase, - Parameter::WasmUtf16DecodingByte, - Parameter::WasmSha256Base, - Parameter::WasmSha256Byte, - Parameter::WasmKeccak256Base, - Parameter::WasmKeccak256Byte, - Parameter::WasmKeccak512Base, - Parameter::WasmKeccak512Byte, - Parameter::WasmRipemd160Base, - Parameter::WasmRipemd160Block, - Parameter::WasmEcrecoverBase, - Parameter::WasmEd25519VerifyBase, - Parameter::WasmEd25519VerifyByte, - Parameter::WasmLogBase, - Parameter::WasmLogByte, - Parameter::WasmStorageWriteBase, - Parameter::WasmStorageWriteKeyByte, - Parameter::WasmStorageWriteValueByte, - Parameter::WasmStorageWriteEvictedByte, - Parameter::WasmStorageReadBase, - Parameter::WasmStorageReadKeyByte, - Parameter::WasmStorageReadValueByte, - Parameter::WasmStorageRemoveBase, - Parameter::WasmStorageRemoveKeyByte, - Parameter::WasmStorageRemoveRetValueByte, - Parameter::WasmStorageHasKeyBase, - Parameter::WasmStorageHasKeyByte, - Parameter::WasmStorageIterCreatePrefixBase, - Parameter::WasmStorageIterCreatePrefixByte, - Parameter::WasmStorageIterCreateRangeBase, - Parameter::WasmStorageIterCreateFromByte, - Parameter::WasmStorageIterCreateToByte, - Parameter::WasmStorageIterNextBase, - Parameter::WasmStorageIterNextKeyByte, - Parameter::WasmStorageIterNextValueByte, - Parameter::WasmTouchingTrieNode, - Parameter::WasmReadCachedTrieNode, - Parameter::WasmPromiseAndBase, - Parameter::WasmPromiseAndPerPromise, - Parameter::WasmPromiseReturn, - Parameter::WasmValidatorStakeBase, - Parameter::WasmValidatorTotalStakeBase, - Parameter::WasmAltBn128G1MultiexpBase, - Parameter::WasmAltBn128G1MultiexpElement, - Parameter::WasmAltBn128PairingCheckBase, - Parameter::WasmAltBn128PairingCheckElement, - Parameter::WasmAltBn128G1SumBase, - Parameter::WasmAltBn128G1SumElement, - ] - .iter() - } - /// Iterate through all parameters that define numerical limits for /// contracts that are executed in the WASM VM. pub fn vm_limits() -> slice::Iter<'static, Parameter> { diff --git a/core/primitives/res/runtime_configs/53.txt b/core/primitives/res/runtime_configs/53.txt index 1b2f0ce84f9..13bd07041f3 100644 --- a/core/primitives/res/runtime_configs/53.txt +++ b/core/primitives/res/runtime_configs/53.txt @@ -1,5 +1,4 @@ action_deploy_contract_per_byte_execution: 6_812_999 -> 64_572_944 -wasm_read_cached_trie_node: 2_280_000_000 wasmer2_stack_limit: 204_800 max_length_storage_key: 4_194_304 -> 2_048 max_locals_per_contract: 1_000_000 diff --git a/core/primitives/res/runtime_configs/parameters.txt b/core/primitives/res/runtime_configs/parameters.txt index ceeb3ac3333..c4ab8633ab6 100644 --- a/core/primitives/res/runtime_configs/parameters.txt +++ b/core/primitives/res/runtime_configs/parameters.txt @@ -119,6 +119,7 @@ wasm_storage_iter_next_base: 0 wasm_storage_iter_next_key_byte: 0 wasm_storage_iter_next_value_byte: 0 wasm_touching_trie_node: 16_101_955_926 +wasm_read_cached_trie_node: 2_280_000_000 wasm_promise_and_base: 1_465_013_400 wasm_promise_and_per_promise: 5_452_176 wasm_promise_return: 560_152_386 diff --git a/core/primitives/res/runtime_configs/parameters_testnet.txt b/core/primitives/res/runtime_configs/parameters_testnet.txt index 0cad1863c3b..158cce82351 100644 --- a/core/primitives/res/runtime_configs/parameters_testnet.txt +++ b/core/primitives/res/runtime_configs/parameters_testnet.txt @@ -115,6 +115,7 @@ wasm_storage_iter_next_base: 0 wasm_storage_iter_next_key_byte: 0 wasm_storage_iter_next_value_byte: 0 wasm_touching_trie_node: 16_101_955_926 +wasm_read_cached_trie_node: 2_280_000_000 wasm_promise_and_base: 1_465_013_400 wasm_promise_and_per_promise: 5_452_176 wasm_promise_return: 560_152_386 diff --git a/core/primitives/src/runtime/config_store.rs b/core/primitives/src/runtime/config_store.rs index 7b337c82eba..46bdc750095 100644 --- a/core/primitives/src/runtime/config_store.rs +++ b/core/primitives/src/runtime/config_store.rs @@ -109,7 +109,7 @@ mod tests { use crate::version::ProtocolFeature::{ LowerDataReceiptAndEcrecoverBaseCost, LowerStorageCost, LowerStorageKeyLimit, }; - use near_primitives_core::config::ActionCosts; + use near_primitives_core::config::{ActionCosts, ExtCosts}; const GENESIS_PROTOCOL_VERSION: ProtocolVersion = 29; const RECEIPTS_DEPTH: u64 = 63; @@ -212,8 +212,8 @@ mod tests { let base_cfg = store.get_config(LowerStorageCost.protocol_version()); let new_cfg = store.get_config(LowerDataReceiptAndEcrecoverBaseCost.protocol_version()); assert!( - base_cfg.wasm_config.ext_costs.ecrecover_base - > new_cfg.wasm_config.ext_costs.ecrecover_base + base_cfg.wasm_config.ext_costs.cost(ExtCosts::ecrecover_base) + > new_cfg.wasm_config.ext_costs.cost(ExtCosts::ecrecover_base) ); } diff --git a/core/primitives/src/runtime/parameter_table.rs b/core/primitives/src/runtime/parameter_table.rs index 50372ed6fff..d040df0d4be 100644 --- a/core/primitives/src/runtime/parameter_table.rs +++ b/core/primitives/src/runtime/parameter_table.rs @@ -1,5 +1,5 @@ use super::config::{AccountCreationConfig, RuntimeConfig}; -use near_primitives_core::config::VMConfig; +use near_primitives_core::config::{ExtCostsConfig, VMConfig}; use near_primitives_core::parameter::{FeeParameter, Parameter}; use near_primitives_core::runtime::fees::{RuntimeFeesConfig, StorageUsageConfig}; use num_rational::Rational; @@ -81,8 +81,11 @@ impl TryFrom<&ParameterTable> for RuntimeConfig { }, }, wasm_config: VMConfig { - ext_costs: serde_json::from_value(params.json_map(Parameter::ext_costs(), "wasm_")) - .map_err(InvalidConfigError::WrongStructure)?, + ext_costs: ExtCostsConfig { + costs: enum_map::enum_map! { + cost => params.get_parsed(cost.param())? + }, + }, grow_mem_cost: params.get_parsed(Parameter::WasmGrowMemCost)?, regular_op_cost: params.get_parsed(Parameter::WasmRegularOpCost)?, limit_config: serde_json::from_value(params.json_map(Parameter::vm_limits(), "")) diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index ef1bf9dc328..8870f6a4859 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use borsh::{BorshDeserialize, BorshSerialize}; use chrono::DateTime; -use near_primitives_core::config::{ActionCosts, VMConfig}; +use near_primitives_core::config::{ActionCosts, ExtCosts, VMConfig}; use near_primitives_core::runtime::fees::Fee; use num_rational::Rational; use serde::{Deserialize, Serialize}; @@ -1995,10 +1995,7 @@ pub struct RuntimeConfigView { /// processing transaction and receipts. pub transaction_costs: RuntimeFeesConfigView, /// Config of wasm operations. - /// - /// TODO: This should be refactored to `VMConfigView` to detach the config - /// format from RPC output. - pub wasm_config: VMConfig, + pub wasm_config: VMConfigView, /// Config that defines rules for account creation. pub account_creation_config: AccountCreationConfigView, } @@ -2157,7 +2154,7 @@ impl From for RuntimeConfigView { .fees .pessimistic_gas_price_inflation_ratio, }, - wasm_config: config.wasm_config, + wasm_config: VMConfigView::from(config.wasm_config), account_creation_config: AccountCreationConfigView { min_allowed_top_level_account_length: config .account_creation_config @@ -2207,7 +2204,7 @@ impl From for RuntimeConfig { }, }, - wasm_config: config.wasm_config, + wasm_config: VMConfig::from(config.wasm_config), account_creation_config: crate::runtime::config::AccountCreationConfig { min_allowed_top_level_account_length: config .account_creation_config @@ -2217,3 +2214,357 @@ impl From for RuntimeConfig { } } } + +#[derive(Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq)] +pub struct VMConfigView { + /// Costs for runtime externals + pub ext_costs: ExtCostsConfigView, + + /// 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. + /// + /// TODO: Consider changing this to `VMLimitConfigView` to avoid dependency + /// on runtime. + pub limit_config: near_primitives_core::config::VMLimitConfig, +} + +impl From for VMConfigView { + fn from(config: VMConfig) -> Self { + Self { + ext_costs: ExtCostsConfigView::from(config.ext_costs), + grow_mem_cost: config.grow_mem_cost, + regular_op_cost: config.regular_op_cost, + limit_config: config.limit_config, + } + } +} + +impl From for VMConfig { + 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, + limit_config: view.limit_config, + } + } +} + +/// Typed view of ExtCostsConfig t preserve JSON output field names in protocol +/// config RPC output. +#[derive(Debug, Serialize, Deserialize, Clone, Hash, PartialEq, Eq)] +pub struct ExtCostsConfigView { + /// Base cost for calling a host function. + pub base: Gas, + + /// Base cost of loading a pre-compiled contract + pub contract_loading_base: Gas, + /// Cost per byte of loading a pre-compiled contract + pub contract_loading_bytes: Gas, + + /// Base cost for guest memory read + pub read_memory_base: Gas, + /// Cost for guest memory read + pub read_memory_byte: Gas, + + /// Base cost for guest memory write + pub write_memory_base: Gas, + /// Cost for guest memory write per byte + pub write_memory_byte: Gas, + + /// Base cost for reading from register + pub read_register_base: Gas, + /// Cost for reading byte from register + pub read_register_byte: Gas, + + /// Base cost for writing into register + pub write_register_base: Gas, + /// Cost for writing byte into register + pub write_register_byte: Gas, + + /// Base cost of decoding utf8. It's used for `log_utf8` and `panic_utf8`. + pub utf8_decoding_base: Gas, + /// Cost per byte of decoding utf8. It's used for `log_utf8` and `panic_utf8`. + pub utf8_decoding_byte: Gas, + + /// Base cost of decoding utf16. It's used for `log_utf16`. + pub utf16_decoding_base: Gas, + /// Cost per byte of decoding utf16. It's used for `log_utf16`. + pub utf16_decoding_byte: Gas, + + /// Cost of getting sha256 base + pub sha256_base: Gas, + /// Cost of getting sha256 per byte + pub sha256_byte: Gas, + + /// Cost of getting sha256 base + pub keccak256_base: Gas, + /// Cost of getting sha256 per byte + pub keccak256_byte: Gas, + + /// Cost of getting sha256 base + pub keccak512_base: Gas, + /// Cost of getting sha256 per byte + pub keccak512_byte: Gas, + + /// Cost of getting ripemd160 base + pub ripemd160_base: Gas, + /// Cost of getting ripemd160 per message block + pub ripemd160_block: Gas, + + /// Cost of getting ed25519 base + #[cfg(feature = "protocol_feature_ed25519_verify")] + pub ed25519_verify_base: Gas, + /// Cost of getting ed25519 per byte + #[cfg(feature = "protocol_feature_ed25519_verify")] + pub ed25519_verify_byte: Gas, + + /// Cost of calling ecrecover + pub ecrecover_base: Gas, + + /// Cost for calling logging. + pub log_base: Gas, + /// Cost for logging per byte + pub log_byte: Gas, + + // ############### + // # Storage API # + // ############### + /// Storage trie write key base cost + pub storage_write_base: Gas, + /// Storage trie write key per byte cost + pub storage_write_key_byte: Gas, + /// Storage trie write value per byte cost + pub storage_write_value_byte: Gas, + /// Storage trie write cost per byte of evicted value. + pub storage_write_evicted_byte: Gas, + + /// Storage trie read key base cost + pub storage_read_base: Gas, + /// Storage trie read key per byte cost + pub storage_read_key_byte: Gas, + /// Storage trie read value cost per byte cost + pub storage_read_value_byte: Gas, + + /// Remove key from trie base cost + pub storage_remove_base: Gas, + /// Remove key from trie per byte cost + pub storage_remove_key_byte: Gas, + /// Remove key from trie ret value byte cost + pub storage_remove_ret_value_byte: Gas, + + /// Storage trie check for key existence cost base + pub storage_has_key_base: Gas, + /// Storage trie check for key existence per key byte + pub storage_has_key_byte: Gas, + + /// Create trie prefix iterator cost base + pub storage_iter_create_prefix_base: Gas, + /// Create trie prefix iterator cost per byte. + pub storage_iter_create_prefix_byte: Gas, + + /// Create trie range iterator cost base + pub storage_iter_create_range_base: Gas, + /// Create trie range iterator cost per byte of from key. + pub storage_iter_create_from_byte: Gas, + /// Create trie range iterator cost per byte of to key. + pub storage_iter_create_to_byte: Gas, + + /// Trie iterator per key base cost + pub storage_iter_next_base: Gas, + /// Trie iterator next key byte cost + pub storage_iter_next_key_byte: Gas, + /// Trie iterator next key byte cost + pub storage_iter_next_value_byte: Gas, + + /// Cost per reading trie node from DB + pub touching_trie_node: Gas, + /// Cost for reading trie node from memory + pub read_cached_trie_node: Gas, + + // ############### + // # Promise API # + // ############### + /// Cost for calling `promise_and` + pub promise_and_base: Gas, + /// Cost for calling `promise_and` for each promise + pub promise_and_per_promise: Gas, + /// Cost for calling `promise_return` + pub promise_return: Gas, + + // ############### + // # Validator API # + // ############### + /// Cost of calling `validator_stake`. + pub validator_stake_base: Gas, + /// Cost of calling `validator_total_stake`. + pub validator_total_stake_base: Gas, + + // Removed parameters, only here for keeping the output backward-compatible. + pub contract_compile_base: Gas, + pub contract_compile_bytes: Gas, + + // ############# + // # Alt BN128 # + // ############# + /// Base cost for multiexp + pub alt_bn128_g1_multiexp_base: Gas, + /// Per element cost for multiexp + pub alt_bn128_g1_multiexp_element: Gas, + /// Base cost for sum + pub alt_bn128_g1_sum_base: Gas, + /// Per element cost for sum + pub alt_bn128_g1_sum_element: Gas, + /// Base cost for pairing check + pub alt_bn128_pairing_check_base: Gas, + /// Per element cost for pairing check + pub alt_bn128_pairing_check_element: Gas, +} + +impl From for ExtCostsConfigView { + fn from(config: near_primitives_core::config::ExtCostsConfig) -> Self { + Self { + base: config.cost(ExtCosts::base), + contract_loading_base: config.cost(ExtCosts::contract_loading_base), + contract_loading_bytes: config.cost(ExtCosts::contract_loading_bytes), + read_memory_base: config.cost(ExtCosts::read_memory_base), + read_memory_byte: config.cost(ExtCosts::read_memory_byte), + write_memory_base: config.cost(ExtCosts::write_memory_base), + write_memory_byte: config.cost(ExtCosts::write_memory_byte), + read_register_base: config.cost(ExtCosts::read_register_base), + read_register_byte: config.cost(ExtCosts::read_register_byte), + write_register_base: config.cost(ExtCosts::write_register_base), + write_register_byte: config.cost(ExtCosts::write_register_byte), + utf8_decoding_base: config.cost(ExtCosts::utf8_decoding_base), + utf8_decoding_byte: config.cost(ExtCosts::utf8_decoding_byte), + utf16_decoding_base: config.cost(ExtCosts::utf16_decoding_base), + utf16_decoding_byte: config.cost(ExtCosts::utf16_decoding_byte), + sha256_base: config.cost(ExtCosts::sha256_base), + sha256_byte: config.cost(ExtCosts::sha256_byte), + keccak256_base: config.cost(ExtCosts::keccak256_base), + keccak256_byte: config.cost(ExtCosts::keccak256_byte), + keccak512_base: config.cost(ExtCosts::keccak512_base), + keccak512_byte: config.cost(ExtCosts::keccak512_byte), + ripemd160_base: config.cost(ExtCosts::ripemd160_base), + ripemd160_block: config.cost(ExtCosts::ripemd160_block), + #[cfg(feature = "protocol_feature_ed25519_verify")] + ed25519_verify_base: config.cost(ExtCosts::ed25519_verify_base), + #[cfg(feature = "protocol_feature_ed25519_verify")] + ed25519_verify_byte: config.cost(ExtCosts::ed25519_verify_byte), + ecrecover_base: config.cost(ExtCosts::ecrecover_base), + log_base: config.cost(ExtCosts::log_base), + log_byte: config.cost(ExtCosts::log_byte), + storage_write_base: config.cost(ExtCosts::storage_write_base), + storage_write_key_byte: config.cost(ExtCosts::storage_write_key_byte), + storage_write_value_byte: config.cost(ExtCosts::storage_write_value_byte), + storage_write_evicted_byte: config.cost(ExtCosts::storage_write_evicted_byte), + storage_read_base: config.cost(ExtCosts::storage_read_base), + storage_read_key_byte: config.cost(ExtCosts::storage_read_key_byte), + storage_read_value_byte: config.cost(ExtCosts::storage_read_value_byte), + storage_remove_base: config.cost(ExtCosts::storage_remove_base), + storage_remove_key_byte: config.cost(ExtCosts::storage_remove_key_byte), + storage_remove_ret_value_byte: config.cost(ExtCosts::storage_remove_ret_value_byte), + storage_has_key_base: config.cost(ExtCosts::storage_has_key_base), + storage_has_key_byte: config.cost(ExtCosts::storage_has_key_byte), + storage_iter_create_prefix_base: config.cost(ExtCosts::storage_iter_create_prefix_base), + storage_iter_create_prefix_byte: config.cost(ExtCosts::storage_iter_create_prefix_byte), + storage_iter_create_range_base: config.cost(ExtCosts::storage_iter_create_range_base), + storage_iter_create_from_byte: config.cost(ExtCosts::storage_iter_create_from_byte), + storage_iter_create_to_byte: config.cost(ExtCosts::storage_iter_create_to_byte), + storage_iter_next_base: config.cost(ExtCosts::storage_iter_next_base), + storage_iter_next_key_byte: config.cost(ExtCosts::storage_iter_next_key_byte), + storage_iter_next_value_byte: config.cost(ExtCosts::storage_iter_next_value_byte), + touching_trie_node: config.cost(ExtCosts::touching_trie_node), + read_cached_trie_node: config.cost(ExtCosts::read_cached_trie_node), + promise_and_base: config.cost(ExtCosts::promise_and_base), + promise_and_per_promise: config.cost(ExtCosts::promise_and_per_promise), + promise_return: config.cost(ExtCosts::promise_return), + validator_stake_base: config.cost(ExtCosts::validator_stake_base), + validator_total_stake_base: config.cost(ExtCosts::validator_total_stake_base), + alt_bn128_g1_multiexp_base: config.cost(ExtCosts::alt_bn128_g1_multiexp_base), + alt_bn128_g1_multiexp_element: config.cost(ExtCosts::alt_bn128_g1_multiexp_element), + alt_bn128_g1_sum_base: config.cost(ExtCosts::alt_bn128_g1_sum_base), + alt_bn128_g1_sum_element: config.cost(ExtCosts::alt_bn128_g1_sum_element), + alt_bn128_pairing_check_base: config.cost(ExtCosts::alt_bn128_pairing_check_base), + alt_bn128_pairing_check_element: config.cost(ExtCosts::alt_bn128_pairing_check_element), + // removed parameters + contract_compile_base: 0, + contract_compile_bytes: 0, + } + } +} + +impl From for near_primitives_core::config::ExtCostsConfig { + fn from(view: ExtCostsConfigView) -> Self { + let costs = enum_map::enum_map! { + ExtCosts::base => view.base, + ExtCosts::contract_loading_base => view.contract_loading_base, + ExtCosts::contract_loading_bytes => view.contract_loading_bytes, + ExtCosts::read_memory_base => view.read_memory_base, + ExtCosts::read_memory_byte => view.read_memory_byte, + ExtCosts::write_memory_base => view.write_memory_base, + ExtCosts::write_memory_byte => view.write_memory_byte, + ExtCosts::read_register_base => view.read_register_base, + ExtCosts::read_register_byte => view.read_register_byte, + ExtCosts::write_register_base => view.write_register_base, + ExtCosts::write_register_byte => view.write_register_byte, + ExtCosts::utf8_decoding_base => view.utf8_decoding_base, + ExtCosts::utf8_decoding_byte => view.utf8_decoding_byte, + ExtCosts::utf16_decoding_base => view.utf16_decoding_base, + ExtCosts::utf16_decoding_byte => view.utf16_decoding_byte, + ExtCosts::sha256_base => view.sha256_base, + ExtCosts::sha256_byte => view.sha256_byte, + ExtCosts::keccak256_base => view.keccak256_base, + ExtCosts::keccak256_byte => view.keccak256_byte, + ExtCosts::keccak512_base => view.keccak512_base, + ExtCosts::keccak512_byte => view.keccak512_byte, + ExtCosts::ripemd160_base => view.ripemd160_base, + ExtCosts::ripemd160_block => view.ripemd160_block, + #[cfg(feature = "protocol_feature_ed25519_verify")] + ExtCosts::ed25519_verify_base => view.ed25519_verify_base, + #[cfg(feature = "protocol_feature_ed25519_verify")] + ExtCosts::ed25519_verify_byte => view.ed25519_verify_byte, + ExtCosts::ecrecover_base => view.ecrecover_base, + ExtCosts::log_base => view.log_base, + ExtCosts::log_byte => view.log_byte, + ExtCosts::storage_write_base => view.storage_write_base, + ExtCosts::storage_write_key_byte => view.storage_write_key_byte, + ExtCosts::storage_write_value_byte => view.storage_write_value_byte, + ExtCosts::storage_write_evicted_byte => view.storage_write_evicted_byte, + ExtCosts::storage_read_base => view.storage_read_base, + ExtCosts::storage_read_key_byte => view.storage_read_key_byte, + ExtCosts::storage_read_value_byte => view.storage_read_value_byte, + ExtCosts::storage_remove_base => view.storage_remove_base, + ExtCosts::storage_remove_key_byte => view.storage_remove_key_byte, + ExtCosts::storage_remove_ret_value_byte => view.storage_remove_ret_value_byte, + ExtCosts::storage_has_key_base => view.storage_has_key_base, + ExtCosts::storage_has_key_byte => view.storage_has_key_byte, + ExtCosts::storage_iter_create_prefix_base => view.storage_iter_create_prefix_base, + ExtCosts::storage_iter_create_prefix_byte => view.storage_iter_create_prefix_byte, + ExtCosts::storage_iter_create_range_base => view.storage_iter_create_range_base, + ExtCosts::storage_iter_create_from_byte => view.storage_iter_create_from_byte, + ExtCosts::storage_iter_create_to_byte => view.storage_iter_create_to_byte, + ExtCosts::storage_iter_next_base => view.storage_iter_next_base, + ExtCosts::storage_iter_next_key_byte => view.storage_iter_next_key_byte, + ExtCosts::storage_iter_next_value_byte => view.storage_iter_next_value_byte, + ExtCosts::touching_trie_node => view.touching_trie_node, + ExtCosts::read_cached_trie_node => view.read_cached_trie_node, + ExtCosts::promise_and_base => view.promise_and_base, + ExtCosts::promise_and_per_promise => view.promise_and_per_promise, + ExtCosts::promise_return => view.promise_return, + ExtCosts::validator_stake_base => view.validator_stake_base, + ExtCosts::validator_total_stake_base => view.validator_total_stake_base, + ExtCosts::alt_bn128_g1_multiexp_base => view.alt_bn128_g1_multiexp_base, + ExtCosts::alt_bn128_g1_multiexp_element => view.alt_bn128_g1_multiexp_element, + ExtCosts::alt_bn128_g1_sum_base => view.alt_bn128_g1_sum_base, + ExtCosts::alt_bn128_g1_sum_element => view.alt_bn128_g1_sum_element, + ExtCosts::alt_bn128_pairing_check_base => view.alt_bn128_pairing_check_base, + ExtCosts::alt_bn128_pairing_check_element => view.alt_bn128_pairing_check_element, + }; + Self { costs } + } +} diff --git a/integration-tests/src/tests/client/process_blocks.rs b/integration-tests/src/tests/client/process_blocks.rs index 74d525f961f..d5bdaf5078d 100644 --- a/integration-tests/src/tests/client/process_blocks.rs +++ b/integration-tests/src/tests/client/process_blocks.rs @@ -9,7 +9,7 @@ use assert_matches::assert_matches; use futures::{future, FutureExt}; use near_chain::test_utils::ValidatorSchedule; use near_chunks::test_utils::MockClientAdapterForShardsManager; -use near_primitives::config::ActionCosts; +use near_primitives::config::{ActionCosts, ExtCosts}; use near_primitives::num_rational::{Ratio, Rational32}; use near_actix_test_utils::run_actix; @@ -2667,13 +2667,13 @@ fn test_execution_metadata() { { "cost_category": "WASM_HOST_COST", "cost": "BASE", - "gas_used": config.wasm_config.ext_costs.base.to_string() + "gas_used": config.wasm_config.ext_costs.cost(ExtCosts::base).to_string() }, // We include compilation costs into running the function. { "cost_category": "WASM_HOST_COST", "cost": "CONTRACT_LOADING_BASE", - "gas_used": config.wasm_config.ext_costs.contract_loading_base.to_string() + "gas_used": config.wasm_config.ext_costs.cost(ExtCosts::contract_loading_base).to_string() }, { "cost_category": "WASM_HOST_COST", diff --git a/integration-tests/src/tests/runtime/sanity_checks.rs b/integration-tests/src/tests/runtime/sanity_checks.rs index 4d2a9183858..2eaa7a30933 100644 --- a/integration-tests/src/tests/runtime/sanity_checks.rs +++ b/integration-tests/src/tests/runtime/sanity_checks.rs @@ -1,5 +1,6 @@ use crate::node::{Node, RuntimeNode}; use near_chain_configs::Genesis; +use near_primitives::config::ExtCosts; use near_primitives::runtime::config::RuntimeConfig; use near_primitives::runtime::config_store::RuntimeConfigStore; use near_primitives::serialize::to_base64; @@ -232,7 +233,8 @@ fn test_sanity_used_gas() { // Executing `used_gas` costs `base_cost`. When executing `used_gas` twice // within a metered block, the returned values should differ by that amount. - let base_cost = node.client.read().unwrap().runtime_config.wasm_config.ext_costs.base; + let base_cost = + node.client.read().unwrap().runtime_config.wasm_config.ext_costs.cost(ExtCosts::base); assert_eq!(used_gas[1] - used_gas[0], base_cost); // The fees for executing a metered block's WASM code should be paid before diff --git a/integration-tests/src/tests/standard_cases/mod.rs b/integration-tests/src/tests/standard_cases/mod.rs index 3d54381d98c..314751fb02e 100644 --- a/integration-tests/src/tests/standard_cases/mod.rs +++ b/integration-tests/src/tests/standard_cases/mod.rs @@ -7,7 +7,7 @@ use assert_matches::assert_matches; use near_crypto::{InMemorySigner, KeyType}; use near_jsonrpc_primitives::errors::ServerError; use near_primitives::account::{AccessKey, AccessKeyPermission, FunctionCallPermission}; -use near_primitives::config::ActionCosts; +use near_primitives::config::{ActionCosts, ExtCosts}; use near_primitives::errors::{ ActionError, ActionErrorKind, InvalidAccessKeyError, InvalidTxError, TxExecutionError, }; @@ -1347,12 +1347,12 @@ fn get_trie_nodes_count( for cost in metadata.gas_profile.clone().unwrap_or_default().iter() { match cost.cost.as_str() { "TOUCHING_TRIE_NODE" => { - count.db_reads += - cost.gas_used / runtime_config.wasm_config.ext_costs.touching_trie_node; + count.db_reads += cost.gas_used + / runtime_config.wasm_config.ext_costs.cost(ExtCosts::touching_trie_node); } "READ_CACHED_TRIE_NODE" => { - count.mem_reads += - cost.gas_used / runtime_config.wasm_config.ext_costs.read_cached_trie_node; + count.mem_reads += cost.gas_used + / runtime_config.wasm_config.ext_costs.cost(ExtCosts::read_cached_trie_node); } _ => {} }; diff --git a/nearcore/src/runtime/mod.rs b/nearcore/src/runtime/mod.rs index 70166c3737a..443743527b7 100644 --- a/nearcore/src/runtime/mod.rs +++ b/nearcore/src/runtime/mod.rs @@ -18,6 +18,7 @@ use near_o11y::log_assert; use near_pool::types::PoolIterator; use near_primitives::account::{AccessKey, Account}; use near_primitives::challenge::ChallengesResult; +use near_primitives::config::ExtCosts; use near_primitives::contract::ContractCode; use near_primitives::epoch_manager::block_info::BlockInfo; use near_primitives::epoch_manager::EpochConfig; @@ -830,8 +831,8 @@ impl RuntimeAdapter for NightshadeRuntime { // of parameters, this corresponds to about 13megs worth of // transactions. let size_limit = transactions_gas_limit - / (runtime_config.wasm_config.ext_costs.storage_write_value_byte - + runtime_config.wasm_config.ext_costs.storage_read_value_byte); + / (runtime_config.wasm_config.ext_costs.cost(ExtCosts::storage_write_value_byte) + + runtime_config.wasm_config.ext_costs.cost(ExtCosts::storage_read_value_byte)); while total_gas_burnt < transactions_gas_limit && total_size < size_limit { if let Some(iter) = pool_iterator.next() { 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 3318afd4a98..91b146ab22e 100644 --- a/runtime/runtime-params-estimator/src/costs_to_runtime_config.rs +++ b/runtime/runtime-params-estimator/src/costs_to_runtime_config.rs @@ -1,9 +1,8 @@ use near_primitives::runtime::config::AccountCreationConfig; use near_primitives::runtime::config_store::RuntimeConfigStore; use near_primitives::runtime::fees::{Fee, RuntimeFeesConfig}; -use near_primitives::types::Gas; use near_primitives::version::PROTOCOL_VERSION; -use near_vm_logic::{ActionCosts, ExtCostsConfig, VMConfig}; +use near_vm_logic::{ActionCosts, ExtCosts, ExtCostsConfig, VMConfig}; use node_runtime::config::RuntimeConfig; use anyhow::Context; @@ -76,80 +75,82 @@ fn runtime_fees_config(cost_table: &CostTable) -> anyhow::Result anyhow::Result { - let get = |cost: Cost| -> anyhow::Result { - cost_table.get(cost).with_context(|| format!("undefined cost: {}", cost)) - }; + Ok(ExtCostsConfig { + costs: enum_map::enum_map! { + // TODO: storage_iter_* operations below are deprecated, so just hardcode zero price, + // and remove those operations ASAP. + ExtCosts::storage_iter_create_prefix_base => 0, + ExtCosts::storage_iter_create_prefix_byte => 0, + ExtCosts::storage_iter_create_range_base => 0, + ExtCosts::storage_iter_create_from_byte => 0, + ExtCosts::storage_iter_create_to_byte => 0, + ExtCosts::storage_iter_next_base => 0, + ExtCosts::storage_iter_next_key_byte => 0, + ExtCosts::storage_iter_next_value_byte => 0, + // TODO: accurately price host functions that expose validator information. + ExtCosts::validator_stake_base => 303944908800, + ExtCosts::validator_total_stake_base => 303944908800, + cost => { + let estimation = estimation(cost).with_context(|| format!("external WASM cost has no estimation defined: {}", cost))?; + cost_table.get(estimation).with_context(|| format!("undefined external WASM cost: {}", cost))? + }, + }, + }) +} - let res = ExtCostsConfig { - base: get(Cost::HostFunctionCall)?, - contract_loading_base: 0, - contract_loading_bytes: 0, - read_memory_base: get(Cost::ReadMemoryBase)?, - read_memory_byte: get(Cost::ReadMemoryByte)?, - write_memory_base: get(Cost::WriteMemoryBase)?, - write_memory_byte: get(Cost::WriteMemoryByte)?, - read_register_base: get(Cost::ReadRegisterBase)?, - read_register_byte: get(Cost::ReadRegisterByte)?, - write_register_base: get(Cost::WriteRegisterBase)?, - write_register_byte: get(Cost::WriteRegisterByte)?, - utf8_decoding_base: get(Cost::Utf8DecodingBase)?, - utf8_decoding_byte: get(Cost::Utf8DecodingByte)?, - utf16_decoding_base: get(Cost::Utf16DecodingBase)?, - utf16_decoding_byte: get(Cost::Utf16DecodingByte)?, - sha256_base: get(Cost::Sha256Base)?, - sha256_byte: get(Cost::Sha256Byte)?, - keccak256_base: get(Cost::Keccak256Base)?, - keccak256_byte: get(Cost::Keccak256Byte)?, - keccak512_base: get(Cost::Keccak512Base)?, - keccak512_byte: get(Cost::Keccak512Byte)?, - ripemd160_base: get(Cost::Ripemd160Base)?, - ripemd160_block: get(Cost::Ripemd160Block)?, - ecrecover_base: get(Cost::EcrecoverBase)?, +fn estimation(cost: ExtCosts) -> Option { + Some(match cost { + ExtCosts::base => Cost::HostFunctionCall, + ExtCosts::read_memory_base => Cost::ReadMemoryBase, + ExtCosts::read_memory_byte => Cost::ReadMemoryByte, + ExtCosts::write_memory_base => Cost::WriteMemoryBase, + ExtCosts::write_memory_byte => Cost::WriteMemoryByte, + ExtCosts::read_register_base => Cost::ReadRegisterBase, + ExtCosts::read_register_byte => Cost::ReadRegisterByte, + ExtCosts::write_register_base => Cost::WriteRegisterBase, + ExtCosts::write_register_byte => Cost::WriteRegisterByte, + ExtCosts::utf8_decoding_base => Cost::Utf8DecodingBase, + ExtCosts::utf8_decoding_byte => Cost::Utf8DecodingByte, + ExtCosts::utf16_decoding_base => Cost::Utf16DecodingBase, + ExtCosts::utf16_decoding_byte => Cost::Utf16DecodingByte, + ExtCosts::sha256_base => Cost::Sha256Base, + ExtCosts::sha256_byte => Cost::Sha256Byte, + ExtCosts::keccak256_base => Cost::Keccak256Base, + ExtCosts::keccak256_byte => Cost::Keccak256Byte, + ExtCosts::keccak512_base => Cost::Keccak512Base, + ExtCosts::keccak512_byte => Cost::Keccak512Byte, + ExtCosts::ripemd160_base => Cost::Ripemd160Base, + ExtCosts::ripemd160_block => Cost::Ripemd160Block, + ExtCosts::ecrecover_base => Cost::EcrecoverBase, #[cfg(feature = "protocol_feature_ed25519_verify")] - ed25519_verify_base: get(Cost::Ed25519VerifyBase)?, + ExtCosts::ed25519_verify_base => Cost::Ed25519VerifyBase, #[cfg(feature = "protocol_feature_ed25519_verify")] - ed25519_verify_byte: get(Cost::Ed25519VerifyByte)?, - log_base: get(Cost::LogBase)?, - log_byte: get(Cost::LogByte)?, - storage_write_base: get(Cost::StorageWriteBase)?, - storage_write_key_byte: get(Cost::StorageWriteKeyByte)?, - storage_write_value_byte: get(Cost::StorageWriteValueByte)?, - storage_write_evicted_byte: get(Cost::StorageWriteEvictedByte)?, - storage_read_base: get(Cost::StorageReadBase)?, - storage_read_key_byte: get(Cost::StorageReadKeyByte)?, - storage_read_value_byte: get(Cost::StorageReadValueByte)?, - storage_remove_base: get(Cost::StorageRemoveBase)?, - storage_remove_key_byte: get(Cost::StorageRemoveKeyByte)?, - storage_remove_ret_value_byte: get(Cost::StorageRemoveRetValueByte)?, - storage_has_key_base: get(Cost::StorageHasKeyBase)?, - storage_has_key_byte: get(Cost::StorageHasKeyByte)?, - // TODO: storage_iter_* operations below are deprecated, so just hardcode zero price, - // and remove those operations ASAP. - 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: get(Cost::TouchingTrieNode)?, - read_cached_trie_node: get(Cost::ReadCachedTrieNode)?, - promise_and_base: get(Cost::PromiseAndBase)?, - promise_and_per_promise: get(Cost::PromiseAndPerPromise)?, - promise_return: get(Cost::PromiseReturn)?, - // TODO: accurately price host functions that expose validator information. - validator_stake_base: 303944908800, - validator_total_stake_base: 303944908800, - _unused1: 0, - _unused2: 0, - alt_bn128_g1_sum_base: get(Cost::AltBn128G1SumBase)?, - alt_bn128_g1_sum_element: get(Cost::AltBn128G1SumElement)?, - alt_bn128_g1_multiexp_base: get(Cost::AltBn128G1MultiexpBase)?, - alt_bn128_g1_multiexp_element: get(Cost::AltBn128G1MultiexpElement)?, - alt_bn128_pairing_check_base: get(Cost::AltBn128PairingCheckBase)?, - alt_bn128_pairing_check_element: get(Cost::AltBn128PairingCheckElement)?, - }; - - Ok(res) + ExtCosts::ed25519_verify_byte => Cost::Ed25519VerifyByte, + ExtCosts::log_base => Cost::LogBase, + ExtCosts::log_byte => Cost::LogByte, + ExtCosts::storage_write_base => Cost::StorageWriteBase, + ExtCosts::storage_write_key_byte => Cost::StorageWriteKeyByte, + ExtCosts::storage_write_value_byte => Cost::StorageWriteValueByte, + ExtCosts::storage_write_evicted_byte => Cost::StorageWriteEvictedByte, + ExtCosts::storage_read_base => Cost::StorageReadBase, + ExtCosts::storage_read_key_byte => Cost::StorageReadKeyByte, + ExtCosts::storage_read_value_byte => Cost::StorageReadValueByte, + ExtCosts::storage_remove_base => Cost::StorageRemoveBase, + ExtCosts::storage_remove_key_byte => Cost::StorageRemoveKeyByte, + ExtCosts::storage_remove_ret_value_byte => Cost::StorageRemoveRetValueByte, + ExtCosts::storage_has_key_base => Cost::StorageHasKeyBase, + ExtCosts::storage_has_key_byte => Cost::StorageHasKeyByte, + ExtCosts::touching_trie_node => Cost::TouchingTrieNode, + ExtCosts::read_cached_trie_node => Cost::ReadCachedTrieNode, + ExtCosts::promise_and_base => Cost::PromiseAndBase, + ExtCosts::promise_and_per_promise => Cost::PromiseAndPerPromise, + ExtCosts::promise_return => Cost::PromiseReturn, + ExtCosts::alt_bn128_g1_sum_base => Cost::AltBn128G1SumBase, + ExtCosts::alt_bn128_g1_sum_element => Cost::AltBn128G1SumElement, + ExtCosts::alt_bn128_g1_multiexp_base => Cost::AltBn128G1MultiexpBase, + ExtCosts::alt_bn128_g1_multiexp_element => Cost::AltBn128G1MultiexpElement, + ExtCosts::alt_bn128_pairing_check_base => Cost::AltBn128PairingCheckBase, + ExtCosts::alt_bn128_pairing_check_element => Cost::AltBn128PairingCheckElement, + _ => return None, + }) } From a0def6d30ab7276ed8c09fa4a8637a9c9310b8fe Mon Sep 17 00:00:00 2001 From: Saketh Are Date: Wed, 7 Dec 2022 12:36:46 -0500 Subject: [PATCH 073/188] Migrate remaining routing tests to network crate (#8168) Follow up to #8073, #8109, and #8110 for the two remaining tests in `integration-tests/src/tests/network/routing.rs`. --- chain/network/src/peer_manager/testonly.rs | 42 ++++++++++ .../network/src/peer_manager/tests/routing.rs | 78 +++++++++++++++++++ integration-tests/src/tests/network/mod.rs | 1 - .../src/tests/network/routing.rs | 57 -------------- integration-tests/src/tests/network/runner.rs | 70 ----------------- 5 files changed, 120 insertions(+), 128 deletions(-) delete mode 100644 integration-tests/src/tests/network/routing.rs diff --git a/chain/network/src/peer_manager/testonly.rs b/chain/network/src/peer_manager/testonly.rs index 653a0d92f73..98196091a68 100644 --- a/chain/network/src/peer_manager/testonly.rs +++ b/chain/network/src/peer_manager/testonly.rs @@ -137,6 +137,13 @@ impl ActorHandler { } } + pub async fn send_outbound_connect(&self, peer_info: &PeerInfo, tier: tcp::Tier) { + let addr = self.actix.addr.clone(); + let peer_info = peer_info.clone(); + let stream = tcp::Stream::connect(&peer_info, tier).await.unwrap(); + addr.do_send(PeerManagerMessageRequest::OutboundTcpConnect(stream).with_span_context()); + } + pub fn connect_to( &self, peer_info: &PeerInfo, @@ -352,6 +359,25 @@ impl ActorHandler { } } + pub async fn wait_for_direct_connection(&self, target_peer_id: PeerId) { + let mut events = self.events.from_now(); + loop { + let connections = + self.with_state(|s| async move { s.tier2.load().ready.clone() }).await; + + if connections.contains_key(&target_peer_id) { + return; + } + + events + .recv_until(|ev| match ev { + Event::PeerManager(PME::HandshakeCompleted { .. }) => Some(()), + _ => None, + }) + .await; + } + } + // Awaits until the routing_table matches `want`. pub async fn wait_for_routing_table(&self, want: &[(PeerId, Vec)]) { let mut events = self.events.from_now(); @@ -391,6 +417,22 @@ impl ActorHandler { } } + pub async fn wait_for_num_connected_peers(&self, wanted: usize) { + let mut events = self.events.from_now(); + loop { + let got = self.with_state(|s| async move { s.tier2.load().ready.len() }).await; + if got == wanted { + return; + } + events + .recv_until(|ev| match ev { + Event::PeerManager(PME::EdgesAdded { .. }) => Some(()), + _ => None, + }) + .await; + } + } + /// Executes `NetworkState::tier1_connect` method. pub async fn tier1_connect(&self, clock: &time::Clock) { let clock = clock.clone(); diff --git a/chain/network/src/peer_manager/tests/routing.rs b/chain/network/src/peer_manager/tests/routing.rs index 38b34291014..bac7f86bbdc 100644 --- a/chain/network/src/peer_manager/tests/routing.rs +++ b/chain/network/src/peer_manager/tests/routing.rs @@ -17,8 +17,10 @@ use crate::time; use crate::types::PeerInfo; use crate::types::PeerMessage; use near_o11y::testonly::init_test_logger; +use near_primitives::network::PeerId; use near_store::db::TestDB; use pretty_assertions::assert_eq; +use rand::seq::IteratorRandom; use rand::Rng as _; use std::collections::HashSet; use std::net::Ipv4Addr; @@ -1222,3 +1224,79 @@ async fn do_not_block_announce_account_broadcast() { pm1.announce_account(aa.clone()).await; assert_eq!(&aa.peer_id, &pm2.wait_for_account_owner(&aa.account_id).await); } + +/// Check that two archival nodes keep connected after network rebalance. Nodes 0 and 1 are archival nodes, others aren't. +/// Initially connect 2, 3, 4 to 0. Then connect 1 to 0, this connection should persist, even after other nodes tries +/// to connect to node 0 again. +/// +/// Do four rounds where 2, 3, 4 tries to connect to 0 and check that connection between 0 and 1 was never dropped. +#[tokio::test] +async fn archival_node() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + let mut cfgs = make_configs(&chain, rng, 5, 5, false); + for config in cfgs.iter_mut() { + config.max_num_peers = 3; + config.ideal_connections_lo = 2; + config.ideal_connections_hi = 2; + config.safe_set_size = 1; + config.minimum_outbound_peers = 0; + } + cfgs[0].archive = true; + cfgs[1].archive = true; + + tracing::info!(target:"test", "start five nodes, the first two of which are archival nodes"); + let pm0 = start_pm(clock.clock(), TestDB::new(), cfgs[0].clone(), chain.clone()).await; + let pm1 = start_pm(clock.clock(), TestDB::new(), cfgs[1].clone(), chain.clone()).await; + let pm2 = start_pm(clock.clock(), TestDB::new(), cfgs[2].clone(), chain.clone()).await; + let pm3 = start_pm(clock.clock(), TestDB::new(), cfgs[3].clone(), chain.clone()).await; + let pm4 = start_pm(clock.clock(), TestDB::new(), cfgs[4].clone(), chain.clone()).await; + + let id1 = pm1.cfg.node_id(); + + // capture pm0 event stream + let mut pm0_ev = pm0.events.from_now(); + + tracing::info!(target:"test", "connect node 2 to node 0"); + pm2.send_outbound_connect(&pm0.peer_info(), tcp::Tier::T2).await; + tracing::info!(target:"test", "connect node 3 to node 0"); + pm3.send_outbound_connect(&pm0.peer_info(), tcp::Tier::T2).await; + + tracing::info!(target:"test", "connect node 4 to node 0 and wait for pm0 to close a connection"); + pm4.send_outbound_connect(&pm0.peer_info(), tcp::Tier::T2).await; + wait_for_connection_closed(&mut pm0_ev, ClosingReason::PeerManager).await; + + tracing::info!(target:"test", "connect node 1 to node 0 and wait for pm0 to close a connection"); + pm1.send_outbound_connect(&pm0.peer_info(), tcp::Tier::T2).await; + wait_for_connection_closed(&mut pm0_ev, ClosingReason::PeerManager).await; + + tracing::info!(target:"test", "check that node 0 and node 1 are still connected"); + pm0.wait_for_direct_connection(id1.clone()).await; + + for _step in 0..10 { + tracing::info!(target:"test", "[{_step}] select a node which node 0 is not connected to"); + let pm0_connections: HashSet = + pm0.with_state(|s| async move { s.tier2.load().ready.keys().cloned().collect() }).await; + + let chosen = vec![&pm2, &pm3, &pm4] + .iter() + .filter(|&pm| !pm0_connections.contains(&pm.cfg.node_id())) + .choose(rng) + .unwrap() + .clone(); + + tracing::info!(target:"test", "[{_step}] wait for the chosen node to finish disconnecting from node 0"); + chosen.wait_for_num_connected_peers(0).await; + + tracing::info!(target:"test", "[{_step}] connect the chosen node to node 0 and wait for pm0 to close a connection"); + chosen.send_outbound_connect(&pm0.peer_info(), tcp::Tier::T2).await; + wait_for_connection_closed(&mut pm0_ev, ClosingReason::PeerManager).await; + + tracing::info!(target:"test", "[{_step}] check that node 0 and node 1 are still connected"); + pm0.wait_for_direct_connection(id1.clone()).await; + } +} diff --git a/integration-tests/src/tests/network/mod.rs b/integration-tests/src/tests/network/mod.rs index 0738982137d..714bb3f5fe3 100644 --- a/integration-tests/src/tests/network/mod.rs +++ b/integration-tests/src/tests/network/mod.rs @@ -2,6 +2,5 @@ mod ban_peers; mod churn_attack; mod full_network; mod peer_handshake; -mod routing; mod runner; mod stress_network; diff --git a/integration-tests/src/tests/network/routing.rs b/integration-tests/src/tests/network/routing.rs deleted file mode 100644 index 094d654bbe1..00000000000 --- a/integration-tests/src/tests/network/routing.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::tests::network::runner::*; -use near_network::time; - -#[test] -fn account_propagation() -> anyhow::Result<()> { - let mut runner = Runner::new(3, 2); - - runner.push(Action::AddEdge { from: 0, to: 1, force: true }); - runner.push(Action::CheckAccountId(1, vec![0, 1])); - runner.push(Action::AddEdge { from: 0, to: 2, force: true }); - runner.push(Action::CheckAccountId(2, vec![0, 1])); - - start_test(runner) -} - -/// Check that two archival nodes keep connected after network rebalance. Nodes 0 and 1 are archival nodes, others aren't. -/// Initially connect 2, 3, 4 to 0. Then connect 1 to 0, this connection should persist, even after other nodes tries -/// to connect to node 0 again. -/// -/// Do four rounds where 2, 3, 4 tries to connect to 0 and check that connection between 0 and 1 was never dropped. -#[test] -// TODO(#5389) fix this test, ignoring for now to unlock merging -fn archival_node() -> anyhow::Result<()> { - let mut runner = Runner::new(5, 5) - .max_num_peers(3) - .ideal_connections(2, 2) - .safe_set_size(1) - .minimum_outbound_peers(0) - .set_as_archival(0) - .set_as_archival(1); - - runner.push(Action::AddEdge { from: 2, to: 0, force: true }); - runner.push(Action::Wait(time::Duration::milliseconds(50))); - runner.push(Action::AddEdge { from: 3, to: 0, force: true }); - runner.push(Action::Wait(time::Duration::milliseconds(50))); - runner.push(Action::AddEdge { from: 4, to: 0, force: true }); - runner.push(Action::Wait(time::Duration::milliseconds(50))); - runner.push_action(check_expected_connections(0, Some(2), Some(2))); - - runner.push(Action::AddEdge { from: 1, to: 0, force: true }); - runner.push(Action::Wait(time::Duration::milliseconds(50))); - runner.push_action(check_expected_connections(0, Some(2), Some(2))); - runner.push_action(check_direct_connection(0, 1)); - - for _step in 0..4 { - runner.push(Action::AddEdge { from: 2, to: 0, force: true }); - runner.push(Action::Wait(time::Duration::milliseconds(50))); - runner.push(Action::AddEdge { from: 3, to: 0, force: true }); - runner.push(Action::Wait(time::Duration::milliseconds(50))); - runner.push(Action::AddEdge { from: 4, to: 0, force: true }); - runner.push(Action::Wait(time::Duration::milliseconds(50))); - runner.push_action(check_expected_connections(0, Some(2), Some(2))); - runner.push_action(check_direct_connection(0, 1)); - } - - start_test(runner) -} diff --git a/integration-tests/src/tests/network/runner.rs b/integration-tests/src/tests/network/runner.rs index df0a54fea49..6bf3242e2e0 100644 --- a/integration-tests/src/tests/network/runner.rs +++ b/integration-tests/src/tests/network/runner.rs @@ -126,7 +126,6 @@ pub enum Action { force: bool, }, CheckRoutingTable(usize, Vec<(usize, Vec)>), - CheckAccountId(usize, Vec), // Send stop signal to some node. Stop(usize), // Wait time in milliseconds @@ -172,29 +171,6 @@ async fn check_routing_table( Ok(ControlFlow::Continue(())) } -async fn check_account_id( - info: &mut RunningInfo, - source: usize, - known_validators: Vec, -) -> anyhow::Result { - let mut expected_known = vec![]; - for u in known_validators.clone() { - expected_known.push(info.runner.test_config[u].account_id.clone()); - } - let pm = &info.get_node(source)?.actix.addr; - let rt = match pm.send(PeerManagerMessageRequest::FetchRoutingTable.with_span_context()).await? - { - PeerManagerMessageResponse::FetchRoutingTable(rt) => rt, - _ => bail!("bad response"), - }; - for v in &expected_known { - if !rt.account_peers.contains_key(v) { - return Ok(ControlFlow::Continue(())); - } - } - Ok(ControlFlow::Break(())) -} - impl StateMachine { fn new() -> Self { Self { actions: vec![] } @@ -245,11 +221,6 @@ impl StateMachine { Box::pin(check_routing_table(info, u, expected.clone())) })); } - Action::CheckAccountId(source, known_validators) => { - self.actions.push(Box::new(move |info| { - Box::pin(check_account_id(info, source, known_validators.clone())) - })); - } Action::Stop(source) => { self.actions.push(Box::new(move |info: &mut RunningInfo| Box::pin(async move { debug!(target: "test", num_prev_actions, action = ?action_clone, "runner.rs: Action"); @@ -351,12 +322,6 @@ impl Runner { self } - /// Set node `u` as archival node. - pub fn set_as_archival(mut self, u: usize) -> Self { - self.test_config[u].archive = true; - self - } - /// Specify boot nodes. By default there are no boot nodes. pub fn use_boot_nodes(mut self, boot_nodes: Vec) -> Self { self.apply_all(move |test_config| { @@ -592,41 +557,6 @@ pub fn check_expected_connections( }) } -async fn check_direct_connection_inner( - info: &mut RunningInfo, - node_id: usize, - target_id: usize, -) -> anyhow::Result { - let target_peer_id = info.runner.test_config[target_id].peer_id(); - debug!(target: "test", node_id, ?target_id, "runner.rs: check_direct_connection"); - let pm = &info.get_node(node_id)?.actix.addr; - let rt = match pm.send(PeerManagerMessageRequest::FetchRoutingTable.with_span_context()).await? - { - PeerManagerMessageResponse::FetchRoutingTable(rt) => rt, - _ => bail!("bad response"), - }; - let routes = if let Some(routes) = rt.next_hops.get(&target_peer_id) { - routes - } else { - debug!(target: "test", ?target_peer_id, node_id, target_id, - "runner.rs: check_direct_connection NO ROUTES!", - ); - return Ok(ControlFlow::Continue(())); - }; - debug!(target: "test", ?target_peer_id, ?routes, node_id, target_id, - "runner.rs: check_direct_connection", - ); - if !routes.contains(&target_peer_id) { - return Ok(ControlFlow::Continue(())); - } - Ok(ControlFlow::Break(())) -} - -/// Check that `node_id` has a direct connection to `target_id`. -pub fn check_direct_connection(node_id: usize, target_id: usize) -> ActionFn { - Box::new(move |info| Box::pin(check_direct_connection_inner(info, node_id, target_id))) -} - /// Restart a node that was already stopped. pub fn restart(node_id: usize) -> ActionFn { Box::new(move |info: &mut RunningInfo| { From a7f7ed711c9cfb2a55c866b143880efb676f9f18 Mon Sep 17 00:00:00 2001 From: mm-near <91919554+mm-near@users.noreply.github.com> Date: Wed, 7 Dec 2022 18:49:39 +0100 Subject: [PATCH 074/188] Added create_test_signer function that simplifies tests (#8176) Added create_test_signer that creates the signer for the given account with the seed that matches the account name. Can be used in tests only. --- chain/chain/src/doomslug.rs | 30 +----- chain/chain/src/store.rs | 50 ++-------- chain/chain/src/test_utils.rs | 18 +--- chain/chain/src/tests/doomslug.rs | 10 +- chain/chain/src/tests/gc.rs | 10 +- chain/chain/src/types.rs | 16 +--- chain/chunks/src/lib.rs | 6 +- chain/chunks/src/test_utils.rs | 9 +- chain/client/src/sync/header.rs | 8 +- chain/client/src/test_utils.rs | 27 ++---- chain/client/src/tests/process_blocks.rs | 6 +- chain/client/src/tests/query_client.rs | 5 +- chain/network/src/config.rs | 10 +- core/primitives/src/test_utils.rs | 14 ++- docs/practices/testing/test_utils.md | 4 +- .../src/tests/client/challenges.rs | 8 +- .../src/tests/client/process_blocks.rs | 79 +++++----------- .../src/tests/client/runtimes.rs | 7 +- .../src/tests/nearcore/sync_nodes.rs | 15 +-- integration-tests/src/tests/network/runner.rs | 9 +- nearcore/src/config.rs | 15 +-- nearcore/src/runtime/mod.rs | 93 +++++++------------ test-utils/testlib/src/process_blocks.rs | 6 +- 23 files changed, 133 insertions(+), 322 deletions(-) diff --git a/chain/chain/src/doomslug.rs b/chain/chain/src/doomslug.rs index c54b0c040aa..28f1b4bc50c 100644 --- a/chain/chain/src/doomslug.rs +++ b/chain/chain/src/doomslug.rs @@ -713,9 +713,9 @@ mod tests { use near_crypto::{KeyType, SecretKey}; use near_primitives::block::{Approval, ApprovalInner}; use near_primitives::hash::hash; + use near_primitives::test_utils::create_test_signer; use near_primitives::time::Clock; use near_primitives::types::ApprovalStake; - use near_primitives::validator_signer::InMemoryValidatorSigner; use crate::doomslug::{ DoomslugApprovalsTrackersAtHeight, DoomslugBlockProductionReadiness, DoomslugThresholdMode, @@ -730,11 +730,7 @@ mod tests { Duration::from_millis(1000), Duration::from_millis(100), Duration::from_millis(3000), - Some(Arc::new(InMemoryValidatorSigner::from_seed( - "test".parse().unwrap(), - KeyType::ED25519, - "test", - ))), + Some(Arc::new(create_test_signer("test"))), DoomslugThresholdMode::TwoThirds, ); @@ -870,20 +866,10 @@ mod tests { .collect::>(); let signers = accounts .iter() - .map(|(account_id, _, _)| { - InMemoryValidatorSigner::from_seed( - account_id.parse().unwrap(), - KeyType::ED25519, - account_id, - ) - }) + .map(|(account_id, _, _)| create_test_signer(account_id)) .collect::>(); - let signer = Arc::new(InMemoryValidatorSigner::from_seed( - "test".parse().unwrap(), - KeyType::ED25519, - "test", - )); + let signer = Arc::new(create_test_signer("test")); let mut ds = Doomslug::new( 0, Duration::from_millis(400), @@ -998,13 +984,7 @@ mod tests { let accounts = vec![("test1", 2, 0), ("test2", 1, 2), ("test3", 3, 3), ("test4", 2, 2)]; let signers = accounts .iter() - .map(|(account_id, _, _)| { - InMemoryValidatorSigner::from_seed( - account_id.parse().unwrap(), - KeyType::ED25519, - account_id, - ) - }) + .map(|(account_id, _, _)| create_test_signer(account_id)) .collect::>(); let stakes = accounts .into_iter() diff --git a/chain/chain/src/store.rs b/chain/chain/src/store.rs index 87c5a05edbd..600ada7b166 100644 --- a/chain/chain/src/store.rs +++ b/chain/chain/src/store.rs @@ -2976,11 +2976,11 @@ mod tests { use near_primitives::merkle::PartialMerkleTree; use near_chain_configs::{GCConfig, GenesisConfig}; - use near_crypto::KeyType; use near_primitives::block::{Block, Tip}; use near_primitives::epoch_manager::block_info::BlockInfo; use near_primitives::errors::InvalidTxError; use near_primitives::hash::hash; + use near_primitives::test_utils::create_test_signer; use near_primitives::types::{BlockHeight, EpochId, NumBlocks}; use near_primitives::utils::index_to_bytes; use near_primitives::validator_signer::InMemoryValidatorSigner; @@ -3018,11 +3018,7 @@ mod tests { let transaction_validity_period = 5; let mut chain = get_chain(); let genesis = chain.get_block_by_height(0).unwrap(); - let signer = Arc::new(InMemoryValidatorSigner::from_seed( - "test1".parse().unwrap(), - KeyType::ED25519, - "test1", - )); + let signer = Arc::new(create_test_signer("test1")); let short_fork = vec![Block::empty_with_height(&genesis, 1, &*signer)]; let mut store_update = chain.mut_store().store_update(); store_update.save_block_header(short_fork[0].header().clone()).unwrap(); @@ -3076,11 +3072,7 @@ mod tests { let transaction_validity_period = 5; let mut chain = get_chain(); let genesis = chain.get_block_by_height(0).unwrap(); - let signer = Arc::new(InMemoryValidatorSigner::from_seed( - "test1".parse().unwrap(), - KeyType::ED25519, - "test1", - )); + let signer = Arc::new(create_test_signer("test1")); let mut blocks = vec![]; let mut prev_block = genesis; for i in 1..(transaction_validity_period + 2) { @@ -3130,11 +3122,7 @@ mod tests { let transaction_validity_period = 5; let mut chain = get_chain(); let genesis = chain.get_block_by_height(0).unwrap(); - let signer = Arc::new(InMemoryValidatorSigner::from_seed( - "test1".parse().unwrap(), - KeyType::ED25519, - "test1", - )); + let signer = Arc::new(create_test_signer("test1")); let mut short_fork = vec![]; let mut prev_block = genesis.clone(); for i in 1..(transaction_validity_period + 2) { @@ -3180,11 +3168,7 @@ mod tests { fn test_cache_invalidation() { let mut chain = get_chain(); let genesis = chain.get_block_by_height(0).unwrap(); - let signer = Arc::new(InMemoryValidatorSigner::from_seed( - "test1".parse().unwrap(), - KeyType::ED25519, - "test1", - )); + let signer = Arc::new(create_test_signer("test1")); let block1 = Block::empty_with_height(&genesis, 1, &*signer); let mut block2 = block1.clone(); block2.mut_header().get_mut().inner_lite.epoch_id = EpochId(hash(&[1, 2, 3])); @@ -3225,11 +3209,7 @@ mod tests { let mut chain = get_chain_with_epoch_length(1); let runtime_adapter = chain.runtime_adapter.clone(); let genesis = chain.get_block_by_height(0).unwrap(); - let signer = Arc::new(InMemoryValidatorSigner::from_seed( - "test1".parse().unwrap(), - KeyType::ED25519, - "test1", - )); + let signer = Arc::new(create_test_signer("test1")); let mut prev_block = genesis; let mut blocks = vec![prev_block.clone()]; for i in 1..15 { @@ -3317,11 +3297,7 @@ mod tests { let mut chain = get_chain(); let runtime_adapter = chain.runtime_adapter.clone(); let genesis = chain.get_block_by_height(0).unwrap(); - let signer = Arc::new(InMemoryValidatorSigner::from_seed( - "test1".parse().unwrap(), - KeyType::ED25519, - "test1", - )); + let signer = Arc::new(create_test_signer("test1")); let mut prev_block = genesis; let mut blocks = vec![prev_block.clone()]; for i in 1..10 { @@ -3389,11 +3365,7 @@ mod tests { fn test_clear_old_data_too_many_heights_common(gc_blocks_limit: NumBlocks) { let mut chain = get_chain_with_epoch_length(1); let genesis = chain.get_block_by_height(0).unwrap(); - let signer = Arc::new(InMemoryValidatorSigner::from_seed( - "test1".parse().unwrap(), - KeyType::ED25519, - "test1", - )); + let signer = Arc::new(create_test_signer("test1")); let mut prev_block = genesis.clone(); let mut blocks = vec![prev_block.clone()]; { @@ -3474,11 +3446,7 @@ mod tests { let mut chain = get_chain(); let runtime_adapter = chain.runtime_adapter.clone(); let genesis = chain.get_block_by_height(0).unwrap(); - let signer = Arc::new(InMemoryValidatorSigner::from_seed( - "test1".parse().unwrap(), - KeyType::ED25519, - "test1", - )); + let signer = Arc::new(create_test_signer("test1")); let mut prev_block = genesis; let mut blocks = vec![prev_block.clone()]; for i in 1..10 { diff --git a/chain/chain/src/test_utils.rs b/chain/chain/src/test_utils.rs index 7e69a2736cd..6406bbb14ae 100644 --- a/chain/chain/src/test_utils.rs +++ b/chain/chain/src/test_utils.rs @@ -4,12 +4,12 @@ mod validator_schedule; use std::cmp::Ordering; use std::sync::Arc; +use near_primitives::test_utils::create_test_signer; use num_rational::Ratio; use tracing::debug; use near_chain_primitives::Error; -use near_crypto::KeyType; use near_primitives::block::Block; use near_primitives::hash::CryptoHash; @@ -105,12 +105,8 @@ pub fn setup_with_tx_validity_period( ChainConfig::test(), ) .unwrap(); - let test_account = "test".parse::().unwrap(); - let signer = Arc::new(InMemoryValidatorSigner::from_seed( - test_account.clone(), - KeyType::ED25519, - test_account.as_ref(), - )); + + let signer = Arc::new(create_test_signer("test")); (chain, runtime, signer) } @@ -120,12 +116,8 @@ pub fn setup_with_validators( tx_validity_period: NumBlocks, ) -> (Chain, Arc, Vec>) { let store = create_test_store(); - let signers = vs - .all_block_producers() - .map(|x| { - Arc::new(InMemoryValidatorSigner::from_seed(x.clone(), KeyType::ED25519, x.as_ref())) - }) - .collect(); + let signers = + vs.all_block_producers().map(|x| Arc::new(create_test_signer(x.as_str()))).collect(); let runtime = Arc::new(KeyValueRuntime::new_with_validators(store, vs, epoch_length)); let chain = Chain::new( runtime.clone(), diff --git a/chain/chain/src/tests/doomslug.rs b/chain/chain/src/tests/doomslug.rs index 0fd90f1298a..653a92ed250 100644 --- a/chain/chain/src/tests/doomslug.rs +++ b/chain/chain/src/tests/doomslug.rs @@ -1,3 +1,4 @@ +use near_primitives::test_utils::create_test_signer; use near_primitives::time::Clock; use rand::{thread_rng, Rng}; use std::collections::{HashMap, HashSet}; @@ -9,7 +10,6 @@ use near_crypto::{KeyType, SecretKey}; use near_primitives::block::Approval; use near_primitives::hash::{hash, CryptoHash}; use near_primitives::types::{ApprovalStake, BlockHeight}; -use near_primitives::validator_signer::InMemoryValidatorSigner; fn block_hash(height: BlockHeight, ord: usize) -> CryptoHash { hash(([height.to_le_bytes(), ord.to_le_bytes()].concat()).as_ref()) @@ -48,13 +48,7 @@ fn one_iter( .collect::>(); let signers = account_ids .iter() - .map(|account_id| { - Arc::new(InMemoryValidatorSigner::from_seed( - account_id.parse().unwrap(), - KeyType::ED25519, - account_id, - )) - }) + .map(|account_id| Arc::new(create_test_signer(account_id))) .collect::>(); let mut doomslugs = signers .iter() diff --git a/chain/chain/src/tests/gc.rs b/chain/chain/src/tests/gc.rs index 2b1adea914f..65ac41102cb 100644 --- a/chain/chain/src/tests/gc.rs +++ b/chain/chain/src/tests/gc.rs @@ -6,13 +6,11 @@ use crate::types::{ChainConfig, ChainGenesis, Tip}; use crate::DoomslugThresholdMode; use near_chain_configs::GCConfig; -use near_crypto::KeyType; use near_primitives::block::Block; use near_primitives::merkle::PartialMerkleTree; use near_primitives::shard_layout::ShardUId; -use near_primitives::test_utils::TestBlockBuilder; +use near_primitives::test_utils::{create_test_signer, TestBlockBuilder}; use near_primitives::types::{NumBlocks, NumShards, StateRoot}; -use near_primitives::validator_signer::InMemoryValidatorSigner; use near_store::test_utils::{create_test_store, gen_changes}; use near_store::{ShardTries, Trie, WrappedTrieChanges}; use rand::Rng; @@ -61,11 +59,7 @@ fn do_fork( ); } let mut rng = rand::thread_rng(); - let signer = Arc::new(InMemoryValidatorSigner::from_seed( - "test1".parse().unwrap(), - KeyType::ED25519, - "test1", - )); + let signer = Arc::new(create_test_signer("test1")); let num_shards = prev_state_roots.len() as u64; let runtime_adapter = chain.runtime_adapter.clone(); for i in 0..num_blocks { diff --git a/chain/chain/src/types.rs b/chain/chain/src/types.rs index d786886e7ab..75518493fe6 100644 --- a/chain/chain/src/types.rs +++ b/chain/chain/src/types.rs @@ -577,15 +577,13 @@ pub struct LatestKnown { #[cfg(test)] mod tests { - use near_primitives::test_utils::TestBlockBuilder; + use near_primitives::test_utils::{create_test_signer, TestBlockBuilder}; use near_primitives::time::Utc; - use near_crypto::KeyType; use near_primitives::block::{genesis_chunks, Approval}; use near_primitives::hash::hash; use near_primitives::merkle::verify_path; use near_primitives::transaction::{ExecutionMetadata, ExecutionOutcome, ExecutionStatus}; - use near_primitives::validator_signer::InMemoryValidatorSigner; use near_primitives::version::PROTOCOL_VERSION; use super::*; @@ -605,18 +603,10 @@ mod tests { 1_000_000_000, CryptoHash::hash_borsh(genesis_bps), ); - let signer = Arc::new(InMemoryValidatorSigner::from_seed( - "other".parse().unwrap(), - KeyType::ED25519, - "other", - )); + let signer = Arc::new(create_test_signer("other")); let b1 = TestBlockBuilder::new(&genesis, signer.clone()).build(); assert!(b1.header().verify_block_producer(&signer.public_key())); - let other_signer = InMemoryValidatorSigner::from_seed( - "other2".parse().unwrap(), - KeyType::ED25519, - "other2", - ); + let other_signer = create_test_signer("other2"); let approvals = vec![Some(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/src/lib.rs b/chain/chunks/src/lib.rs index 02534b18bcf..dc4a516a422 100644 --- a/chain/chunks/src/lib.rs +++ b/chain/chunks/src/lib.rs @@ -2174,12 +2174,12 @@ mod test { use assert_matches::assert_matches; use near_chain::types::EpochManagerAdapter; + use near_primitives::test_utils::create_test_signer; use std::sync::Arc; use std::time::Duration; use near_chain::test_utils::{KeyValueRuntime, ValidatorSchedule}; use near_chain::{Chain, ChainStore}; - use near_crypto::KeyType; use near_network::test_utils::MockPeerManagerAdapter; use near_network::types::NetworkRequests; use near_o11y::testonly::init_test_logger; @@ -2188,7 +2188,6 @@ mod test { use near_primitives::merkle::merklize; use near_primitives::sharding::ReedSolomonWrapper; use near_primitives::types::EpochId; - use near_primitives::validator_signer::InMemoryValidatorSigner; use near_primitives::version::PROTOCOL_VERSION; use near_store::test_utils::create_test_store; @@ -2267,8 +2266,7 @@ mod test { chain_store.new_read_only_chunks_store(), None, ); - let signer = - InMemoryValidatorSigner::from_seed("test".parse().unwrap(), KeyType::ED25519, "test"); + let signer = create_test_signer("test"); let mut rs = ReedSolomonWrapper::new(4, 10); let shard_layout = runtime_adapter.get_shard_layout(&EpochId::default()).unwrap(); let (encoded_chunk, proof) = ShardsManager::create_encoded_shard_chunk( diff --git a/chain/chunks/src/test_utils.rs b/chain/chunks/src/test_utils.rs index 157c422ee07..27ded3ec032 100644 --- a/chain/chunks/src/test_utils.rs +++ b/chain/chunks/src/test_utils.rs @@ -6,12 +6,12 @@ use futures::future::BoxFuture; use futures::FutureExt; use near_network::types::MsgRecipient; use near_primitives::receipt::Receipt; +use near_primitives::test_utils::create_test_signer; use near_primitives::time::Clock; use near_chain::test_utils::{KeyValueRuntime, ValidatorSchedule}; use near_chain::types::{EpochManagerAdapter, RuntimeAdapter, Tip}; use near_chain::{Chain, ChainStore}; -use near_crypto::KeyType; use near_network::test_utils::MockPeerManagerAdapter; use near_o11y::WithSpanContext; use near_primitives::block::BlockHeader; @@ -24,7 +24,6 @@ use near_primitives::sharding::{ use near_primitives::types::NumShards; use near_primitives::types::{AccountId, EpochId, ShardId}; use near_primitives::types::{BlockHeight, MerkleHash}; -use near_primitives::validator_signer::InMemoryValidatorSigner; use near_primitives::version::PROTOCOL_VERSION; use near_store::Store; @@ -201,11 +200,7 @@ impl ChunkTestFixture { let mock_epoch_id = mock_runtime.get_epoch_id_from_prev_block(&mock_ancestor_hash).unwrap(); let mock_chunk_producer = mock_runtime.get_chunk_producer(&mock_epoch_id, mock_height, mock_shard_id).unwrap(); - let signer = InMemoryValidatorSigner::from_seed( - mock_chunk_producer.clone(), - KeyType::ED25519, - mock_chunk_producer.as_ref(), - ); + let signer = create_test_signer(mock_chunk_producer.as_str()); let validators: Vec<_> = mock_runtime .get_epoch_block_producers_ordered(&EpochId::default(), &CryptoHash::default()) .unwrap() diff --git a/chain/client/src/sync/header.rs b/chain/client/src/sync/header.rs index e21c145a813..4f75f1d775e 100644 --- a/chain/client/src/sync/header.rs +++ b/chain/client/src/sync/header.rs @@ -321,12 +321,12 @@ mod test { use near_network::test_utils::MockPeerManagerAdapter; use near_primitives::block::{Approval, Block, GenesisId}; use near_primitives::network::PeerId; + use near_primitives::test_utils::create_test_signer; use super::*; use near_network::types::{BlockInfo, FullPeerInfo, PeerInfo}; use near_primitives::merkle::PartialMerkleTree; use near_primitives::types::EpochId; - use near_primitives::validator_signer::InMemoryValidatorSigner; use near_primitives::version::PROTOCOL_VERSION; use num_rational::Ratio; @@ -483,11 +483,7 @@ mod test { .iter() .map(|account_id| { account_id.map(|account_id| { - let signer = InMemoryValidatorSigner::from_seed( - account_id.parse().unwrap(), - KeyType::ED25519, - account_id, - ); + let signer = create_test_signer(account_id); Approval::new( *last_block.hash(), last_block.header().height(), diff --git a/chain/client/src/test_utils.rs b/chain/client/src/test_utils.rs index 2776fbb6f2d..2655203b8b6 100644 --- a/chain/client/src/test_utils.rs +++ b/chain/client/src/test_utils.rs @@ -8,6 +8,7 @@ use std::time::Duration; use actix::{Actor, Addr, AsyncContext, Context}; use chrono::DateTime; use futures::{future, FutureExt}; +use near_primitives::test_utils::create_test_signer; use num_rational::Ratio; use once_cell::sync::OnceCell; use rand::{thread_rng, Rng}; @@ -59,7 +60,7 @@ use near_primitives::types::{ AccountId, Balance, BlockHeight, BlockHeightDelta, EpochId, NumBlocks, NumSeats, ShardId, }; use near_primitives::utils::MaybeValidated; -use near_primitives::validator_signer::{InMemoryValidatorSigner, ValidatorSigner}; +use near_primitives::validator_signer::ValidatorSigner; use near_primitives::version::{ProtocolVersion, PROTOCOL_VERSION}; use near_primitives::views::{ AccountView, FinalExecutionOutcomeView, QueryRequest, QueryResponseKind, StateItem, @@ -227,11 +228,7 @@ pub fn setup( .unwrap(); let genesis_block = chain.get_block(&chain.genesis().hash().clone()).unwrap(); - let signer = Arc::new(InMemoryValidatorSigner::from_seed( - account_id.clone(), - KeyType::ED25519, - account_id.as_ref(), - )); + let signer = Arc::new(create_test_signer(account_id.as_str())); let telemetry = TelemetryActor::default().start(); let config = ClientConfig::test( skip_sync_wait, @@ -316,11 +313,7 @@ pub fn setup_only_view( ) .unwrap(); - let signer = Arc::new(InMemoryValidatorSigner::from_seed( - account_id.clone(), - KeyType::ED25519, - account_id.as_ref(), - )); + let signer = Arc::new(create_test_signer(account_id.as_str())); TelemetryActor::default().start(); let config = ClientConfig::test( skip_sync_wait, @@ -1097,10 +1090,8 @@ pub fn setup_client_with_runtime( runtime_adapter: Arc, rng_seed: RngSeed, ) -> Client { - let validator_signer = account_id.map(|x| { - Arc::new(InMemoryValidatorSigner::from_seed(x.clone(), KeyType::ED25519, x.as_ref())) - as Arc - }); + let validator_signer = + account_id.map(|x| Arc::new(create_test_signer(x.as_str())) as Arc); let mut config = ClientConfig::test(true, 10, 20, num_validator_seats, false, true); config.epoch_length = chain_genesis.epoch_length; let mut client = Client::new( @@ -1546,11 +1537,7 @@ impl TestEnv { let mut block = self.clients[0].produce_block(tip.height + 1).unwrap().unwrap(); block.mut_header().set_latest_protocol_version(protocol_version); - block.mut_header().resign(&InMemoryValidatorSigner::from_seed( - block_producer.clone(), - KeyType::ED25519, - block_producer.as_ref(), - )); + block.mut_header().resign(&create_test_signer(block_producer.as_str())); let _ = self.clients[0] .process_block_test_no_produce_chunk(block.into(), Provenance::NONE) diff --git a/chain/client/src/tests/process_blocks.rs b/chain/client/src/tests/process_blocks.rs index fdc26dc1f22..ffb22962b27 100644 --- a/chain/client/src/tests/process_blocks.rs +++ b/chain/client/src/tests/process_blocks.rs @@ -2,8 +2,8 @@ use crate::test_utils::TestEnv; use near_chain::{test_utils, ChainGenesis, Provenance}; use near_crypto::{KeyType, PublicKey}; use near_primitives::network::PeerId; +use near_primitives::test_utils::create_test_signer; use near_primitives::types::validator_stake::ValidatorStake; -use near_primitives::validator_signer::InMemoryValidatorSigner; use std::sync::Arc; /// Only process one block per height @@ -16,8 +16,8 @@ fn test_not_process_height_twice() { // modify the block and resign it let mut duplicate_block = block.clone(); env.process_block(0, block, Provenance::PRODUCED); - let validator_signer = - InMemoryValidatorSigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let validator_signer = create_test_signer("test0"); + let proposals = vec![ValidatorStake::new("test1".parse().unwrap(), PublicKey::empty(KeyType::ED25519), 0)]; duplicate_block.mut_header().get_mut().inner_rest.validator_proposals = proposals; diff --git a/chain/client/src/tests/query_client.rs b/chain/client/src/tests/query_client.rs index 5ff0e4ff0dc..60fa7d87230 100644 --- a/chain/client/src/tests/query_client.rs +++ b/chain/client/src/tests/query_client.rs @@ -2,6 +2,7 @@ use actix::System; use futures::{future, FutureExt}; use near_chain::test_utils::ValidatorSchedule; use near_primitives::merkle::PartialMerkleTree; +use near_primitives::test_utils::create_test_signer; use std::sync::Arc; use std::time::Duration; @@ -27,7 +28,6 @@ use near_primitives::time::Utc; use near_primitives::transaction::SignedTransaction; use near_primitives::types::{BlockId, BlockReference, EpochId}; use near_primitives::utils::to_timestamp; -use near_primitives::validator_signer::InMemoryValidatorSigner; use near_primitives::version::PROTOCOL_VERSION; use near_primitives::views::{QueryRequest, QueryResponseKind}; use num_rational::Ratio; @@ -66,8 +66,7 @@ fn query_status_not_crash() { run_actix(async { let (client, view_client) = setup_no_network(vec!["test".parse().unwrap()], "other".parse().unwrap(), true, false); - let signer = - InMemoryValidatorSigner::from_seed("test".parse().unwrap(), KeyType::ED25519, "test"); + let signer = create_test_signer("test"); let actor = view_client.send(GetBlockWithMerkleTree::latest().with_span_context()); let actor = actor.then(move |res| { let (block, block_merkle_tree) = res.unwrap().unwrap(); diff --git a/chain/network/src/config.rs b/chain/network/src/config.rs index 5fb320bad60..a6921e87798 100644 --- a/chain/network/src/config.rs +++ b/chain/network/src/config.rs @@ -10,8 +10,9 @@ use crate::types::ROUTED_MESSAGE_TTL; use anyhow::Context; use near_crypto::{KeyType, SecretKey}; use near_primitives::network::PeerId; +use near_primitives::test_utils::create_test_signer; use near_primitives::types::AccountId; -use near_primitives::validator_signer::{InMemoryValidatorSigner, ValidatorSigner}; +use near_primitives::validator_signer::ValidatorSigner; use std::collections::HashSet; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::sync::Arc; @@ -299,13 +300,8 @@ impl NetworkConfig { pub fn from_seed(seed: &str, port: u16) -> Self { let node_key = SecretKey::from_seed(KeyType::ED25519, seed); let node_addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port)); - let account_id = seed.parse().unwrap(); let validator = ValidatorConfig { - signer: Arc::new(InMemoryValidatorSigner::from_seed( - account_id, - KeyType::ED25519, - seed, - )), + signer: Arc::new(create_test_signer(seed)), proxies: ValidatorProxies::Static(vec![PeerAddr { addr: node_addr, peer_id: PeerId::new(node_key.public_key()), diff --git a/core/primitives/src/test_utils.rs b/core/primitives/src/test_utils.rs index 302f997cd07..0a6879fcfe8 100644 --- a/core/primitives/src/test_utils.rs +++ b/core/primitives/src/test_utils.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::sync::Arc; -use near_crypto::{EmptySigner, PublicKey, Signature, Signer}; +use near_crypto::{EmptySigner, KeyType, PublicKey, Signature, Signer}; use near_primitives_core::types::ProtocolVersion; use crate::account::{AccessKey, AccessKeyPermission, Account}; @@ -18,7 +18,7 @@ use crate::transaction::{ TransferAction, }; use crate::types::{AccountId, Balance, BlockHeight, EpochId, EpochInfoProvider, Gas, Nonce}; -use crate::validator_signer::ValidatorSigner; +use crate::validator_signer::{InMemoryValidatorSigner, ValidatorSigner}; use crate::version::PROTOCOL_VERSION; pub fn account_new(amount: Balance, code_hash: CryptoHash) -> Account { @@ -586,3 +586,13 @@ impl EpochInfoProvider for MockEpochInfoProvider { pub fn encode(xs: &[u64]) -> Vec { xs.iter().flat_map(|it| it.to_le_bytes()).collect() } + +// Helper function that creates a new signer for a given account, that uses the account name as seed. +// Should be used only in tests. +pub fn create_test_signer(account_name: &str) -> InMemoryValidatorSigner { + InMemoryValidatorSigner::from_seed( + account_name.parse().unwrap(), + KeyType::ED25519, + account_name, + ) +} diff --git a/docs/practices/testing/test_utils.md b/docs/practices/testing/test_utils.md index fa0ecfaf31d..fe7932f49e5 100644 --- a/docs/practices/testing/test_utils.md +++ b/docs/practices/testing/test_utils.md @@ -20,8 +20,10 @@ let alice: AccountId = "alice.near".parse().unwrap() ### Signatures In memory signer (generates the key based on seed). There is a slight preference to use the seed that is matching the account name. +This will create a signer for account 'test' using 'test' as a seed. + ```rust -InMemoryValidatorSigner::from_seed("account".parse().unwrap(), KeyType::ED25519, "account".to_owned()) +let signer: InMemoryValidatorSigner = create_test_signer("test"); ``` ## Store diff --git a/integration-tests/src/tests/client/challenges.rs b/integration-tests/src/tests/client/challenges.rs index 2ee0266ab37..2b745fbc6ca 100644 --- a/integration-tests/src/tests/client/challenges.rs +++ b/integration-tests/src/tests/client/challenges.rs @@ -1,5 +1,6 @@ use assert_matches::assert_matches; use borsh::BorshSerialize; +use near_primitives::test_utils::create_test_signer; use crate::tests::client::process_blocks::create_nightshade_runtimes; use near_chain::validate::validate_challenge; @@ -24,7 +25,6 @@ use near_primitives::sharding::{EncodedShardChunk, ReedSolomonWrapper}; use near_primitives::transaction::SignedTransaction; use near_primitives::types::chunk_extra::ChunkExtra; use near_primitives::types::{AccountId, EpochId}; -use near_primitives::validator_signer::InMemoryValidatorSigner; use near_primitives::version::PROTOCOL_VERSION; use near_store::test_utils::create_test_store; use near_store::Trie; @@ -99,8 +99,7 @@ fn test_verify_block_double_sign_challenge() { env.process_block(0, b1.clone(), Provenance::NONE); - let signer = - InMemoryValidatorSigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let signer = create_test_signer("test0"); let mut block_merkle_tree = PartialMerkleTree::default(); block_merkle_tree.insert(*genesis.hash()); let b2 = Block::produce( @@ -333,8 +332,7 @@ fn test_verify_chunk_invalid_state_challenge() { ))]) .build(); let signer = InMemorySigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); - let validator_signer = - InMemoryValidatorSigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let validator_signer = create_test_signer("test0"); let genesis_hash = *env.clients[0].chain.genesis().hash(); env.produce_block(0, 1); env.clients[0].process_tx( diff --git a/integration-tests/src/tests/client/process_blocks.rs b/integration-tests/src/tests/client/process_blocks.rs index d5bdaf5078d..4178f4cbc5e 100644 --- a/integration-tests/src/tests/client/process_blocks.rs +++ b/integration-tests/src/tests/client/process_blocks.rs @@ -56,6 +56,7 @@ use near_primitives::sharding::{ }; use near_primitives::state_part::PartId; use near_primitives::syncing::{get_num_state_parts, ShardStateSyncResponseHeader, StatePartKey}; +use near_primitives::test_utils::create_test_signer; use near_primitives::transaction::{ Action, DeployContractAction, ExecutionStatus, FunctionCallAction, SignedTransaction, Transaction, @@ -81,11 +82,8 @@ pub fn set_block_protocol_version( block_producer: AccountId, protocol_version: ProtocolVersion, ) { - let validator_signer = InMemoryValidatorSigner::from_seed( - block_producer.clone(), - KeyType::ED25519, - block_producer.as_ref(), - ); + let validator_signer = create_test_signer(block_producer.as_str()); + block.mut_header().set_latest_protocol_version(protocol_version); block.mut_header().resign(&validator_signer); } @@ -373,11 +371,7 @@ fn receive_network_block() { let (last_block, block_merkle_tree) = res.unwrap().unwrap(); let mut block_merkle_tree = PartialMerkleTree::clone(&block_merkle_tree); block_merkle_tree.insert(last_block.header.hash); - let signer = InMemoryValidatorSigner::from_seed( - "test1".parse().unwrap(), - KeyType::ED25519, - "test1", - ); + let signer = create_test_signer("test1"); let next_block_ordinal = last_block.header.block_ordinal.unwrap() + 1; let block = Block::produce( PROTOCOL_VERSION, @@ -456,11 +450,7 @@ fn produce_block_with_approvals() { let (last_block, block_merkle_tree) = res.unwrap().unwrap(); let mut block_merkle_tree = PartialMerkleTree::clone(&block_merkle_tree); block_merkle_tree.insert(last_block.header.hash); - let signer1 = InMemoryValidatorSigner::from_seed( - "test2".parse().unwrap(), - KeyType::ED25519, - "test2", - ); + let signer1 = create_test_signer("test2"); let next_block_ordinal = last_block.header.block_ordinal.unwrap() + 1; let block = Block::produce( PROTOCOL_VERSION, @@ -504,8 +494,7 @@ fn produce_block_with_approvals() { format!("test{}", i) }) .unwrap(); - let signer = - InMemoryValidatorSigner::from_seed(s.clone(), KeyType::ED25519, s.as_ref()); + let signer = create_test_signer(s.as_str()); let approval = Approval::new( *block.hash(), block.header().height(), @@ -669,11 +658,7 @@ fn invalid_blocks_common(is_requested: bool) { let (last_block, block_merkle_tree) = res.unwrap().unwrap(); let mut block_merkle_tree = PartialMerkleTree::clone(&block_merkle_tree); block_merkle_tree.insert(last_block.header.hash); - let signer = InMemoryValidatorSigner::from_seed( - "test".parse().unwrap(), - KeyType::ED25519, - "test", - ); + let signer = create_test_signer("test"); let next_block_ordinal = last_block.header.block_ordinal.unwrap() + 1; let valid_block = Block::produce( PROTOCOL_VERSION, @@ -844,11 +829,7 @@ fn ban_peer_for_invalid_block_common(mode: InvalidBlockMode) { let block_producer_idx = block.header().height() as usize % validators.len(); let block_producer = &validators[block_producer_idx]; - let validator_signer1 = InMemoryValidatorSigner::from_seed( - block_producer.clone(), - KeyType::ED25519, - block_producer.as_ref(), - ); + let validator_signer1 = create_test_signer(block_producer.as_str()); sent_bad_blocks = true; let mut block_mut = block.clone(); match mode { @@ -1133,8 +1114,7 @@ fn test_time_attack() { chain_genesis, TEST_SEED, ); - let signer = - InMemoryValidatorSigner::from_seed("test1".parse().unwrap(), KeyType::ED25519, "test1"); + let signer = create_test_signer("test1"); let genesis = client.chain.get_block_by_height(0).unwrap(); let mut b1 = Block::empty_with_height(&genesis, 1, &signer); b1.mut_header().get_mut().inner_lite.timestamp = @@ -1168,20 +1148,15 @@ fn test_invalid_approvals() { chain_genesis, TEST_SEED, ); - let signer = - InMemoryValidatorSigner::from_seed("test1".parse().unwrap(), KeyType::ED25519, "test1"); + let signer = create_test_signer("test1"); let genesis = client.chain.get_block_by_height(0).unwrap(); let mut b1 = Block::empty_with_height(&genesis, 1, &signer); b1.mut_header().get_mut().inner_rest.approvals = (0..100) .map(|i| { let account_id = AccountId::try_from(format!("test{}", i)).unwrap(); Some( - InMemoryValidatorSigner::from_seed( - account_id.clone(), - KeyType::ED25519, - account_id.as_ref(), - ) - .sign_approval(&ApprovalInner::Endorsement(*genesis.hash()), 1), + create_test_signer(account_id.as_str()) + .sign_approval(&ApprovalInner::Endorsement(*genesis.hash()), 1), ) }) .collect(); @@ -1219,8 +1194,7 @@ fn test_invalid_gas_price() { chain_genesis, TEST_SEED, ); - let signer = - InMemoryValidatorSigner::from_seed("test1".parse().unwrap(), KeyType::ED25519, "test1"); + let signer = create_test_signer("test1"); let genesis = client.chain.get_block_by_height(0).unwrap(); let mut b1 = Block::empty_with_height(&genesis, 1, &signer); b1.mut_header().get_mut().inner_rest.gas_price = 0; @@ -1235,8 +1209,7 @@ fn test_invalid_height_too_large() { let mut env = TestEnv::builder(ChainGenesis::test()).build(); let b1 = env.clients[0].produce_block(1).unwrap().unwrap(); let _ = env.clients[0].process_block_test(b1.clone().into(), Provenance::PRODUCED).unwrap(); - let signer = - InMemoryValidatorSigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let signer = create_test_signer("test0"); let b2 = Block::empty_with_height(&b1, u64::MAX, &signer); let res = env.clients[0].process_block_test(b2.into(), Provenance::NONE); assert_matches!(res.unwrap_err(), Error::InvalidBlockHeight(_)); @@ -1948,8 +1921,7 @@ fn test_gas_price_overflow() { fn test_invalid_block_root() { let mut env = TestEnv::builder(ChainGenesis::test()).build(); let mut b1 = env.clients[0].produce_block(1).unwrap().unwrap(); - let signer = - InMemoryValidatorSigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let signer = create_test_signer("test0"); b1.mut_header().get_mut().inner_lite.block_merkle_root = CryptoHash::default(); b1.mut_header().resign(&signer); let res = env.clients[0].process_block_test(b1.into(), Provenance::NONE); @@ -2155,8 +2127,7 @@ fn test_block_height_processed_orphan() { let mut env = TestEnv::builder(ChainGenesis::test()).build(); let block = env.clients[0].produce_block(1).unwrap().unwrap(); let mut orphan_block = block; - let validator_signer = - InMemoryValidatorSigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let validator_signer = create_test_signer("test0"); orphan_block.mut_header().get_mut().prev_hash = hash(&[1]); orphan_block.mut_header().resign(&validator_signer); let block_height = orphan_block.header().height(); @@ -2224,8 +2195,7 @@ fn test_validate_chunk_extra() { } // Construct two blocks that contain the same chunk and make the chunk unavailable. - let validator_signer = - InMemoryValidatorSigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let validator_signer = create_test_signer("test0"); let next_height = last_block.header().height() + 1; let (encoded_chunk, merkle_paths, receipts) = create_chunk_on_height(&mut env.clients[0], next_height); @@ -2306,8 +2276,7 @@ fn test_gas_price_change_no_chunk() { let mut env = TestEnv::builder(chain_genesis) .runtime_adapters(create_nightshade_runtimes(&genesis, 1)) .build(); - let validator_signer = - InMemoryValidatorSigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let validator_signer = create_test_signer("test0"); for i in 1..=20 { let mut block = env.clients[0].produce_block(i).unwrap().unwrap(); if i <= 5 || (i > 10 && i <= 15) { @@ -2920,8 +2889,7 @@ fn test_fork_receipt_ids() { env.process_block(0, produced_block.clone(), Provenance::PRODUCED); // Construct two blocks that contain the same chunk and make the chunk unavailable. - let validator_signer = - InMemoryValidatorSigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let validator_signer = create_test_signer("test0"); let next_height = produced_block.header().height() + 1; let (encoded_chunk, _, _) = create_chunk_on_height(&mut env.clients[0], next_height); let mut block1 = env.clients[0].produce_block(next_height).unwrap().unwrap(); @@ -2968,8 +2936,7 @@ fn test_fork_execution_outcome() { } // Construct two blocks that contain the same chunk and make the chunk unavailable. - let validator_signer = - InMemoryValidatorSigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let validator_signer = create_test_signer("test0"); let next_height = last_height + 1; let (encoded_chunk, _, _) = create_chunk_on_height(&mut env.clients[0], next_height); let mut block1 = env.clients[0].produce_block(next_height).unwrap().unwrap(); @@ -3071,8 +3038,7 @@ fn test_header_version_downgrade() { let mut env = TestEnv::builder(chain_genesis) .runtime_adapters(create_nightshade_runtimes(&genesis, 1)) .build(); - let validator_signer = - InMemoryValidatorSigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let validator_signer = create_test_signer("test0"); for i in 1..10 { let block = env.clients[0].produce_block(i).unwrap().unwrap(); env.process_block(0, block, Provenance::NONE); @@ -3119,8 +3085,7 @@ fn test_node_shutdown_with_old_protocol_version() { let mut env = TestEnv::builder(ChainGenesis::test()) .runtime_adapters(create_nightshade_runtimes(&genesis, 1)) .build(); - let validator_signer = - InMemoryValidatorSigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let validator_signer = create_test_signer("test0"); for i in 1..=5 { let mut block = env.clients[0].produce_block(i).unwrap().unwrap(); block.mut_header().get_mut().inner_rest.latest_protocol_version = PROTOCOL_VERSION + 1; diff --git a/integration-tests/src/tests/client/runtimes.rs b/integration-tests/src/tests/client/runtimes.rs index dd7239363fa..2e485d9bbef 100644 --- a/integration-tests/src/tests/client/runtimes.rs +++ b/integration-tests/src/tests/client/runtimes.rs @@ -16,6 +16,7 @@ use near_primitives::hash::hash; use near_primitives::network::PeerId; use near_primitives::sharding::ShardChunkHeaderInner; use near_primitives::sharding::{PartialEncodedChunk, ShardChunkHeader}; +use near_primitives::test_utils::create_test_signer; use near_primitives::utils::MaybeValidated; use near_primitives::validator_signer::InMemoryValidatorSigner; use near_store::test_utils::create_test_store; @@ -45,8 +46,7 @@ fn create_runtimes(n: usize) -> Vec> { fn test_pending_approvals() { let mut env = TestEnv::builder(ChainGenesis::test()).runtime_adapters(create_runtimes(1)).build(); - let signer = - InMemoryValidatorSigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let signer = create_test_signer("test0"); let parent_hash = hash(&[1]); let approval = Approval::new(parent_hash, 0, 1, &signer); let peer_id = PeerId::random(); @@ -66,8 +66,7 @@ fn test_invalid_approvals() { .runtime_adapters(create_runtimes(1)) .network_adapters(vec![network_adapter]) .build(); - let signer = - InMemoryValidatorSigner::from_seed("random".parse().unwrap(), KeyType::ED25519, "random"); + let signer = create_test_signer("random"); let parent_hash = hash(&[1]); // Approval not from a validator. Should be dropped let approval = Approval::new(parent_hash, 1, 3, &signer); diff --git a/integration-tests/src/tests/nearcore/sync_nodes.rs b/integration-tests/src/tests/nearcore/sync_nodes.rs index e0e12801a7b..73905b09cfe 100644 --- a/integration-tests/src/tests/nearcore/sync_nodes.rs +++ b/integration-tests/src/tests/nearcore/sync_nodes.rs @@ -4,6 +4,7 @@ use std::time::Duration; use actix::{Actor, Addr, System}; use futures::{future, FutureExt}; +use near_primitives::test_utils::create_test_signer; use crate::genesis_helpers::genesis_block; use crate::test_helpers::heavy_test; @@ -23,7 +24,7 @@ use near_primitives::num_rational::Ratio; use near_primitives::transaction::SignedTransaction; use near_primitives::types::validator_stake::ValidatorStake; use near_primitives::types::{BlockHeightDelta, EpochId}; -use near_primitives::validator_signer::{InMemoryValidatorSigner, ValidatorSigner}; +use near_primitives::validator_signer::ValidatorSigner; use near_primitives::version::PROTOCOL_VERSION; use nearcore::config::{GenesisExt, TESTING_INIT_STAKE}; use nearcore::{load_test_config, start_with_config, NearConfig}; @@ -132,11 +133,7 @@ fn sync_nodes() { let nearcore::NearNode { client: client1, .. } = start_with_config(dir1.path(), near1).expect("start_with_config"); - let signer = InMemoryValidatorSigner::from_seed( - "other".parse().unwrap(), - KeyType::ED25519, - "other", - ); + let signer = create_test_signer("other"); let _ = add_blocks(vec![genesis_block], client1, 13, genesis.config.epoch_length, &signer); @@ -183,11 +180,7 @@ fn sync_after_sync_nodes() { let nearcore::NearNode { view_client: view_client2, .. } = start_with_config(dir2.path(), near2).expect("start_with_config"); - let signer = InMemoryValidatorSigner::from_seed( - "other".parse().unwrap(), - KeyType::ED25519, - "other", - ); + let signer = create_test_signer("other"); let blocks = add_blocks( vec![genesis_block], client1.clone(), diff --git a/integration-tests/src/tests/network/runner.rs b/integration-tests/src/tests/network/runner.rs index 6bf3242e2e0..64e4b6b39b6 100644 --- a/integration-tests/src/tests/network/runner.rs +++ b/integration-tests/src/tests/network/runner.rs @@ -4,7 +4,6 @@ use near_chain::test_utils::{KeyValueRuntime, ValidatorSchedule}; use near_chain::{Chain, ChainGenesis}; use near_chain_configs::ClientConfig; use near_client::{start_client, start_view_client}; -use near_crypto::KeyType; use near_network::actix::ActixSystem; use near_network::blacklist; use near_network::config; @@ -22,8 +21,8 @@ use near_o11y::testonly::init_test_logger; use near_o11y::WithSpanContextExt; use near_primitives::block::GenesisId; use near_primitives::network::PeerId; +use near_primitives::test_utils::create_test_signer; use near_primitives::types::{AccountId, ValidatorId}; -use near_primitives::validator_signer::InMemoryValidatorSigner; use near_telemetry::{TelemetryActor, TelemetryConfig}; use std::collections::HashSet; use std::future::Future; @@ -55,11 +54,7 @@ fn setup_network_node( vs, 5, )); - let signer = Arc::new(InMemoryValidatorSigner::from_seed( - account_id.clone(), - KeyType::ED25519, - account_id.as_ref(), - )); + let signer = Arc::new(create_test_signer(account_id.as_str())); let telemetry_actor = TelemetryActor::new(TelemetryConfig::default()).start(); let db = store.into_inner(near_store::Temperature::Hot); diff --git a/nearcore/src/config.rs b/nearcore/src/config.rs index c755a10a4ef..a032403a2dc 100644 --- a/nearcore/src/config.rs +++ b/nearcore/src/config.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use std::time::Duration; use anyhow::{anyhow, bail, Context}; +use near_primitives::test_utils::create_test_signer; use near_primitives::time::Clock; use num_rational::Rational32; use serde::{Deserialize, Serialize}; @@ -1056,12 +1057,8 @@ pub fn create_testnet_configs_from_seeds( fixed_shards: Option>, ) -> (Vec, Vec, Vec, Genesis) { let num_validator_seats = (seeds.len() - num_non_validator_seats as usize) as NumSeats; - let validator_signers = seeds - .iter() - .map(|seed| { - InMemoryValidatorSigner::from_seed(seed.parse().unwrap(), KeyType::ED25519, seed) - }) - .collect::>(); + let validator_signers = + seeds.iter().map(|seed| create_test_signer(seed.as_str())).collect::>(); let network_signers = seeds .iter() .map(|seed| InMemorySigner::from_seed("node".parse().unwrap(), KeyType::ED25519, seed)) @@ -1331,11 +1328,7 @@ pub fn load_test_config(seed: &str, port: u16, genesis: Genesis) -> NearConfig { } else { let signer = Arc::new(InMemorySigner::from_seed(seed.parse().unwrap(), KeyType::ED25519, seed)); - let validator_signer = Arc::new(InMemoryValidatorSigner::from_seed( - seed.parse().unwrap(), - KeyType::ED25519, - seed, - )) as Arc; + let validator_signer = Arc::new(create_test_signer(seed)) as Arc; (signer, Some(validator_signer)) }; NearConfig::new(config, genesis, signer.into(), validator_signer).unwrap() diff --git a/nearcore/src/runtime/mod.rs b/nearcore/src/runtime/mod.rs index 443743527b7..592e712d774 100644 --- a/nearcore/src/runtime/mod.rs +++ b/nearcore/src/runtime/mod.rs @@ -1528,6 +1528,7 @@ mod test { use std::collections::BTreeSet; use near_chain::{Chain, ChainGenesis}; + use near_primitives::test_utils::create_test_signer; use near_primitives::types::validator_stake::ValidatorStake; use num_rational::Ratio; @@ -1542,7 +1543,7 @@ mod test { use near_primitives::types::{ BlockHeightDelta, Nonce, ValidatorId, ValidatorInfoIdentifier, ValidatorKickoutReason, }; - use near_primitives::validator_signer::{InMemoryValidatorSigner, ValidatorSigner}; + use near_primitives::validator_signer::ValidatorSigner; use near_primitives::views::{ AccountView, CurrentEpochValidatorInfo, EpochValidatorInfo, NextEpochValidatorInfo, ValidatorKickoutView, @@ -1958,10 +1959,8 @@ mod test { .map(|i| AccountId::try_from(format!("test{}", i + 1)).unwrap()) .collect::>(); let mut env = TestEnv::new(vec![validators.clone()], 2, false); - let block_producers: Vec<_> = validators - .iter() - .map(|id| InMemoryValidatorSigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) - .collect(); + let block_producers: Vec<_> = + validators.iter().map(|id| create_test_signer(id.as_str())).collect(); let signer = InMemorySigner::from_seed( validators[0].clone(), KeyType::ED25519, @@ -1970,11 +1969,7 @@ mod test { // test1 doubles stake and the new account stakes the same, so test2 will be kicked out.` let staking_transaction = stake(1, &signer, &block_producers[0], TESTING_INIT_STAKE * 2); let new_account = AccountId::try_from(format!("test{}", num_nodes + 1)).unwrap(); - let new_validator = InMemoryValidatorSigner::from_seed( - new_account.clone(), - KeyType::ED25519, - new_account.as_ref(), - ); + let new_validator = create_test_signer(new_account.as_str()); let new_signer = InMemorySigner::from_seed(new_account.clone(), KeyType::ED25519, new_account.as_ref()); let create_account_transaction = SignedTransaction::create_account( @@ -2058,10 +2053,8 @@ mod test { .map(|i| AccountId::try_from(format!("test{}", i + 1)).unwrap()) .collect::>(); let mut env = TestEnv::new(vec![validators.clone()], 2, false); - let block_producers: Vec<_> = validators - .iter() - .map(|id| InMemoryValidatorSigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) - .collect(); + let block_producers: Vec<_> = + validators.iter().map(|id| create_test_signer(id.as_str())).collect(); let signer = InMemorySigner::from_seed( validators[0].clone(), KeyType::ED25519, @@ -2099,10 +2092,8 @@ mod test { .map(|i| AccountId::try_from(format!("test{}", i + 1)).unwrap()) .collect::>(); let mut env = TestEnv::new(vec![validators.clone()], 4, false); - let block_producers: Vec<_> = validators - .iter() - .map(|id| InMemoryValidatorSigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) - .collect(); + let block_producers: Vec<_> = + validators.iter().map(|id| create_test_signer(id.as_str())).collect(); let signers: Vec<_> = validators .iter() .map(|id| InMemorySigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) @@ -2203,10 +2194,8 @@ mod test { .map(|i| AccountId::try_from(format!("test{}", i + 1)).unwrap()) .collect::>(); let mut env = TestEnv::new(vec![validators.clone()], 5, false); - let block_producers: Vec<_> = validators - .iter() - .map(|id| InMemoryValidatorSigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) - .collect(); + let block_producers: Vec<_> = + validators.iter().map(|id| create_test_signer(id.as_str())).collect(); let signers: Vec<_> = validators .iter() .map(|id| InMemorySigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) @@ -2276,10 +2265,8 @@ mod test { .map(|i| AccountId::try_from(format!("test{}", i + 1)).unwrap()) .collect::>(); let mut env = TestEnv::new(vec![validators.clone()], 2, false); - let block_producers: Vec<_> = validators - .iter() - .map(|id| InMemoryValidatorSigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) - .collect(); + let block_producers: Vec<_> = + validators.iter().map(|id| create_test_signer(id.as_str())).collect(); let signer = InMemorySigner::from_seed( validators[0].clone(), KeyType::ED25519, @@ -2371,10 +2358,8 @@ mod test { .map(|i| AccountId::try_from(format!("test{}", i + 1)).unwrap()) .collect::>(); let mut env = TestEnv::new(vec![validators.clone()], 2, false); - let block_producers: Vec<_> = validators - .iter() - .map(|id| InMemoryValidatorSigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) - .collect(); + let block_producers: Vec<_> = + validators.iter().map(|id| create_test_signer(id.as_str())).collect(); let signer = InMemorySigner::from_seed( validators[0].clone(), KeyType::ED25519, @@ -2522,10 +2507,8 @@ mod test { TrackedConfig::Accounts(vec![validators[1].clone()]), true, ); - let block_producers: Vec<_> = validators - .iter() - .map(|id| InMemoryValidatorSigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) - .collect(); + let block_producers: Vec<_> = + validators.iter().map(|id| create_test_signer(id.as_str())).collect(); let signer = InMemorySigner::from_seed( validators[1].clone(), KeyType::ED25519, @@ -2636,10 +2619,8 @@ mod test { .map(|i| AccountId::try_from(format!("test{}", i + 1)).unwrap()) .collect::>(); let mut env = TestEnv::new(vec![validators.clone()], 3, false); - let block_producers: Vec<_> = validators - .iter() - .map(|id| InMemoryValidatorSigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) - .collect(); + let block_producers: Vec<_> = + validators.iter().map(|id| create_test_signer(id.as_str())).collect(); let signer = InMemorySigner::from_seed( validators[2].clone(), @@ -2825,10 +2806,8 @@ mod test { // validator 20000, ); - let block_producers: Vec<_> = validators - .iter() - .map(|id| InMemoryValidatorSigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) - .collect(); + let block_producers: Vec<_> = + validators.iter().map(|id| create_test_signer(id.as_str())).collect(); let signers: Vec<_> = validators .iter() .map(|id| InMemorySigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) @@ -2897,10 +2876,8 @@ mod test { // validator 20000, ); - let block_producers: Vec<_> = validators - .iter() - .map(|id| InMemoryValidatorSigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) - .collect(); + let block_producers: Vec<_> = + validators.iter().map(|id| create_test_signer(id.as_str())).collect(); let signers: Vec<_> = validators .iter() .map(|id| InMemorySigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) @@ -2953,10 +2930,8 @@ mod test { let validators = (0..num_nodes).map(|i| format!("test{}", i + 1).parse().unwrap()).collect::>(); let mut env = TestEnv::new(vec![validators.clone()], epoch_length, true); - let block_producers: Vec<_> = validators - .iter() - .map(|id| InMemoryValidatorSigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) - .collect(); + let block_producers: Vec<_> = + validators.iter().map(|id| create_test_signer(id.as_str())).collect(); for _ in 0..(epoch_length + 1) { env.step_default(vec![]); @@ -2985,10 +2960,8 @@ mod test { .map(|i| AccountId::try_from(format!("test{}", i + 1)).unwrap()) .collect::>(); let mut env = TestEnv::new(vec![validators.clone()], 4, false); - let block_producers: Vec<_> = validators - .iter() - .map(|id| InMemoryValidatorSigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) - .collect(); + let block_producers: Vec<_> = + validators.iter().map(|id| create_test_signer(id.as_str())).collect(); let signers: Vec<_> = validators .iter() .map(|id| InMemorySigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) @@ -3034,10 +3007,8 @@ mod test { .map(|i| AccountId::try_from(format!("test{}", i + 1)).unwrap()) .collect::>(); let mut env = TestEnv::new(vec![validators.clone()], 4, false); - let block_producers: Vec<_> = validators - .iter() - .map(|id| InMemoryValidatorSigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) - .collect(); + let block_producers: Vec<_> = + validators.iter().map(|id| create_test_signer(id.as_str())).collect(); let signers: Vec<_> = validators .iter() .map(|id| InMemorySigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) @@ -3059,10 +3030,8 @@ mod test { .map(|i| AccountId::try_from(format!("test{}", i + 1)).unwrap()) .collect::>(); let mut env = TestEnv::new(vec![validators.clone()], 4, false); - let block_producers: Vec<_> = validators - .iter() - .map(|id| InMemoryValidatorSigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) - .collect(); + let block_producers: Vec<_> = + validators.iter().map(|id| create_test_signer(id.as_str())).collect(); let signers: Vec<_> = validators .iter() .map(|id| InMemorySigner::from_seed(id.clone(), KeyType::ED25519, id.as_ref())) diff --git a/test-utils/testlib/src/process_blocks.rs b/test-utils/testlib/src/process_blocks.rs index 0453bc7b92d..28491d2b894 100644 --- a/test-utils/testlib/src/process_blocks.rs +++ b/test-utils/testlib/src/process_blocks.rs @@ -1,6 +1,5 @@ use near_chain::{Block, BlockHeader}; -use near_crypto::KeyType; -use near_primitives::validator_signer::InMemoryValidatorSigner; +use near_primitives::test_utils::create_test_signer; use std::sync::Arc; pub fn set_no_chunk_in_block(block: &mut Block, prev_block: &Block) { @@ -41,7 +40,6 @@ pub fn set_no_chunk_in_block(block: &mut Block, prev_block: &Block) { header.inner_rest.gas_price = prev_block.header().gas_price(); } } - let validator_signer = - InMemoryValidatorSigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let validator_signer = create_test_signer("test0"); block.mut_header().resign(&validator_signer); } From 6ae192b44dff2fd3ef922bd5086830082f5d59d2 Mon Sep 17 00:00:00 2001 From: mzhangmzz <34969888+mzhangmzz@users.noreply.github.com> Date: Wed, 7 Dec 2022 13:54:31 -0500 Subject: [PATCH 075/188] Fix a bug in processing skips approvals when there are forks (#8165) * fix a bug in skip procesing * add test * remove comments --- chain/client/src/client.rs | 23 ++++++++++++++++++-- chain/client/src/tests/doomslug.rs | 34 ++++++++++++++++++++++++++++++ chain/client/src/tests/mod.rs | 1 + 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 chain/client/src/tests/doomslug.rs diff --git a/chain/client/src/client.rs b/chain/client/src/client.rs index ed0005a0fb1..81b9cf02c7e 100644 --- a/chain/client/src/client.rs +++ b/chain/client/src/client.rs @@ -1728,8 +1728,27 @@ impl Client { let parent_hash = match inner { ApprovalInner::Endorsement(parent_hash) => *parent_hash, ApprovalInner::Skip(parent_height) => { - match self.chain.get_block_header_by_height(*parent_height) { - Ok(header) => *header.hash(), + match self.chain.store().get_all_block_hashes_by_height(*parent_height) { + Ok(hashes) => { + // If there is more than one block at the height, all of them will be + // eligible to build the next block on, so we just pick one. + let hash = hashes.values().flatten().next(); + match hash { + Some(hash) => *hash, + None => { + self.handle_process_approval_error( + approval, + approval_type, + true, + near_chain::Error::Other(format!( + "Cannot find any block on height {}", + parent_height + )), + ); + return; + } + } + } Err(e) => { self.handle_process_approval_error(approval, approval_type, true, e); return; diff --git a/chain/client/src/tests/doomslug.rs b/chain/client/src/tests/doomslug.rs new file mode 100644 index 00000000000..628f851c7ad --- /dev/null +++ b/chain/client/src/tests/doomslug.rs @@ -0,0 +1,34 @@ +use crate::test_utils::TestEnv; +use near_chain::{ChainGenesis, Provenance}; +use near_crypto::KeyType; +use near_o11y::testonly::init_test_logger; +use near_primitives::block::{Approval, ApprovalType}; +use near_primitives::hash::CryptoHash; +use near_primitives::validator_signer::InMemoryValidatorSigner; + +/// This file contains tests that test the interaction of client and doomslug, including how client handles approvals, etc. +/// It does not include the unit tests for the Doomslug class. That is located in chain/chain/src/doomslug.rs + +// This tests the scenario that if the chain switch back and forth in between two forks, client code +// can process the skip messages correctly and send it to doomslug. Specifically, it tests the following +// case: +// existing chain looks like 0 - 1 +// \ 2 +// test that if the node receives Skip(2, 4), it can process it successfully. +#[test] +fn test_processing_skips_on_forks() { + init_test_logger(); + + let mut env = + TestEnv::builder(ChainGenesis::test()).clients_count(2).validator_seats(2).build(); + let b1 = env.clients[1].produce_block(1).unwrap().unwrap(); + let b2 = env.clients[0].produce_block(2).unwrap().unwrap(); + assert_eq!(b1.header().prev_hash(), b2.header().prev_hash()); + env.process_block(1, b1, Provenance::NONE); + env.process_block(1, b2, Provenance::NONE); + let validator_signer = + InMemoryValidatorSigner::from_seed("test1".parse().unwrap(), KeyType::ED25519, "test1"); + let approval = Approval::new(CryptoHash::default(), 1, 3, &validator_signer); + env.clients[1].collect_block_approval(&approval, ApprovalType::SelfApproval); + assert!(!env.clients[1].doomslug.approval_status_at_height(&3).approvals.is_empty()); +} diff --git a/chain/client/src/tests/mod.rs b/chain/client/src/tests/mod.rs index f26dbffaad6..3b45a4c05c7 100644 --- a/chain/client/src/tests/mod.rs +++ b/chain/client/src/tests/mod.rs @@ -3,6 +3,7 @@ mod catching_up; mod chunks_management; mod consensus; mod cross_shard_tx; +mod doomslug; mod maintenance_windows; mod process_blocks; mod query_client; From fd97a60e34fb2340471ae1c843e26d7f5aca8a0a Mon Sep 17 00:00:00 2001 From: wacban Date: Wed, 7 Dec 2022 20:52:07 +0000 Subject: [PATCH 076/188] [fix] Fix localnet config - local ports should be set to true (#8180) --- nearcore/src/config.rs | 3 ++- neard/src/cli.rs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/nearcore/src/config.rs b/nearcore/src/config.rs index a032403a2dc..905bfad1f5a 100644 --- a/nearcore/src/config.rs +++ b/nearcore/src/config.rs @@ -1163,6 +1163,7 @@ pub fn init_testnet_configs( num_validator_seats: NumSeats, num_non_validator_seats: NumSeats, prefix: &str, + local_ports: bool, archive: bool, fixed_shards: bool, ) { @@ -1171,7 +1172,7 @@ pub fn init_testnet_configs( num_validator_seats, num_non_validator_seats, prefix, - false, + local_ports, archive, fixed_shards, ); diff --git a/neard/src/cli.rs b/neard/src/cli.rs index b48b0ba91bb..14e071adc5f 100644 --- a/neard/src/cli.rs +++ b/neard/src/cli.rs @@ -564,6 +564,7 @@ impl LocalnetCmd { self.validators, self.non_validators, &self.prefix, + true, self.archival_nodes, self.fixed_shards, ); From 6be3306a7a7b85f976fdd27b35be315612bbe2dd Mon Sep 17 00:00:00 2001 From: wacban Date: Thu, 8 Dec 2022 09:04:04 +0000 Subject: [PATCH 077/188] [doc] Fix a link to the testing page in the CONTRIBUTING.md (#8172) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d9fb20b805..4e28aa0f06b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,7 +67,7 @@ following steps when creating a PR: 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 - document](https://github.com/nearprotocol/nearcore/wiki/Writing-tests-for-nearcore) + document](https://github.com/near/nearcore/blob/master/docs/practices/testing/README.md) for our testing guidelines and an overview of the testing infrastructure. 4. When ready, send a pull request against the `master` branch of the `nearcore` repository. From 1347142b4728e12e1bb3f76eb393da46d68e9a8a Mon Sep 17 00:00:00 2001 From: pompon0 Date: Thu, 8 Dec 2022 12:06:36 +0100 Subject: [PATCH 078/188] removed edge field from Connection (#8160) removed edge field from Connection to avoid redundancy with GraphSnapshot.local_edges removed sending a duplicate RoutingTableUpdate message in response to RequestUpdateNonce --- chain/network/src/peer/peer_actor.rs | 32 ++++--- .../src/peer_manager/connection/mod.rs | 19 +--- .../src/peer_manager/network_state/mod.rs | 11 ++- .../src/peer_manager/network_state/routing.rs | 7 -- .../src/peer_manager/peer_manager_actor.rs | 6 +- chain/network/src/peer_manager/testonly.rs | 9 +- .../src/peer_manager/tests/accounts_data.rs | 8 +- .../src/peer_manager/tests/connection_pool.rs | 86 +++++++++++++++++++ chain/network/src/peer_manager/tests/nonce.rs | 8 +- chain/network/src/peer_manager/tests/tier1.rs | 18 ++-- 10 files changed, 144 insertions(+), 60 deletions(-) diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index 04ead509189..989eb78f868 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -1,5 +1,4 @@ use crate::accounts_data; -use crate::concurrency::arc_mutex::ArcMutex; use crate::concurrency::atomic_cell::AtomicCell; use crate::concurrency::demux; use crate::network_protocol::{ @@ -586,7 +585,6 @@ impl PeerActor { tier, addr: ctx.address(), peer_info: peer_info.clone(), - edge: ArcMutex::new(edge), owned_account: handshake.owned_account.clone(), genesis_id: handshake.sender_chain_info.genesis_id.clone(), tracked_shards: handshake.sender_chain_info.tracked_shards.clone(), @@ -658,7 +656,8 @@ impl PeerActor { let network_state = self.network_state.clone(); let clock = self.clock.clone(); let conn = conn.clone(); - async move { network_state.register(&clock,conn).await } + let edge = edge.clone(); + async move { network_state.register(&clock,edge,conn).await } }) .map(move |res, act: &mut PeerActor, ctx| { match res { @@ -766,7 +765,7 @@ impl PeerActor { act.network_state.config.event_sink.push(Event::HandshakeCompleted(HandshakeCompletedEvent{ stream_id: act.stream_id, - edge: conn.edge.load().as_ref().clone(), + edge, tier: conn.tier, })); }, @@ -1117,27 +1116,26 @@ impl PeerActor { let network_state = self.network_state.clone(); ctx.spawn(wrap_future(async move { let peer_id = &conn.peer_info.id; - let edge = match network_state.graph.load().local_edges.get(peer_id) { + match network_state.graph.load().local_edges.get(peer_id) { Some(cur_edge) if cur_edge.edge_type() == EdgeState::Active && cur_edge.nonce() >= edge_info.nonce => { - cur_edge.clone() + // Found a newer local edge, so just send it to the peer. + conn.send_message(Arc::new(PeerMessage::SyncRoutingTable( + RoutingTableUpdate::from_edges(vec![cur_edge.clone()]), + ))); } - _ => match network_state - .finalize_edge(&clock, peer_id.clone(), edge_info) - .await - { - Ok(edge) => edge, - Err(ban_reason) => { + // Sign the edge and broadcast it to everyone (finalize_edge does both). + _ => { + if let Err(ban_reason) = network_state + .finalize_edge(&clock, peer_id.clone(), edge_info) + .await + { conn.stop(Some(ban_reason)); - return; } - }, + } }; - conn.send_message(Arc::new(PeerMessage::SyncRoutingTable( - RoutingTableUpdate::from_edges(vec![edge]), - ))); network_state .config .event_sink diff --git a/chain/network/src/peer_manager/connection/mod.rs b/chain/network/src/peer_manager/connection/mod.rs index 89231af4cce..728585020ab 100644 --- a/chain/network/src/peer_manager/connection/mod.rs +++ b/chain/network/src/peer_manager/connection/mod.rs @@ -2,7 +2,7 @@ use crate::concurrency::arc_mutex::ArcMutex; use crate::concurrency::atomic_cell::AtomicCell; use crate::concurrency::demux; use crate::network_protocol::{ - Edge, PeerInfo, PeerMessage, RoutedMessageBody, SignedAccountData, SignedOwnedAccount, + PeerInfo, PeerMessage, RoutedMessageBody, SignedAccountData, SignedOwnedAccount, SyncAccountsData, }; use crate::peer::peer_actor; @@ -78,7 +78,6 @@ pub(crate) struct Connection { pub addr: actix::Addr, pub peer_info: PeerInfo, - pub edge: ArcMutex, /// AccountKey ownership proof. pub owned_account: Option, /// Chain Id and hash of genesis block. @@ -103,7 +102,7 @@ pub(crate) struct Connection { /// prometheus gauge point guard. pub _peer_connections_metric: metrics::GaugePoint, - /// A helper data structure for limiting reading, reporting stats. + /// Demultiplexer for the calls to send_accounts_data(). pub send_accounts_data_demux: demux::Demux>, ()>, } @@ -111,7 +110,6 @@ impl fmt::Debug for Connection { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Connection") .field("peer_info", &self.peer_info) - .field("edge", &self.edge.load()) .field("peer_type", &self.peer_type) .field("established_time", &self.established_time) .finish() @@ -413,19 +411,6 @@ impl Pool { ((), pool) }); } - /// Update the edge in the pool (if it is newer). - pub fn update_edge(&self, new_edge: &Edge) { - let pool = self.load(); - let Some(other) = new_edge.other(&pool.me) else { return }; - let Some(conn) = pool.ready.get(other) else { return }; - // Returns an error if the current edge is not older than new_edge. - let _ = conn.edge.try_update(|e| { - if e.nonce() >= new_edge.nonce() { - return Err(()); - } - Ok(((), new_edge.clone())) - }); - } /// Send message to peer that belongs to our active set /// Return whether the message is sent or not. diff --git a/chain/network/src/peer_manager/network_state/mod.rs b/chain/network/src/peer_manager/network_state/mod.rs index c4f75e7869e..0617ead91b9 100644 --- a/chain/network/src/peer_manager/network_state/mod.rs +++ b/chain/network/src/peer_manager/network_state/mod.rs @@ -254,6 +254,7 @@ impl NetworkState { pub async fn register( self: &Arc, clock: &time::Clock, + edge: Edge, conn: Arc, ) -> Result<(), RegisterPeerError> { let this = self.clone(); @@ -286,6 +287,10 @@ impl NetworkState { return Err(RegisterPeerError::NotTier1Peer); } } + let (_, ok) = this.graph.verify(vec![edge]).await; + if !ok { + return Err(RegisterPeerError::InvalidEdge); + } this.tier1.insert_ready(conn).map_err(RegisterPeerError::PoolError)?; } tcp::Tier::T2 => { @@ -301,10 +306,10 @@ impl NetworkState { return Err(RegisterPeerError::ConnectionLimitExceeded); } } - // Verify and broadcast the edge of the connection. Only then insert the new - // connection to TIER2 pool, so that nothing is broadcasted to conn. + // First verify and broadcast the edge of the connection, so that in case + // it is invalid, the connection is not added to the pool. // TODO(gprusak): consider actually banning the peer for consistency. - this.add_edges(&clock, vec![conn.edge.load().as_ref().clone()]) + this.add_edges(&clock, vec![edge]) .await .map_err(|_: ReasonForBan| RegisterPeerError::InvalidEdge)?; this.tier2.insert_ready(conn.clone()).map_err(RegisterPeerError::PoolError)?; diff --git a/chain/network/src/peer_manager/network_state/routing.rs b/chain/network/src/peer_manager/network_state/routing.rs index cea54f684fd..a272f0c62af 100644 --- a/chain/network/src/peer_manager/network_state/routing.rs +++ b/chain/network/src/peer_manager/network_state/routing.rs @@ -99,13 +99,6 @@ impl NetworkState { return result; } - // Select local edges (where we are one of the peers) - and update the peer's Connection nonces. - for e in &edges { - if let Some(_) = e.other(&self.config.node_id()) { - self.tier2.update_edge(&e); - } - } - let this = self.clone(); let clock = clock.clone(); let _ = self diff --git a/chain/network/src/peer_manager/peer_manager_actor.rs b/chain/network/src/peer_manager/peer_manager_actor.rs index 86d8725a840..78fc929dacf 100644 --- a/chain/network/src/peer_manager/peer_manager_actor.rs +++ b/chain/network/src/peer_manager/peer_manager_actor.rs @@ -592,6 +592,7 @@ impl PeerManagerActor { let tier1 = self.state.tier1.load(); let tier2 = self.state.tier2.load(); let now = self.clock.now(); + let graph = self.state.graph.load(); let connected_peer = |cp: &Arc| ConnectedPeerInfo { full_peer_info: cp.full_peer_info(), received_bytes_per_sec: cp.stats.received_bytes_per_sec.load(Ordering::Relaxed), @@ -600,7 +601,10 @@ impl PeerManagerActor { last_time_received_message: cp.last_time_received_message.load(), connection_established_time: cp.established_time, peer_type: cp.peer_type, - nonce: cp.edge.load().nonce(), + nonce: match graph.local_edges.get(&cp.peer_info.id) { + Some(e) => e.nonce(), + None => 0, + }, }; NetworkInfo { connected_peers: tier2.ready.values().map(connected_peer).collect(), diff --git a/chain/network/src/peer_manager/testonly.rs b/chain/network/src/peer_manager/testonly.rs index 98196091a68..801b0e2e48f 100644 --- a/chain/network/src/peer_manager/testonly.rs +++ b/chain/network/src/peer_manager/testonly.rs @@ -64,12 +64,15 @@ pub fn unwrap_sync_accounts_data_processed(ev: Event) -> Option ChainInfo { +pub(crate) fn make_chain_info( + chain: &data::Chain, + validators: &[&config::NetworkConfig], +) -> ChainInfo { // Construct ChainInfo with tier1_accounts set to `validators`. let mut chain_info = chain.get_chain_info(); let mut account_keys = AccountKeys::new(); - for pm in validators { - let s = &pm.cfg.validator.as_ref().unwrap().signer; + for cfg in validators { + let s = &cfg.validator.as_ref().unwrap().signer; account_keys.entry(s.validator_id().clone()).or_default().insert(s.public_key()); } chain_info.tier1_accounts = Arc::new(account_keys); diff --git a/chain/network/src/peer_manager/tests/accounts_data.rs b/chain/network/src/peer_manager/tests/accounts_data.rs index 1a1a7b62c97..faa8a477e3f 100644 --- a/chain/network/src/peer_manager/tests/accounts_data.rs +++ b/chain/network/src/peer_manager/tests/accounts_data.rs @@ -140,7 +140,10 @@ async fn gradual_epoch_change() { for ids in (0..pms.len()).permutations(pms.len()) { tracing::info!(target:"test", "permutation {ids:?}"); clock.advance(time::Duration::hours(1)); - let chain_info = testonly::make_chain_info(&chain, &pms.iter().collect::>()[..]); + let chain_info = testonly::make_chain_info( + &chain, + &pms.iter().map(|pm| &pm.cfg).collect::>()[..], + ); let mut want = HashSet::new(); // Advance epoch in the given order. @@ -217,7 +220,8 @@ async fn rate_limiting() { } // Construct ChainInfo with tier1_accounts containing all validators. - let chain_info = testonly::make_chain_info(&chain, &pms.iter().collect::>()[..]); + let chain_info = + testonly::make_chain_info(&chain, &pms.iter().map(|pm| &pm.cfg).collect::>()[..]); clock.advance(time::Duration::hours(1)); diff --git a/chain/network/src/peer_manager/tests/connection_pool.rs b/chain/network/src/peer_manager/tests/connection_pool.rs index 4258425964b..2e4ebfee685 100644 --- a/chain/network/src/peer_manager/tests/connection_pool.rs +++ b/chain/network/src/peer_manager/tests/connection_pool.rs @@ -224,3 +224,89 @@ async fn owned_account_conflict() { drop(conn1); drop(pm); } + +#[tokio::test] +async fn invalid_edge() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + let pm = peer_manager::testonly::start( + clock.clock(), + near_store::db::TestDB::new(), + chain.make_config(rng), + chain.clone(), + ) + .await; + let cfg = chain.make_config(rng); + let chain_info = peer_manager::testonly::make_chain_info(&chain, &[&cfg]); + pm.set_chain_info(chain_info).await; + + let testcases = [ + ( + "wrong key", + PartialEdgeInfo::new(&cfg.node_id(), &pm.cfg.node_id(), 1, &data::make_secret_key(rng)), + ), + ( + "wrong source", + PartialEdgeInfo::new(&data::make_peer_id(rng), &pm.cfg.node_id(), 1, &cfg.node_key), + ), + ( + "wrong target", + PartialEdgeInfo::new(&cfg.node_id(), &data::make_peer_id(rng), 1, &cfg.node_key), + ), + ]; + + for (name, edge) in &testcases { + for tier in [tcp::Tier::T1, tcp::Tier::T2] { + tracing::info!(target:"test","{name} {tier:?}"); + let stream = tcp::Stream::connect(&pm.peer_info(), tier).await.unwrap(); + let stream_id = stream.id(); + let port = stream.local_addr.port(); + let mut events = pm.events.from_now(); + let mut stream = Stream::new(Some(Encoding::Proto), stream); + let vc = cfg.validator.clone().unwrap(); + let handshake = Handshake { + protocol_version: PROTOCOL_VERSION, + oldest_supported_version: PROTOCOL_VERSION, + sender_peer_id: cfg.node_id(), + target_peer_id: pm.cfg.node_id(), + sender_listen_port: Some(port), + sender_chain_info: chain.get_peer_chain_info(), + partial_edge_info: edge.clone(), + owned_account: Some( + OwnedAccount { + account_key: vc.signer.public_key().clone(), + peer_id: cfg.node_id(), + timestamp: clock.now_utc(), + } + .sign(vc.signer.as_ref()), + ), + }; + let handshake = match tier { + tcp::Tier::T1 => PeerMessage::Tier1Handshake(handshake), + tcp::Tier::T2 => PeerMessage::Tier2Handshake(handshake), + }; + stream.write(&handshake).await; + let reason = events + .recv_until(|ev| match ev { + Event::PeerManager(PME::ConnectionClosed(ev)) if ev.stream_id == stream_id => { + Some(ev.reason) + } + Event::PeerManager(PME::HandshakeCompleted(ev)) + if ev.stream_id == stream_id => + { + panic!("PeerManager accepted the handshake") + } + _ => None, + }) + .await; + assert_eq!( + ClosingReason::RejectedByPeerManager(RegisterPeerError::InvalidEdge), + reason + ); + } + } +} diff --git a/chain/network/src/peer_manager/tests/nonce.rs b/chain/network/src/peer_manager/tests/nonce.rs index d447af00416..10a2d3c6e05 100644 --- a/chain/network/src/peer_manager/tests/nonce.rs +++ b/chain/network/src/peer_manager/tests/nonce.rs @@ -154,16 +154,16 @@ async fn test_nonce_refresh() { // Check that the nonces were properly updates on both pm and pm2 states. let pm_peer_info = pm.peer_info().id.clone(); let pm2_nonce = pm2 - .with_state(|s| async move { - s.tier2.load().ready.get(&pm_peer_info).unwrap().edge.load().nonce() - }) + .with_state( + |s| async move { s.graph.load().local_edges.get(&pm_peer_info).unwrap().nonce() }, + ) .await; assert_eq!(Edge::nonce_to_utc(pm2_nonce).unwrap().unwrap(), new_nonce_utc); let pm_nonce = pm .with_state(|s| async move { - s.tier2.load().ready.get(&pm2.peer_info().id).unwrap().edge.load().nonce() + s.graph.load().local_edges.get(&pm2.peer_info().id).unwrap().nonce() }) .await; diff --git a/chain/network/src/peer_manager/tests/tier1.rs b/chain/network/src/peer_manager/tests/tier1.rs index 2367fccb3e3..8e0ac5260e6 100644 --- a/chain/network/src/peer_manager/tests/tier1.rs +++ b/chain/network/src/peer_manager/tests/tier1.rs @@ -108,7 +108,7 @@ async fn first_proxy_advertisement() { chain.clone(), ) .await; - let chain_info = peer_manager::testonly::make_chain_info(&chain, &[&pm]); + let chain_info = peer_manager::testonly::make_chain_info(&chain, &[&pm.cfg]); tracing::info!(target:"test", "set_chain_info()"); // TODO(gprusak): The default config constructed via chain.make_config(), // currently returns a validator config with its own server addr in the list of TIER1 proxies. @@ -150,7 +150,10 @@ async fn direct_connections() { } tracing::info!(target:"test", "Set chain info."); - let chain_info = peer_manager::testonly::make_chain_info(&chain, &pms[..]); + let chain_info = peer_manager::testonly::make_chain_info( + &chain, + &pms.iter().map(|pm| &pm.cfg).collect::>()[..], + ); for pm in &pms { pm.set_chain_info(chain_info.clone()).await; } @@ -227,7 +230,10 @@ async fn proxy_connections() { all.extend(proxies.clone()); all.push(&hub); - let chain_info = peer_manager::testonly::make_chain_info(&chain, &validators[..]); + let chain_info = peer_manager::testonly::make_chain_info( + &chain, + &validators.iter().map(|pm| &pm.cfg).collect::>()[..], + ); for pm in &all { pm.set_chain_info(chain_info.clone()).await; } @@ -252,7 +258,7 @@ async fn account_keys_change() { hub.connect_to(&v2.peer_info(), tcp::Tier::T2).await; // TIER1 nodes in 1st epoch are {v0,v1}. - let chain_info = peer_manager::testonly::make_chain_info(&chain, &[&v0, &v1]); + let chain_info = peer_manager::testonly::make_chain_info(&chain, &[&v0.cfg, &v1.cfg]); for pm in [&v0, &v1, &v2, &hub] { pm.set_chain_info(chain_info.clone()).await; } @@ -260,7 +266,7 @@ async fn account_keys_change() { test_clique(rng, &[&v0, &v1]).await; // TIER1 nodes in 2nd epoch are {v0,v2}. - let chain_info = peer_manager::testonly::make_chain_info(&chain, &[&v0, &v2]); + let chain_info = peer_manager::testonly::make_chain_info(&chain, &[&v0.cfg, &v2.cfg]); for pm in [&v0, &v1, &v2, &hub] { pm.set_chain_info(chain_info.clone()).await; } @@ -313,7 +319,7 @@ async fn proxy_change() { tracing::info!(target:"test", "p0 goes down"); drop(p0); tracing::info!(target:"test", "remaining nodes learn that [v0,v1] are TIER1 nodes"); - let chain_info = peer_manager::testonly::make_chain_info(&chain, &[&v0, &v1]); + let chain_info = peer_manager::testonly::make_chain_info(&chain, &[&v0.cfg, &v1.cfg]); for pm in [&v0, &v1, &p1, &hub] { pm.set_chain_info(chain_info.clone()).await; } From 50a1437bfdbbd38d1b1dc77b1af7b012cf83c9f6 Mon Sep 17 00:00:00 2001 From: Ekleog-NEAR <96595974+Ekleog-NEAR@users.noreply.github.com> Date: Thu, 8 Dec 2022 13:13:38 +0100 Subject: [PATCH 079/188] reject imported memories early (#8146) They were already rejected by wasmer2 before this change, just after preparation. So this should not be a protocol change. Second attempt at #8029, without a protocol change this time --- runtime/near-vm-runner/src/prepare.rs | 28 ++++--------------- .../src/tests/runtime_errors.rs | 24 +++++++++++++++- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/runtime/near-vm-runner/src/prepare.rs b/runtime/near-vm-runner/src/prepare.rs index 11152bb43d0..f42a02bd516 100644 --- a/runtime/near-vm-runner/src/prepare.rs +++ b/runtime/near-vm-runner/src/prepare.rs @@ -117,11 +117,11 @@ pub fn prepare_contract(original_code: &[u8], config: &VMConfig) -> Result pwasm_12::prepare_contract(original_code, config), near_vm_logic::StackLimiterVersion::V1 => ContractModule::init(original_code, config)? + .scan_imports()? .standardize_mem() .ensure_no_internal_memory()? .inject_gas_metering()? .inject_stack_height_metering()? - .scan_imports()? .into_wasm_code(), } } @@ -212,8 +212,6 @@ impl<'a> ContractModule<'a> { let import_entries = module.import_section().map(elements::ImportSection::entries).unwrap_or(&[]); - let mut imported_mem_type = None; - for import in import_entries { if import.module() != "env" { // This import tries to import something from non-"env" module, @@ -223,10 +221,7 @@ impl<'a> ContractModule<'a> { let type_idx = match *import.external() { External::Function(ref type_idx) => type_idx, - External::Memory(ref memory_type) => { - imported_mem_type = Some(memory_type); - continue; - } + External::Memory(_) => return Err(PrepareError::Memory), _ => continue, }; @@ -245,17 +240,6 @@ impl<'a> ContractModule<'a> { } */ } - if let Some(memory_type) = imported_mem_type { - // Inspect the module to extract the initial and maximum page count. - let limits = memory_type.limits(); - if limits.initial() != config.limit_config.initial_memory_pages - || limits.maximum() != Some(config.limit_config.max_memory_pages) - { - return Err(PrepareError::Memory); - } - } else { - return Err(PrepareError::Memory); - }; Ok(Self { module, config }) } @@ -443,12 +427,12 @@ mod tests { } #[test] - fn memory() { + fn memory_imports() { // This test assumes that maximum page number is configured to a certain number. assert_eq!(VMConfig::test().limit_config.max_memory_pages, 2048); let r = parse_and_prepare_wat(r#"(module (import "env" "memory" (memory 1 1)))"#); - assert_matches!(r, Ok(_)); + assert_matches!(r, Err(PrepareError::Memory)); // No memory import let r = parse_and_prepare_wat(r#"(module)"#); @@ -460,11 +444,11 @@ mod tests { // no maximum let r = parse_and_prepare_wat(r#"(module (import "env" "memory" (memory 1)))"#); - assert_matches!(r, Ok(_)); + assert_matches!(r, Err(PrepareError::Memory)); // requested maximum exceed configured maximum let r = parse_and_prepare_wat(r#"(module (import "env" "memory" (memory 1 33)))"#); - assert_matches!(r, Ok(_)); + assert_matches!(r, Err(PrepareError::Memory)); } #[test] diff --git a/runtime/near-vm-runner/src/tests/runtime_errors.rs b/runtime/near-vm-runner/src/tests/runtime_errors.rs index d0f84dd5f36..11a1157265e 100644 --- a/runtime/near-vm-runner/src/tests/runtime_errors.rs +++ b/runtime/near-vm-runner/src/tests/runtime_errors.rs @@ -52,7 +52,7 @@ fn test_simple_contract() { } #[test] -fn test_multiple_memories() { +fn test_imported_memory() { test_builder() .wasm(&[ 0, 97, 115, 109, 1, 0, 0, 0, 2, 12, 1, 3, 101, 110, 118, 0, 2, 1, 239, 1, 248, 1, 4, 6, @@ -77,6 +77,28 @@ fn test_multiple_memories() { ]); } +#[test] +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, + ]) + .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: ... + "#]], + ]); +} + #[test] fn test_export_not_found() { test_builder().wat(SIMPLE_CONTRACT) From 2736d282fa750f9a77e2a6966be7bc35863dd66a Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Thu, 8 Dec 2022 14:32:50 +0100 Subject: [PATCH 080/188] runtime: simplify MemoryLike trait (#8155) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit read_memory and write_memory have to perform the same checks as fits_memory already does. So rather than panicking, change the methods to return an error. This makes the interface panic-free and in some situations allows fits_memory call to be skipped. However, separate fits_memory check may still be necessary so keep the method but document in detail why it’s needed and, to keep interfaces consistent, change it to return a Result. Finally, read_memory_u8 is never used so get rid of it. While at it, add tests for WasmerMemory. Essentially copies of tests from Wasmer2Memory. --- runtime/near-vm-logic/src/dependencies.rs | 58 +++++++--- runtime/near-vm-logic/src/logic.rs | 31 ++---- .../near-vm-logic/src/mocks/mock_memory.rs | 20 ++-- runtime/near-vm-runner/src/memory.rs | 102 ++++++++++++++---- runtime/near-vm-runner/src/wasmer2_runner.rs | 89 ++++++++------- runtime/near-vm-runner/src/wasmtime_runner.rs | 54 +++++----- 6 files changed, 217 insertions(+), 137 deletions(-) diff --git a/runtime/near-vm-logic/src/dependencies.rs b/runtime/near-vm-logic/src/dependencies.rs index 688aa0607d2..f0dfd04d35a 100644 --- a/runtime/near-vm-logic/src/dependencies.rs +++ b/runtime/near-vm-logic/src/dependencies.rs @@ -7,29 +7,57 @@ use near_vm_errors::VMLogicError; /// An abstraction over the memory of the smart contract. pub trait MemoryLike { - /// Returns whether the memory interval is completely inside the smart contract memory. - fn fits_memory(&self, offset: u64, len: u64) -> bool; + /// Returns success if the memory interval is completely inside smart + /// contract’s memory. + /// + /// You often don’t need to use this method since [`Self::read_memory`] and + /// [`Self::write_memory`] will perform the check, however it may be + /// necessary to prevent potential denial of service attacks. See + /// [`Self::read_memory`] for description. + fn fits_memory(&self, offset: u64, len: u64) -> Result<(), ()>; /// Reads the content of the given memory interval. /// - /// # Panics + /// Returns error if the memory interval isn’t completely inside the smart + /// contract memory. /// - /// If memory interval is outside the smart contract memory. - fn read_memory(&self, offset: u64, buffer: &mut [u8]); - - /// Reads a single byte from the memory. + /// # Potential denial of service + /// + /// Note that improper use of this function may lead to denial of service + /// attacks. For example, consider the following function: + /// + /// ``` + /// # use near_vm_logic::MemoryLike; /// - /// # Panics + /// fn read_vec(mem: &dyn MemoryLike, ptr: u64, len: u64) -> Result, ()> { + /// let mut vec = vec![0; usize::try_from(len).map_err(|_| ())?]; + /// mem.read_memory(ptr, &mut vec[..])?; + /// Ok(vec) + /// } + /// ``` + /// + /// If attacker controls `len` argument, it may cause attempt at allocation + /// of arbitrarily-large buffer and crash the program. In situations like + /// this, it’s necessary to use [`Self::fits_memory`] method to verify that + /// the length is valid. For example: /// - /// If pointer is outside the smart contract memory. - fn read_memory_u8(&self, offset: u64) -> u8; + /// ``` + /// # use near_vm_logic::MemoryLike; + /// + /// fn read_vec(mem: &dyn MemoryLike, ptr: u64, len: u64) -> Result, ()> { + /// mem.fits_memory(ptr, len)?; + /// let mut vec = vec![0; len as usize]; + /// mem.read_memory(ptr, &mut vec[..])?; + /// Ok(vec) + /// } + /// ``` + fn read_memory(&self, offset: u64, buffer: &mut [u8]) -> Result<(), ()>; /// Writes the buffer into the smart contract memory. /// - /// # Panics - /// - /// If `offset + buffer.len()` is outside the smart contract memory. - fn write_memory(&mut self, offset: u64, buffer: &[u8]); + /// Returns error if the memory interval isn’t completely inside the smart + /// contract memory. + fn write_memory(&mut self, offset: u64, buffer: &[u8]) -> Result<(), ()>; } /// This enum represents if a storage_get call will be performed through flat storage or trie @@ -38,7 +66,7 @@ pub enum StorageGetMode { Trie, } -pub type Result = ::std::result::Result; +pub type Result = ::std::result::Result; /// Logical pointer to a value in storage. /// Allows getting value length before getting the value itself. This is needed so that runtime diff --git a/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs index 77032f6045e..f3f5e7813d3 100644 --- a/runtime/near-vm-logic/src/logic.rs +++ b/runtime/near-vm-logic/src/logic.rs @@ -174,28 +174,21 @@ impl<'a> VMLogic<'a> { // # Memory helper functions # // ########################### - fn try_fit_mem(&mut self, offset: u64, len: u64) -> Result<()> { - if self.memory.fits_memory(offset, len) { - Ok(()) - } else { - Err(HostError::MemoryAccessViolation.into()) - } - } - fn memory_get_into(&mut self, offset: u64, buf: &mut [u8]) -> Result<()> { self.gas_counter.pay_base(read_memory_base)?; self.gas_counter.pay_per(read_memory_byte, buf.len() as _)?; - self.try_fit_mem(offset, buf.len() as _)?; - self.memory.read_memory(offset, buf); - Ok(()) + self.memory.read_memory(offset, buf).map_err(|_| HostError::MemoryAccessViolation.into()) } fn memory_get_vec(&mut self, offset: u64, len: u64) -> Result> { self.gas_counter.pay_base(read_memory_base)?; self.gas_counter.pay_per(read_memory_byte, len)?; - self.try_fit_mem(offset, len)?; + // This check is redundant in the sense that read_memory will perform it + // as well however it’s here to validate that `len` is a valid value. + // See documentation of MemoryLike::read_memory for more information. + self.memory.fits_memory(offset, len).map_err(|_| HostError::MemoryAccessViolation)?; let mut buf = vec![0; len as usize]; - self.memory.read_memory(offset, &mut buf); + self.memory.read_memory(offset, &mut buf).map_err(|_| HostError::MemoryAccessViolation)?; Ok(buf) } @@ -226,9 +219,7 @@ impl<'a> VMLogic<'a> { fn memory_set_slice(&mut self, offset: u64, buf: &[u8]) -> Result<()> { self.gas_counter.pay_base(write_memory_base)?; self.gas_counter.pay_per(write_memory_byte, buf.len() as _)?; - self.try_fit_mem(offset, buf.len() as _)?; - self.memory.write_memory(offset, buf); - Ok(()) + self.memory.write_memory(offset, buf).map_err(|_| HostError::MemoryAccessViolation.into()) } memory_set!(u128, memory_set_u128); @@ -371,7 +362,7 @@ impl<'a> VMLogic<'a> { } else { buf = vec![]; for i in 0..=max_len { - // self.try_fit_mem will check for u64 overflow on the first iteration (i == 0) + // self.memory_get_u8 will check for u64 overflow on the first iteration (i == 0) let el = self.memory_get_u8(ptr + i)?; if el == 0 { break; @@ -396,9 +387,9 @@ impl<'a> VMLogic<'a> { /// * It's up to the caller to set correct len #[cfg(feature = "sandbox")] fn sandbox_get_utf8_string(&mut self, len: u64, ptr: u64) -> Result { - self.try_fit_mem(ptr, len)?; + self.memory.fits_memory(ptr, len).map_err(|_| HostError::MemoryAccessViolation)?; let mut buf = vec![0; len as usize]; - self.memory.read_memory(ptr, &mut buf); + self.memory.read_memory(ptr, &mut buf).map_err(|_| HostError::MemoryAccessViolation)?; String::from_utf8(buf).map_err(|_| HostError::BadUTF8.into()) } @@ -441,7 +432,7 @@ impl<'a> VMLogic<'a> { let limit = max_len / size_of::() as u64; // Takes 2 bytes each iter for i in 0..=limit { - // self.try_fit_mem will check for u64 overflow on the first iteration (i == 0) + // self.memory_get_u16 will check for u64 overflow on the first iteration (i == 0) let start = ptr + i * size_of::() as u64; let el = self.memory_get_u16(start)?; if el == 0 { diff --git a/runtime/near-vm-logic/src/mocks/mock_memory.rs b/runtime/near-vm-logic/src/mocks/mock_memory.rs index 640323f140f..f75be26c372 100644 --- a/runtime/near-vm-logic/src/mocks/mock_memory.rs +++ b/runtime/near-vm-logic/src/mocks/mock_memory.rs @@ -4,23 +4,19 @@ use crate::MemoryLike; pub struct MockedMemory {} impl MemoryLike for MockedMemory { - fn fits_memory(&self, _offset: u64, _len: u64) -> bool { - true + fn fits_memory(&self, _offset: u64, _len: u64) -> Result<(), ()> { + Ok(()) } - fn read_memory(&self, offset: u64, buffer: &mut [u8]) { - let src = unsafe { std::slice::from_raw_parts(offset as *const u8, buffer.len() as usize) }; + fn read_memory(&self, offset: u64, buffer: &mut [u8]) -> Result<(), ()> { + let src = unsafe { std::slice::from_raw_parts(offset as *const u8, buffer.len()) }; buffer.copy_from_slice(src); + Ok(()) } - fn read_memory_u8(&self, offset: u64) -> u8 { - let offset = offset as *const u8; - unsafe { *offset } - } - - fn write_memory(&mut self, offset: u64, buffer: &[u8]) { - let dest = - unsafe { std::slice::from_raw_parts_mut(offset as *mut u8, buffer.len() as usize) }; + fn write_memory(&mut self, offset: u64, buffer: &[u8]) -> Result<(), ()> { + let dest = unsafe { std::slice::from_raw_parts_mut(offset as *mut u8, buffer.len()) }; dest.copy_from_slice(buffer); + Ok(()) } } diff --git a/runtime/near-vm-runner/src/memory.rs b/runtime/near-vm-runner/src/memory.rs index a84e230131d..8d5661ccd3f 100644 --- a/runtime/near-vm-runner/src/memory.rs +++ b/runtime/near-vm-runner/src/memory.rs @@ -1,5 +1,5 @@ use near_vm_logic::MemoryLike; -use wasmer_runtime::units::{Bytes, Pages}; +use wasmer_runtime::units::Pages; use wasmer_runtime::wasm::MemoryDescriptor; use wasmer_runtime::Memory; @@ -25,30 +25,94 @@ impl WasmerMemory { } } +impl WasmerMemory { + fn with_memory(&self, offset: u64, len: usize, func: F) -> Result<(), ()> + where + F: FnOnce(core::slice::Iter<'_, std::cell::Cell>), + { + let start = usize::try_from(offset).map_err(|_| ())?; + let end = start.checked_add(len).ok_or(())?; + self.0.view().get(start..end).map(|mem| func(mem.iter())).ok_or(()) + } +} + impl MemoryLike for WasmerMemory { - fn fits_memory(&self, offset: u64, len: u64) -> bool { - match offset.checked_add(len) { - None => false, - Some(end) => self.0.size().bytes() >= Bytes(end as usize), - } + fn fits_memory(&self, offset: u64, len: u64) -> Result<(), ()> { + self.with_memory(offset, usize::try_from(len).map_err(|_| ())?, |_| ()) + } + + fn read_memory(&self, offset: u64, buffer: &mut [u8]) -> Result<(), ()> { + self.with_memory(offset, buffer.len(), |mem| { + buffer.iter_mut().zip(mem).for_each(|(dst, src)| *dst = src.get()); + }) } - fn read_memory(&self, offset: u64, buffer: &mut [u8]) { - let offset = offset as usize; - for (i, cell) in self.0.view()[offset..(offset + buffer.len())].iter().enumerate() { - buffer[i] = cell.get(); - } + fn write_memory(&mut self, offset: u64, buffer: &[u8]) -> Result<(), ()> { + self.with_memory(offset, buffer.len(), |mem| { + mem.zip(buffer.iter()).for_each(|(dst, src)| dst.set(*src)); + }) } +} + +#[cfg(test)] +mod tests { + use near_vm_logic::MemoryLike; + + use wasmer_types::WASM_PAGE_SIZE; + + #[test] + fn memory_read() { + let memory = super::WasmerMemory::new(1, 1); + let mut buffer = vec![42; WASM_PAGE_SIZE]; + memory.read_memory(0, &mut buffer).unwrap(); + // memory should be zeroed at creation. + assert!(buffer.iter().all(|&v| v == 0)); + } + + #[test] + fn fits_memory() { + const PAGE: u64 = WASM_PAGE_SIZE as u64; + + let memory = super::WasmerMemory::new(1, 1); + + memory.fits_memory(0, PAGE).unwrap(); + memory.fits_memory(PAGE / 2, PAGE as u64 / 2).unwrap(); + memory.fits_memory(PAGE - 1, 1).unwrap(); + memory.fits_memory(PAGE, 0).unwrap(); + + memory.fits_memory(0, PAGE + 1).unwrap_err(); + memory.fits_memory(1, PAGE).unwrap_err(); + memory.fits_memory(PAGE - 1, 2).unwrap_err(); + memory.fits_memory(PAGE, 1).unwrap_err(); + } + + #[test] + fn memory_read_oob() { + let memory = super::WasmerMemory::new(1, 1); + let mut buffer = vec![42; WASM_PAGE_SIZE + 1]; + assert!(memory.read_memory(0, &mut buffer).is_err()); + } + + #[test] + fn memory_write() { + let mut memory = super::WasmerMemory::new(1, 1); + let mut buffer = vec![42; WASM_PAGE_SIZE]; + memory.write_memory(WASM_PAGE_SIZE as u64 / 2, &buffer[..WASM_PAGE_SIZE / 2]).unwrap(); + memory.read_memory(0, &mut buffer).unwrap(); + assert!(buffer[..WASM_PAGE_SIZE / 2].iter().all(|&v| v == 0)); + assert!(buffer[WASM_PAGE_SIZE / 2..].iter().all(|&v| v == 42)); + // Now the buffer is half 0s and half 42s - fn read_memory_u8(&self, offset: u64) -> u8 { - self.0.view()[offset as usize].get() + memory.write_memory(0, &buffer[WASM_PAGE_SIZE / 4..3 * (WASM_PAGE_SIZE / 4)]).unwrap(); + memory.read_memory(0, &mut buffer).unwrap(); + assert!(buffer[..WASM_PAGE_SIZE / 4].iter().all(|&v| v == 0)); + assert!(buffer[WASM_PAGE_SIZE / 4..].iter().all(|&v| v == 42)); } - fn write_memory(&mut self, offset: u64, buffer: &[u8]) { - let offset = offset as usize; - self.0.view()[offset..(offset + buffer.len())] - .iter() - .zip(buffer.iter()) - .for_each(|(cell, v)| cell.set(*v)); + #[test] + fn memory_write_oob() { + let mut memory = super::WasmerMemory::new(1, 1); + let mut buffer = vec![42; WASM_PAGE_SIZE + 1]; + assert!(memory.write_memory(0, &mut buffer).is_err()); } } diff --git a/runtime/near-vm-runner/src/wasmer2_runner.rs b/runtime/near-vm-runner/src/wasmer2_runner.rs index df801688c44..96489d9f7ba 100644 --- a/runtime/near-vm-runner/src/wasmer2_runner.rs +++ b/runtime/near-vm-runner/src/wasmer2_runner.rs @@ -74,12 +74,10 @@ impl Wasmer2Memory { }) } - fn get_memory_buffer(&self, offset: u64, len: usize) -> *mut u8 { - let memory = self.data_offset(offset).map(|(data, remaining)| (data, len <= remaining)); - if let Some((ptr, true)) = memory { - ptr - } else { - panic!("memory access out of bounds") + fn get_memory_buffer(&self, offset: u64, len: usize) -> Result<*mut u8, ()> { + match self.data_offset(offset) { + Some((ptr, remaining)) if len <= remaining => Ok(ptr), + _ => Err(()), } } @@ -89,37 +87,31 @@ impl Wasmer2Memory { } impl MemoryLike for Wasmer2Memory { - fn fits_memory(&self, offset: u64, len: u64) -> bool { - self.data_offset(offset) - .and_then(|(_, remaining)| { - let len = usize::try_from(len).ok()?; - Some(len <= remaining) - }) - .unwrap_or(false) + fn fits_memory(&self, offset: u64, len: u64) -> Result<(), ()> { + let len = usize::try_from(len).map_err(|_| ())?; + self.get_memory_buffer(offset, len).map(|_| ()) } - fn read_memory(&self, offset: u64, buffer: &mut [u8]) { + fn read_memory(&self, offset: u64, buffer: &mut [u8]) -> Result<(), ()> { + let memory = self.get_memory_buffer(offset, buffer.len())?; unsafe { - let memory = self.get_memory_buffer(offset, buffer.len()); // SAFETY: we verified indices into are valid and the pointer will always be valid as // well. Our runtime is currently only executing Wasm code on a single thread, so data // races aren't a concern here. std::ptr::copy_nonoverlapping(memory, buffer.as_mut_ptr(), buffer.len()); } + Ok(()) } - fn read_memory_u8(&self, offset: u64) -> u8 { - unsafe { *self.get_memory_buffer(offset, 1) } - } - - fn write_memory(&mut self, offset: u64, buffer: &[u8]) { + fn write_memory(&mut self, offset: u64, buffer: &[u8]) -> Result<(), ()> { + let memory = self.get_memory_buffer(offset, buffer.len())?; unsafe { - let memory = self.get_memory_buffer(offset, buffer.len()); // SAFETY: we verified indices into are valid and the pointer will always be valid as // well. Our runtime is currently only executing Wasm code on a single thread, so data // races aren't a concern here. std::ptr::copy_nonoverlapping(buffer.as_ptr(), memory, buffer.len()); } + Ok(()) } } @@ -663,6 +655,8 @@ impl crate::runner::VM for Wasmer2VM { #[cfg(test)] mod tests { + use near_vm_logic::MemoryLike; + use assert_matches::assert_matches; use wasmer_types::WASM_PAGE_SIZE; @@ -670,23 +664,38 @@ mod tests { fn get_memory_buffer() { let memory = super::Wasmer2Memory::new(1, 1).unwrap(); // these should not panic with memory out of bounds - memory.get_memory_buffer(0, WASM_PAGE_SIZE); - memory.get_memory_buffer(WASM_PAGE_SIZE as u64 - 1, 1); - memory.get_memory_buffer(WASM_PAGE_SIZE as u64, 0); + memory.get_memory_buffer(0, WASM_PAGE_SIZE).unwrap(); + memory.get_memory_buffer(WASM_PAGE_SIZE as u64 - 1, 1).unwrap(); + memory.get_memory_buffer(WASM_PAGE_SIZE as u64, 0).unwrap(); + } + + #[test] + fn fits_memory() { + const PAGE: u64 = WASM_PAGE_SIZE as u64; + + let memory = super::Wasmer2Memory::new(1, 1).unwrap(); + + memory.fits_memory(0, PAGE).unwrap(); + memory.fits_memory(PAGE / 2, PAGE as u64 / 2).unwrap(); + memory.fits_memory(PAGE - 1, 1).unwrap(); + memory.fits_memory(PAGE, 0).unwrap(); + + memory.fits_memory(0, PAGE + 1).unwrap_err(); + memory.fits_memory(1, PAGE).unwrap_err(); + memory.fits_memory(PAGE - 1, 2).unwrap_err(); + memory.fits_memory(PAGE, 1).unwrap_err(); } #[test] - #[should_panic] fn get_memory_buffer_oob1() { let memory = super::Wasmer2Memory::new(1, 1).unwrap(); - memory.get_memory_buffer(1 + WASM_PAGE_SIZE as u64, 0); + assert!(memory.get_memory_buffer(1 + WASM_PAGE_SIZE as u64, 0).is_err()); } #[test] - #[should_panic] fn get_memory_buffer_oob2() { let memory = super::Wasmer2Memory::new(1, 1).unwrap(); - memory.get_memory_buffer(WASM_PAGE_SIZE as u64, 1); + assert!(memory.get_memory_buffer(WASM_PAGE_SIZE as u64, 1).is_err()); } #[test] @@ -704,48 +713,38 @@ mod tests { fn memory_read() { let memory = super::Wasmer2Memory::new(1, 1).unwrap(); let mut buffer = vec![42; WASM_PAGE_SIZE]; - near_vm_logic::MemoryLike::read_memory(&memory, 0, &mut buffer); + memory.read_memory(0, &mut buffer).unwrap(); // memory should be zeroed at creation. assert!(buffer.iter().all(|&v| v == 0)); } #[test] - #[should_panic] fn memory_read_oob() { let memory = super::Wasmer2Memory::new(1, 1).unwrap(); let mut buffer = vec![42; WASM_PAGE_SIZE + 1]; - near_vm_logic::MemoryLike::read_memory(&memory, 0, &mut buffer); + assert!(memory.read_memory(0, &mut buffer).is_err()); } #[test] fn memory_write() { let mut memory = super::Wasmer2Memory::new(1, 1).unwrap(); let mut buffer = vec![42; WASM_PAGE_SIZE]; - near_vm_logic::MemoryLike::write_memory( - &mut memory, - WASM_PAGE_SIZE as u64 / 2, - &buffer[..WASM_PAGE_SIZE / 2], - ); - near_vm_logic::MemoryLike::read_memory(&memory, 0, &mut buffer); + memory.write_memory(WASM_PAGE_SIZE as u64 / 2, &buffer[..WASM_PAGE_SIZE / 2]).unwrap(); + memory.read_memory(0, &mut buffer).unwrap(); assert!(buffer[..WASM_PAGE_SIZE / 2].iter().all(|&v| v == 0)); assert!(buffer[WASM_PAGE_SIZE / 2..].iter().all(|&v| v == 42)); // Now the buffer is half 0s and half 42s - near_vm_logic::MemoryLike::write_memory( - &mut memory, - 0, - &buffer[WASM_PAGE_SIZE / 4..3 * (WASM_PAGE_SIZE / 4)], - ); - near_vm_logic::MemoryLike::read_memory(&memory, 0, &mut buffer); + memory.write_memory(0, &buffer[WASM_PAGE_SIZE / 4..3 * (WASM_PAGE_SIZE / 4)]).unwrap(); + memory.read_memory(0, &mut buffer).unwrap(); assert!(buffer[..WASM_PAGE_SIZE / 4].iter().all(|&v| v == 0)); assert!(buffer[WASM_PAGE_SIZE / 4..].iter().all(|&v| v == 42)); } #[test] - #[should_panic] fn memory_write_oob() { let mut memory = super::Wasmer2Memory::new(1, 1).unwrap(); let mut buffer = vec![42; WASM_PAGE_SIZE + 1]; - near_vm_logic::MemoryLike::write_memory(&mut memory, 0, &mut buffer); + assert!(memory.write_memory(0, &mut buffer).is_err()); } } diff --git a/runtime/near-vm-runner/src/wasmtime_runner.rs b/runtime/near-vm-runner/src/wasmtime_runner.rs index 102e7858aea..ed10438f8a6 100644 --- a/runtime/near-vm-runner/src/wasmtime_runner.rs +++ b/runtime/near-vm-runner/src/wasmtime_runner.rs @@ -18,8 +18,9 @@ use std::str; use wasmtime::ExternType::Func; use wasmtime::{Engine, Linker, Memory, MemoryType, Module, Store, TrapCode}; +type Caller = wasmtime::Caller<'static, ()>; thread_local! { - pub(crate) static CALLER: RefCell>> = RefCell::new(None); + pub(crate) static CALLER: RefCell> = RefCell::new(None); } pub struct WasmtimeMemory(Memory); @@ -36,37 +37,38 @@ impl WasmtimeMemory { } } +fn with_caller(func: impl FnOnce(&mut Caller) -> T) -> T { + CALLER.with(|caller| func(caller.borrow_mut().as_mut().unwrap())) +} + impl MemoryLike for WasmtimeMemory { - fn fits_memory(&self, offset: u64, len: u64) -> bool { - CALLER.with(|caller| match offset.checked_add(len) { - None => false, - Some(end) => self.0.data_size(caller.borrow_mut().as_mut().unwrap()) as u64 >= end, - }) + fn fits_memory(&self, offset: u64, len: u64) -> Result<(), ()> { + let end = offset.checked_add(len).ok_or(())?; + let end = usize::try_from(end).map_err(|_| ())?; + if end <= with_caller(|caller| self.0.data_size(caller)) { + Ok(()) + } else { + Err(()) + } } - fn read_memory(&self, offset: u64, buffer: &mut [u8]) { - CALLER.with(|caller| { - let offset = offset as usize; - let mut caller = caller.borrow_mut(); - let caller = caller.as_mut().unwrap(); - for i in 0..buffer.len() { - buffer[i] = self.0.data(&mut *caller)[i + offset]; - } + fn read_memory(&self, offset: u64, buffer: &mut [u8]) -> Result<(), ()> { + let start = usize::try_from(offset).map_err(|_| ())?; + let end = start.checked_add(buffer.len()).ok_or(())?; + with_caller(|caller| { + let memory = self.0.data(caller).get(start..end).ok_or(())?; + buffer.copy_from_slice(memory); + Ok(()) }) } - fn read_memory_u8(&self, offset: u64) -> u8 { - CALLER.with(|caller| self.0.data(caller.borrow_mut().as_mut().unwrap())[offset as usize]) - } - - fn write_memory(&mut self, offset: u64, buffer: &[u8]) { - CALLER.with(|caller| { - let offset = offset as usize; - let mut caller = caller.borrow_mut(); - let caller = caller.as_mut().unwrap(); - for i in 0..buffer.len() { - self.0.data_mut(&mut *caller)[i + offset] = buffer[i]; - } + fn write_memory(&mut self, offset: u64, buffer: &[u8]) -> Result<(), ()> { + let start = usize::try_from(offset).map_err(|_| ())?; + let end = start.checked_add(buffer.len()).ok_or(())?; + with_caller(|caller| { + let memory = self.0.data_mut(caller).get_mut(start..end).ok_or(())?; + memory.copy_from_slice(buffer); + Ok(()) }) } } From 416c1ab6c06fad81255b73bceb32484b98255f84 Mon Sep 17 00:00:00 2001 From: Marcelo Diop-Gonzalez Date: Thu, 8 Dec 2022 11:10:51 -0500 Subject: [PATCH 081/188] feat(indexer): add a validate_genesis option to the config (#8137) in the mirror code, where we're starting an indexer for the target chain, the genesis records file is usually very big since we're forking mainnet or testnet state to run a mocknet test. So starting the indexer with full genesis validation takes quite a long time and we don't really need it. --- chain/indexer/src/lib.rs | 9 ++++++++- tools/indexer/example/src/main.rs | 1 + tools/mirror/src/lib.rs | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/chain/indexer/src/lib.rs b/chain/indexer/src/lib.rs index d82d9acb1f2..20c46a80826 100644 --- a/chain/indexer/src/lib.rs +++ b/chain/indexer/src/lib.rs @@ -79,6 +79,8 @@ pub struct IndexerConfig { pub sync_mode: SyncModeEnum, /// Whether await for node to be synced or not pub await_for_node_synced: AwaitForNodeSyncedEnum, + /// Tells whether to validate the genesis file before starting + pub validate_genesis: bool, } /// This is the core component, which handles `nearcore` and internal `streamer`. @@ -98,8 +100,13 @@ impl Indexer { indexer_config.home_dir.display() ); + let genesis_validation_mode = if indexer_config.validate_genesis { + GenesisValidationMode::Full + } else { + GenesisValidationMode::UnsafeFast + }; let near_config = - nearcore::config::load_config(&indexer_config.home_dir, GenesisValidationMode::Full) + nearcore::config::load_config(&indexer_config.home_dir, genesis_validation_mode) .unwrap_or_else(|e| panic!("Error loading config: {:#}", e)); assert!( diff --git a/tools/indexer/example/src/main.rs b/tools/indexer/example/src/main.rs index c59443a8c44..aedaa7f511e 100644 --- a/tools/indexer/example/src/main.rs +++ b/tools/indexer/example/src/main.rs @@ -275,6 +275,7 @@ fn main() -> Result<()> { home_dir, sync_mode: near_indexer::SyncModeEnum::FromInterruption, await_for_node_synced: near_indexer::AwaitForNodeSyncedEnum::WaitForFullSync, + validate_genesis: true, }; let system = actix::System::new(); system.block_on(async move { diff --git a/tools/mirror/src/lib.rs b/tools/mirror/src/lib.rs index fdd625cb7bb..db7234be734 100644 --- a/tools/mirror/src/lib.rs +++ b/tools/mirror/src/lib.rs @@ -861,6 +861,7 @@ impl TxMirror { home_dir: target_home.as_ref().to_path_buf(), sync_mode: near_indexer::SyncModeEnum::LatestSynced, await_for_node_synced: near_indexer::AwaitForNodeSyncedEnum::WaitForFullSync, + validate_genesis: false, }) .context("failed to start target chain indexer")?; let (target_view_client, target_client) = target_indexer.client_actors(); From c5610cd196f78956b1147d53a5e18520cad2beb8 Mon Sep 17 00:00:00 2001 From: Simonas Kazlauskas Date: Thu, 8 Dec 2022 16:27:06 +0000 Subject: [PATCH 082/188] gas: fix the computation of `self.promises_gas` (#8179) With the wrapping computation of `new_used_gas` any contract is in full control of this value, including those that are actually less than either operand (due to overflows.) This is not actually a big deal by itself, if not for the fact that it gives an opportunity for the attacker to nuke the entire network out of service. The in-line comments largely explain the reasoning behind the fix and why it preserves the protocol-facing behaviour, thus not necessitating a protocol version change. To reiterate what the comments say, basically the only case that we want to resolve is for when the attacker picks a value of `new_used_gas` such that it is less than `burnt_gas`. Instead of asserting and aborting we simply set `self.promises_gas = 0`. --- The fix in this PR has been included in 1.29.3 (72d4a4d) and 1.30.0-rc.6 (4290758). --- runtime/near-vm-logic/src/gas_counter.rs | 35 ++++++++--- runtime/near-vm-logic/src/logic.rs | 6 ++ .../near-vm-logic/src/tests/gas_counter.rs | 63 ++++++++++++++++++- 3 files changed, 93 insertions(+), 11 deletions(-) diff --git a/runtime/near-vm-logic/src/gas_counter.rs b/runtime/near-vm-logic/src/gas_counter.rs index 1284c01e935..7183a7cfc60 100644 --- a/runtime/near-vm-logic/src/gas_counter.rs +++ b/runtime/near-vm-logic/src/gas_counter.rs @@ -8,7 +8,6 @@ use near_primitives_core::{ types::Gas, }; use std::collections::HashMap; -use std::fmt; #[inline] pub fn with_ext_cost_counter(f: impl FnOnce(&mut HashMap)) { @@ -56,17 +55,12 @@ pub struct GasCounter { prepaid_gas: Gas, /// If this is a view-only call. is_view: bool, + /// FIXME(nagisa): why do we store a copy both here and in the VMLogic??? ext_costs_config: ExtCostsConfig, /// Where to store profile data, if needed. profile: ProfileData, } -impl fmt::Debug for GasCounter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("").finish() - } -} - impl GasCounter { pub fn new( ext_costs_config: ExtCostsConfig, @@ -127,7 +121,15 @@ impl GasCounter { self.fast_counter.burnt_gas = new_burnt_gas; Ok(()) } else { - Err(self.process_gas_limit(new_burnt_gas, new_burnt_gas + self.promises_gas).into()) + // In the past `new_used_gas` would be computed using an implicit wrapping addition, + // which would then give an opportunity for the `assert` (now `debug_assert`) in the + // callee to fail, leading to a DoS of a node. A wrapping_add in this instance is + // actually fine, even if it gives the attacker full control of the value passed in + // here… + // + // [CONTINUATION IN THE NEXT COMMENT] + let new_used_gas = new_burnt_gas.wrapping_add(self.promises_gas); + Err(self.process_gas_limit(new_burnt_gas, new_used_gas).into()) } } @@ -143,8 +145,21 @@ impl GasCounter { // See https://github.com/near/nearcore/issues/5148. // TODO: consider making this change! let used_gas_limit = min(self.prepaid_gas, new_used_gas); - assert!(used_gas_limit >= self.fast_counter.burnt_gas); - self.promises_gas = used_gas_limit - self.fast_counter.burnt_gas; + // [CONTINUATION OF THE PREVIOUS COMMENT] + // + // Now, there are two distinct ways an attacker can attempt to exploit this code given + // their full control of the `new_used_gas` value. + // + // 1. `self.prepaid_gas < new_used_gas` This is perfectly fine and would be the happy path, + // were the computations performed with arbitrary precision integers all the time. + // 2. `new_used_gas < new_burnt_gas` means that the `new_used_gas` computation wrapped + // and `used_gas_limit` is now set to a lower value than it otherwise should be. In the + // past this would have triggered an unconditional assert leading to nodes crashing and + // network getting stuck/going down. We don’t actually need to assert, though. By + // replacing the wrapping subtraction with a saturating one we make sure that the + // resulting value of `self.promises_gas` is well behaved (becomes 0.) All a potential + // attacker has achieved in this case is throwing some of their gas away. + self.promises_gas = used_gas_limit.saturating_sub(self.fast_counter.burnt_gas); // If we crossed both limits prefer reporting GasLimitExceeded. // Alternative would be to prefer reporting limit that is lower (or diff --git a/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs index f3f5e7813d3..4f58dc9ca1e 100644 --- a/runtime/near-vm-logic/src/logic.rs +++ b/runtime/near-vm-logic/src/logic.rs @@ -170,6 +170,12 @@ impl<'a> VMLogic<'a> { &self.gas_counter } + #[allow(dead_code)] + #[cfg(test)] + pub(crate) fn config(&self) -> &VMConfig { + &self.config + } + // ########################### // # Memory helper functions # // ########################### diff --git a/runtime/near-vm-logic/src/tests/gas_counter.rs b/runtime/near-vm-logic/src/tests/gas_counter.rs index cbcdcb2de16..f564e106e67 100644 --- a/runtime/near-vm-logic/src/tests/gas_counter.rs +++ b/runtime/near-vm-logic/src/tests/gas_counter.rs @@ -5,7 +5,7 @@ use crate::tests::vm_logic_builder::VMLogicBuilder; use crate::types::Gas; use crate::{VMConfig, VMLogic}; use expect_test::expect; -use near_primitives::config::ActionCosts; +use near_primitives::config::{ActionCosts, ExtCosts}; use near_primitives::runtime::fees::Fee; use near_primitives::transaction::{Action, FunctionCallAction}; use near_vm_errors::{HostError, VMLogicError}; @@ -223,6 +223,67 @@ fn function_call_no_weight_refund() { assert!(outcome.used_gas < gas_limit); } +#[test] +fn test_overflowing_burn_gas_with_promises_gas() { + let gas_limit = 3 * 10u64.pow(14); + let mut logic_builder = VMLogicBuilder::default().max_gas_burnt(gas_limit); + let mut logic = logic_builder.build_with_prepaid_gas(gas_limit); + + let index = promise_batch_create(&mut logic, "rick.test").expect("should create a promise"); + logic.promise_batch_action_transfer(index, 100u128.to_le_bytes().as_ptr() as _).unwrap(); + let call_id = logic.promise_batch_then(index, 9, "rick.test".as_ptr() as _).unwrap(); + + let needed_gas_charge = u64::max_value() - logic.gas_counter().used_gas() - 1; + let function_name_len = + needed_gas_charge / logic.config().ext_costs.cost(ExtCosts::read_memory_byte); + let result = logic.promise_batch_action_function_call( + call_id, + function_name_len, + "x".as_ptr() as _, + 1, + "x".as_ptr() as _, + 10u128.to_le_bytes().as_ptr() as _, + 10000, + ); + assert!(matches!( + result, + Err(near_vm_errors::VMLogicError::HostError(near_vm_errors::HostError::GasLimitExceeded)) + )); + assert_eq!(logic.gas_counter().used_gas(), gas_limit); +} + +#[test] +fn test_overflowing_burn_gas_with_promises_gas_2() { + let gas_limit = 3 * 10u64.pow(14); + let mut logic_builder = VMLogicBuilder::default().max_gas_burnt(gas_limit); + let mut logic = logic_builder.build_with_prepaid_gas(gas_limit / 2); + let index = promise_batch_create(&mut logic, "rick.test").expect("should create a promise"); + logic.promise_batch_action_transfer(index, 100u128.to_le_bytes().as_ptr() as _).unwrap(); + logic.promise_batch_then(index, 9, "rick.test".as_ptr() as _).unwrap(); + let minimum_prepay = logic.gas_counter().used_gas(); + let mut logic = logic_builder.build_with_prepaid_gas(minimum_prepay); + let index = promise_batch_create(&mut logic, "rick.test").expect("should create a promise"); + logic.promise_batch_action_transfer(index, 100u128.to_le_bytes().as_ptr() as _).unwrap(); + let call_id = logic.promise_batch_then(index, 9, "rick.test".as_ptr() as _).unwrap(); + let needed_gas_charge = u64::max_value() - logic.gas_counter().used_gas() - 1; + let function_name_len = + needed_gas_charge / logic.config().ext_costs.cost(ExtCosts::read_memory_byte); + let result = logic.promise_batch_action_function_call( + call_id, + function_name_len, + "x".as_ptr() as _, + 1, + "x".as_ptr() as _, + 10u128.to_le_bytes().as_ptr() as _, + 10000, + ); + assert!(matches!( + result, + Err(near_vm_errors::VMLogicError::HostError(near_vm_errors::HostError::GasExceeded)) + )); + assert_eq!(logic.gas_counter().used_gas(), minimum_prepay); +} + /// Check consistent result when exceeding gas limit on a specific action gas parameter. /// /// Increases an action cost to a high value and then watch an execution run out From 0bff3cd0782780df273a58906eb2e45fe697a977 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Fri, 9 Dec 2022 03:19:30 +0100 Subject: [PATCH 083/188] =?UTF-8?q?runtime:=20don=E2=80=99t=20unnecessaril?= =?UTF-8?q?y=20copy=20promise=20indices=20(#8175)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The promise_and method processes one promise index at a time. There’s no benefit in having all them in memory. Rather than copying the indices from Vec into a new Vec simply do the little endian conversion while going through the former vector. This saves an allocation. --- runtime/near-vm-logic/src/logic.rs | 34 ++++++++++++------------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs index 4f58dc9ca1e..df44d77aee8 100644 --- a/runtime/near-vm-logic/src/logic.rs +++ b/runtime/near-vm-logic/src/logic.rs @@ -203,17 +203,6 @@ impl<'a> VMLogic<'a> { memory_get!(u16, memory_get_u16); memory_get!(u8, memory_get_u8); - /// Reads an array of `u64` elements. - fn memory_get_vec_u64(&mut self, offset: u64, num_elements: u64) -> Result> { - let memory_len = num_elements - .checked_mul(size_of::() as u64) - .ok_or(HostError::MemoryAccessViolation)?; - let data = self.memory_get_vec(offset, memory_len)?; - let mut res = vec![0u64; num_elements as usize]; - byteorder::LittleEndian::read_u64_into(&data, &mut res); - Ok(res) - } - fn get_vec_from_memory_or_register(&mut self, offset: u64, len: u64) -> Result> { if len != u64::MAX { self.memory_get_vec(offset, len) @@ -1371,21 +1360,24 @@ impl<'a> VMLogic<'a> { ); } self.gas_counter.pay_base(promise_and_base)?; - self.gas_counter.pay_per( - promise_and_per_promise, - promise_idx_count - .checked_mul(size_of::() as u64) - .ok_or(HostError::IntegerOverflow)?, - )?; + let memory_len = promise_idx_count + .checked_mul(size_of::() as u64) + .ok_or(HostError::IntegerOverflow)?; + self.gas_counter.pay_per(promise_and_per_promise, memory_len)?; - let promise_indices = self.memory_get_vec_u64(promise_idx_ptr, promise_idx_count)?; + // Read indices as little endian u64. + let promise_indices = self.memory_get_vec(promise_idx_ptr, memory_len)?; + let promise_indices = stdx::as_chunks_exact::<{ size_of::() }, u8>(&promise_indices) + .unwrap() + .into_iter() + .map(|bytes| u64::from_le_bytes(*bytes)); let mut receipt_dependencies = vec![]; - for promise_idx in &promise_indices { + for promise_idx in promise_indices { let promise = self .promises - .get(*promise_idx as usize) - .ok_or(HostError::InvalidPromiseIndex { promise_idx: *promise_idx })?; + .get(promise_idx as usize) + .ok_or(HostError::InvalidPromiseIndex { promise_idx })?; match &promise { Promise::Receipt(receipt_idx) => { receipt_dependencies.push(*receipt_idx); From a969c589472bf6bd4dde5fb1c4da0f3fdc28ff9f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Dec 2022 16:59:15 +0100 Subject: [PATCH 084/188] build(deps): bump certifi from 2021.10.8 to 2022.12.7 in /debug_scripts (#8195) Bumps [certifi](https://github.com/certifi/python-certifi) from 2021.10.8 to 2022.12.7. - [Release notes](https://github.com/certifi/python-certifi/releases) - [Commits](https://github.com/certifi/python-certifi/compare/2021.10.08...2022.12.07) --- updated-dependencies: - dependency-name: certifi dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- debug_scripts/Pipfile.lock | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/debug_scripts/Pipfile.lock b/debug_scripts/Pipfile.lock index f682fc663ce..d70b69fb396 100644 --- a/debug_scripts/Pipfile.lock +++ b/debug_scripts/Pipfile.lock @@ -26,18 +26,19 @@ }, "botocore": { "hashes": [ - "sha256:21e164a213beca36033c46026bffa62f2ee2cd2600777271f9a551fb34dba006", - "sha256:70c48c4ae3c2b9ec0ca025385979d01f4c7dae4d9a61c82758d4cf7caa7082cd" + "sha256:663d8f02b98641846eb959c54c840cc33264d5f2dee5b8fc09ee8adbef0f8dcf", + "sha256:89a203bba3c8f2299287e48a9e112e2dbe478cf67eaac26716f0e7f176446146" ], "markers": "python_version >= '3.6'", - "version": "==1.24.37" + "version": "==1.24.46" }, "certifi": { "hashes": [ - "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", - "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" + "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", + "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" ], - "version": "==2021.10.8" + "index": "pypi", + "version": "==2022.12.7" }, "charset-normalizer": { "hashes": [ @@ -49,19 +50,19 @@ }, "idna": { "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" ], "markers": "python_version >= '3'", - "version": "==3.3" + "version": "==3.4" }, "jmespath": { "hashes": [ - "sha256:a490e280edd1f57d6de88636992d05b71e97d69a26a19f058ecf7d304474bf5e", - "sha256:e8dcd576ed616f14ec02eed0005c85973b5890083313860136657e24784e4c04" + "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", + "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" ], "markers": "python_version >= '3.7'", - "version": "==1.0.0" + "version": "==1.0.1" }, "python-dateutil": { "hashes": [ @@ -97,11 +98,11 @@ }, "urllib3": { "hashes": [ - "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", - "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" + "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc", + "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.9" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.13" } }, "develop": {} From 0294733becae0ad3ff4c07e087743649fcda7d94 Mon Sep 17 00:00:00 2001 From: Bowen Wang Date: Fri, 9 Dec 2022 08:14:33 -0800 Subject: [PATCH 085/188] Update SECURITY.md (#8183) Update the security policy to include hackenproof for security issue submissions. --- SECURITY.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 9a3f7a0aad2..6fb2092dfb5 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,11 +7,7 @@ If you have any suggestions or comments about the security policy, please email ## Reporting a vulnerability -All security issues and questions should be reported via email to the [NEAR Security Team](mailto:security@near.org) at security@near.org. -This will be acknowledged within 24 hours by the NEAR Security Team and kick off a review process. -You will receive a more detailed response to the email within 72 hours, indicating perceived severity and the next steps in handling your report. - -After initial reply to your report, the security team will keep you informed about the progress toward patching and public disclosure. +All security issues should be submitted on [hackenproof](https://hackenproof.com/near/near-protocol). The team will review the submissions and decide whether they are eligible for bounty payouts. For more details, please check out the program description on the hackenproof website. ## Handling & disclosure process From 63d77d49abf6ad5cda3ae6ad1488c33efda3b342 Mon Sep 17 00:00:00 2001 From: Saketh Are Date: Fri, 9 Dec 2022 12:06:24 -0500 Subject: [PATCH 086/188] Add TIER1 network debug page (#8181) Adds a separate page `/debug/pages/tier1_network_info` displaying information about TIER1 connections. For TIER1 connections we mostly display the same information as TIER2 connections, except that: - TIER1 messages don't include height, last block hash, or nonce info, so these columns are omitted - TIER1 nodes have configured public proxies; a column is added to display their addresses Includes a couple of minor tweaks to the TIER2 `network_info` page (show PeerId of the current node at the top of the page, rename the connection table's `AccountId` column to `PeerId`). --- chain/client/src/client_actor.rs | 1 + chain/client/src/debug.rs | 1 + chain/client/src/info.rs | 1 + chain/client/src/test_utils.rs | 1 + chain/jsonrpc/res/debug.html | 1 + chain/jsonrpc/res/network_info.css | 51 +++++ chain/jsonrpc/res/network_info.html | 175 ++---------------- chain/jsonrpc/res/network_info.js | 109 +++++++++++ chain/jsonrpc/res/tier1_network_info.html | 162 ++++++++++++++++ chain/jsonrpc/src/lib.rs | 32 ++++ .../src/peer_manager/peer_manager_actor.rs | 3 +- chain/network/src/types.rs | 1 + core/primitives/src/views.rs | 1 + .../src/tests/client/process_blocks.rs | 1 + tools/chainsync-loadtest/src/network.rs | 1 + tools/mock-node/src/lib.rs | 1 + 16 files changed, 377 insertions(+), 165 deletions(-) create mode 100644 chain/jsonrpc/res/network_info.css create mode 100644 chain/jsonrpc/res/network_info.js create mode 100644 chain/jsonrpc/res/tier1_network_info.html diff --git a/chain/client/src/client_actor.rs b/chain/client/src/client_actor.rs index 06c20358f31..e33ed0de315 100644 --- a/chain/client/src/client_actor.rs +++ b/chain/client/src/client_actor.rs @@ -199,6 +199,7 @@ impl ClientActor { received_bytes_per_sec: 0, sent_bytes_per_sec: 0, known_producers: vec![], + tier1_accounts_keys: vec![], tier1_accounts_data: vec![], }, last_validator_announce_time: None, diff --git a/chain/client/src/debug.rs b/chain/client/src/debug.rs index b98b4686d63..3e47949d578 100644 --- a/chain/client/src/debug.rs +++ b/chain/client/src/debug.rs @@ -689,6 +689,7 @@ pub(crate) fn new_network_info_view(chain: &Chain, network_info: &NetworkInfo) - .map(|it| it.iter().map(|peer_id| peer_id.public_key().clone()).collect()), }) .collect(), + tier1_accounts_keys: network_info.tier1_accounts_keys.clone(), tier1_accounts_data: network_info .tier1_accounts_data .iter() diff --git a/chain/client/src/info.rs b/chain/client/src/info.rs index 48688379284..64e822b539b 100644 --- a/chain/client/src/info.rs +++ b/chain/client/src/info.rs @@ -561,6 +561,7 @@ mod tests { received_bytes_per_sec: 0, known_producers: vec![], tier1_connections: vec![], + tier1_accounts_keys: vec![], tier1_accounts_data: vec![], }, &config, diff --git a/chain/client/src/test_utils.rs b/chain/client/src/test_utils.rs index 2655203b8b6..957e9c354e7 100644 --- a/chain/client/src/test_utils.rs +++ b/chain/client/src/test_utils.rs @@ -690,6 +690,7 @@ pub fn setup_mock_all_validators( sent_bytes_per_sec: 0, received_bytes_per_sec: 0, known_producers: vec![], + tier1_accounts_keys: vec![], tier1_accounts_data: vec![], }; client_addr.do_send(SetNetworkInfo(info).with_span_context()); diff --git a/chain/jsonrpc/res/debug.html b/chain/jsonrpc/res/debug.html index 06cd61ca576..79708b09b56 100644 --- a/chain/jsonrpc/res/debug.html +++ b/chain/jsonrpc/res/debug.html @@ -61,6 +61,7 @@

Last blocks

Network info

+

TIER1 Network info

Epoch info

Chain & Chunk info

Sync info

diff --git a/chain/jsonrpc/res/network_info.css b/chain/jsonrpc/res/network_info.css new file mode 100644 index 00000000000..727b21c28d9 --- /dev/null +++ b/chain/jsonrpc/res/network_info.css @@ -0,0 +1,51 @@ +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; +} + +.peer_in_sync { + background-color: green; +} + +.peer_ahead { + background-color: lightblue; +} + +.peer_ahead_alot { + background-color: blueviolet; +} + +.peer_behind_a_little { + background-color: yellowgreen; +} + +.peer_behind { + background-color: yellow; +} + +.peer_far_behind { + background-color: red; +} diff --git a/chain/jsonrpc/res/network_info.html b/chain/jsonrpc/res/network_info.html index b65c1aa4dde..3b2f4873212 100644 --- a/chain/jsonrpc/res/network_info.html +++ b/chain/jsonrpc/res/network_info.html @@ -1,136 +1,10 @@ - + + + + + + + +

+ Welcome to the TIER1 Network Info page! +

+ +
').append(peer.peer_id.substr(9, 5) + "...")) .append($('').append(convertTime(peer.last_time_received_message_millis)).addClass(last_ping_class)) .append($('').append(JSON.stringify(peer.height)).addClass(peer_class)) - .append($('').append(peer.block_hash)) + .append($('').append(displayHash(peer))) .append($('').append(JSON.stringify(peer.tracked_shards))) .append($('').append(JSON.stringify(peer.archival))) .append($('').append(((peer.is_outbound_peer) ? 'OUT' : 'IN'))) diff --git a/chain/network/src/types.rs b/chain/network/src/types.rs index 6a977b4a9c8..6c91619cabe 100644 --- a/chain/network/src/types.rs +++ b/chain/network/src/types.rs @@ -17,7 +17,6 @@ use near_primitives::sharding::PartialEncodedChunkWithArcReceipts; use near_primitives::transaction::SignedTransaction; use near_primitives::types::BlockHeight; use near_primitives::types::{AccountId, EpochId, ShardId}; -use near_primitives::views::{KnownProducerView, NetworkInfoView, PeerInfoView}; use once_cell::sync::OnceCell; use std::collections::HashMap; use std::fmt::Debug; @@ -348,39 +347,6 @@ impl From<&FullPeerInfo> for ConnectedPeerInfo { } } -impl From<&ConnectedPeerInfo> for PeerInfoView { - fn from(connected_peer_info: &ConnectedPeerInfo) -> Self { - let full_peer_info = &connected_peer_info.full_peer_info; - PeerInfoView { - addr: match full_peer_info.peer_info.addr { - Some(socket_addr) => socket_addr.to_string(), - None => "N/A".to_string(), - }, - account_id: full_peer_info.peer_info.account_id.clone(), - height: full_peer_info.chain_info.last_block.map(|x| x.height), - block_hash: full_peer_info.chain_info.last_block.map(|x| x.hash), - tracked_shards: full_peer_info.chain_info.tracked_shards.clone(), - archival: full_peer_info.chain_info.archival, - peer_id: full_peer_info.peer_info.id.public_key().clone(), - received_bytes_per_sec: connected_peer_info.received_bytes_per_sec, - sent_bytes_per_sec: connected_peer_info.sent_bytes_per_sec, - last_time_peer_requested_millis: connected_peer_info - .last_time_peer_requested - .elapsed() - .whole_milliseconds() as u64, - last_time_received_message_millis: connected_peer_info - .last_time_received_message - .elapsed() - .whole_milliseconds() as u64, - connection_established_time_millis: connected_peer_info - .connection_established_time - .elapsed() - .whole_milliseconds() as u64, - is_outbound_peer: connected_peer_info.peer_type == PeerType::Outbound, - } - } -} - // Information about the connected peer that is shared with the rest of the system. #[derive(Debug, Clone)] pub struct ConnectedPeerInfo { @@ -412,32 +378,6 @@ pub struct NetworkInfo { pub tier1_accounts: Vec>, } -impl From for NetworkInfoView { - fn from(network_info: NetworkInfo) -> Self { - NetworkInfoView { - peer_max_count: network_info.peer_max_count, - num_connected_peers: network_info.num_connected_peers, - connected_peers: network_info - .connected_peers - .iter() - .map(|full_peer_info| full_peer_info.into()) - .collect::>(), - known_producers: network_info - .known_producers - .iter() - .map(|it| KnownProducerView { - account_id: it.account_id.clone(), - peer_id: it.peer_id.public_key().clone(), - next_hops: it - .next_hops - .as_ref() - .map(|it| it.iter().map(|peer_id| peer_id.public_key().clone()).collect()), - }) - .collect(), - } - } -} - #[derive(Debug, actix::MessageResponse)] pub enum NetworkResponses { NoResponse, diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index 444f2d43029..dc13a627303 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -337,6 +337,7 @@ pub struct PeerInfoView { pub account_id: Option, pub height: Option, pub block_hash: Option, + pub is_highest_block_invalid: bool, pub tracked_shards: Vec, pub archival: bool, pub peer_id: PublicKey, From 0040e92df13a935e5ca7fef7fd743c54a284e2fb Mon Sep 17 00:00:00 2001 From: Aleksandr Logunov Date: Mon, 21 Nov 2022 20:41:40 +0400 Subject: [PATCH 015/188] feat: configure num threads for flat storage creation (#8088) Make number of threads for background flat storage creation configurable. In the node config we set it to 8, but we also allow node owners to modify it if they observe that BP slows down or creation is too slow until FS becomes a requirement. In the tests we limit number of threads to 1. I think introducing some general structure as `ChainConfig` makes sense here, as we already use a configuration option `save_trie_changes`. I'm open to other opinions how to organize this. In general `NearConfig` structure looks a bit suspicious to me, because it includes `config` taken from `config.json` and many other configs which are just derived from `config`. ## Testing Checking that parameter is taken correctly by the node. --- CHANGELOG.md | 4 ++++ chain/chain/src/chain.rs | 13 +++++++++---- chain/chain/src/flat_storage_creator.rs | 13 +++++++------ chain/chain/src/store.rs | 10 ++++++++-- chain/chain/src/store_validator.rs | 3 ++- chain/chain/src/test_utils.rs | 7 ++++--- chain/chain/src/tests/gc.rs | 10 ++++++++-- chain/chain/src/types.rs | 14 ++++++++++++++ chain/client/src/client.rs | 7 +++++-- chain/client/src/info.rs | 10 ++++++++-- chain/client/src/test_utils.rs | 18 +++++++++++++++--- core/chain-configs/src/client_config.rs | 3 +++ core/store/src/config.rs | 9 +++++++++ integration-tests/src/genesis_helpers.rs | 7 +++++-- nearcore/src/config.rs | 1 + tools/speedy_sync/src/main.rs | 7 +++++-- 16 files changed, 107 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7d93403601..8ddbcc2dcf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,10 @@ to set limits on the trie cache. Deprecates the never announced `store.trie_cache_capacities` option which was mentioned in previous change. [#7578](https://github.com/near/nearcore/pull/7578) +* New option `store.background_migration_threads` in `config.json`. Defines + number of threads to execute background migrations of storage. Currently used + for flat storage migration. Set to 8 by default, can be reduced if it slows down + block processing too much or increased if you want to speed up migration. * Tracing of work across actix workers within a process: [#7866](https://github.com/near/nearcore/pull/7866), [#7819](https://github.com/near/nearcore/pull/7819), diff --git a/chain/chain/src/chain.rs b/chain/chain/src/chain.rs index 62f2027d203..0f09e893bec 100644 --- a/chain/chain/src/chain.rs +++ b/chain/chain/src/chain.rs @@ -68,7 +68,7 @@ use crate::store::{ChainStore, ChainStoreAccess, ChainStoreUpdate, GCMode}; use crate::types::{ AcceptedBlock, ApplySplitStateResult, ApplySplitStateResultOrStateChanges, ApplyTransactionResult, Block, BlockEconomicsConfig, BlockHeader, BlockHeaderInfo, BlockStatus, - ChainGenesis, Provenance, RuntimeAdapter, + ChainConfig, ChainGenesis, Provenance, RuntimeAdapter, }; use crate::validate::{ validate_challenge, validate_chunk_proofs, validate_chunk_with_chunk_extra, @@ -545,11 +545,12 @@ impl Chain { runtime_adapter: Arc, chain_genesis: &ChainGenesis, doomslug_threshold_mode: DoomslugThresholdMode, - save_trie_changes: bool, + chain_config: ChainConfig, ) -> Result { // Get runtime initial state and create genesis block out of it. let (store, state_roots) = runtime_adapter.genesis_state(); - let mut store = ChainStore::new(store, chain_genesis.height, save_trie_changes); + let mut store = + ChainStore::new(store, chain_genesis.height, chain_config.save_trie_changes); let genesis_chunks = genesis_chunks( state_roots.clone(), runtime_adapter.num_shards(&EpochId::default())?, @@ -653,7 +654,11 @@ impl Chain { store_update.commit()?; // Create flat storage or initiate migration to flat storage. - let flat_storage_creator = FlatStorageCreator::new(runtime_adapter.clone(), &store); + let flat_storage_creator = FlatStorageCreator::new( + runtime_adapter.clone(), + &store, + chain_config.background_migration_threads, + ); info!(target: "chain", "Init: header head @ #{} {}; block head @ #{} {}", header_head.height, header_head.last_block_hash, diff --git a/chain/chain/src/flat_storage_creator.rs b/chain/chain/src/flat_storage_creator.rs index b51764f3cd2..518a3bd295c 100644 --- a/chain/chain/src/flat_storage_creator.rs +++ b/chain/chain/src/flat_storage_creator.rs @@ -6,7 +6,7 @@ use near_chain_primitives::Error; use near_primitives::types::{BlockHeight, ShardId}; #[cfg(feature = "protocol_feature_flat_state")] use near_store::flat_state::store_helper; -use near_store::flat_state::{FlatStorageStateStatus, NUM_PARTS_IN_ONE_STEP}; +use near_store::flat_state::FlatStorageStateStatus; use std::sync::Arc; #[cfg(feature = "protocol_feature_flat_state")] use tracing::debug; @@ -109,7 +109,11 @@ pub struct FlatStorageCreator { } impl FlatStorageCreator { - pub fn new(runtime_adapter: Arc, chain_store: &ChainStore) -> Option { + pub fn new( + runtime_adapter: Arc, + chain_store: &ChainStore, + num_threads: usize, + ) -> Option { let chain_head = chain_store.head().unwrap(); let num_shards = runtime_adapter.num_shards(&chain_head.epoch_id).unwrap(); let start_height = chain_head.height; @@ -138,10 +142,7 @@ impl FlatStorageCreator { if creation_needed { Some(Self { shard_creators, - pool: rayon::ThreadPoolBuilder::new() - .num_threads(NUM_PARTS_IN_ONE_STEP as usize) - .build() - .unwrap(), + pool: rayon::ThreadPoolBuilder::new().num_threads(num_threads).build().unwrap(), }) } else { None diff --git a/chain/chain/src/store.rs b/chain/chain/src/store.rs index d218f92a616..87c5a05edbd 100644 --- a/chain/chain/src/store.rs +++ b/chain/chain/src/store.rs @@ -2990,6 +2990,7 @@ mod tests { use crate::store::{ChainStoreAccess, GCMode}; use crate::store_validator::StoreValidator; use crate::test_utils::{KeyValueRuntime, ValidatorSchedule}; + use crate::types::ChainConfig; use crate::{Chain, ChainGenesis, DoomslugThresholdMode, RuntimeAdapter}; fn get_chain() -> Chain { @@ -3003,8 +3004,13 @@ mod tests { .block_producers_per_epoch(vec![vec!["test1".parse().unwrap()]]); let runtime_adapter = Arc::new(KeyValueRuntime::new_with_validators(store, vs, epoch_length)); - Chain::new(runtime_adapter, &chain_genesis, DoomslugThresholdMode::NoApprovals, true) - .unwrap() + Chain::new( + runtime_adapter, + &chain_genesis, + DoomslugThresholdMode::NoApprovals, + ChainConfig::test(), + ) + .unwrap() } #[test] diff --git a/chain/chain/src/store_validator.rs b/chain/chain/src/store_validator.rs index fb530662faf..80c650b16b0 100644 --- a/chain/chain/src/store_validator.rs +++ b/chain/chain/src/store_validator.rs @@ -380,6 +380,7 @@ mod tests { use near_store::test_utils::create_test_store; use crate::test_utils::KeyValueRuntime; + use crate::types::ChainConfig; use crate::{Chain, ChainGenesis, ChainStoreAccess, DoomslugThresholdMode}; use super::*; @@ -395,7 +396,7 @@ mod tests { runtime_adapter.clone(), &chain_genesis, DoomslugThresholdMode::NoApprovals, - true, + ChainConfig::test(), ) .unwrap(); (chain, StoreValidator::new(None, genesis, runtime_adapter, store, false)) diff --git a/chain/chain/src/test_utils.rs b/chain/chain/src/test_utils.rs index 58969e0d645..f959d2c2465 100644 --- a/chain/chain/src/test_utils.rs +++ b/chain/chain/src/test_utils.rs @@ -56,7 +56,8 @@ use crate::block_processing_utils::BlockNotInPoolError; use crate::chain::Chain; use crate::store::ChainStoreAccess; use crate::types::{ - AcceptedBlock, ApplySplitStateResult, ApplyTransactionResult, BlockHeaderInfo, ChainGenesis, + AcceptedBlock, ApplySplitStateResult, ApplyTransactionResult, BlockHeaderInfo, ChainConfig, + ChainGenesis, }; use crate::{BlockHeader, DoomslugThresholdMode, RuntimeAdapter}; use crate::{BlockProcessingArtifact, Provenance}; @@ -1425,7 +1426,7 @@ pub fn setup_with_tx_validity_period( protocol_version: PROTOCOL_VERSION, }, DoomslugThresholdMode::NoApprovals, - true, + ChainConfig::test(), ) .unwrap(); let test_account = "test".parse::().unwrap(); @@ -1465,7 +1466,7 @@ pub fn setup_with_validators( protocol_version: PROTOCOL_VERSION, }, DoomslugThresholdMode::NoApprovals, - true, + ChainConfig::test(), ) .unwrap(); (chain, runtime, signers) diff --git a/chain/chain/src/tests/gc.rs b/chain/chain/src/tests/gc.rs index 2aac08210e1..cdddacec50c 100644 --- a/chain/chain/src/tests/gc.rs +++ b/chain/chain/src/tests/gc.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use crate::chain::Chain; use crate::test_utils::{KeyValueRuntime, ValidatorSchedule}; -use crate::types::{ChainGenesis, Tip}; +use crate::types::{ChainConfig, ChainGenesis, Tip}; use crate::DoomslugThresholdMode; use near_chain_configs::GCConfig; @@ -30,7 +30,13 @@ fn get_chain_with_epoch_length_and_num_shards( .block_producers_per_epoch(vec![vec!["test1".parse().unwrap()]]) .num_shards(num_shards); let runtime_adapter = Arc::new(KeyValueRuntime::new_with_validators(store, vs, epoch_length)); - Chain::new(runtime_adapter, &chain_genesis, DoomslugThresholdMode::NoApprovals, true).unwrap() + Chain::new( + runtime_adapter, + &chain_genesis, + DoomslugThresholdMode::NoApprovals, + ChainConfig::test(), + ) + .unwrap() } // Build a chain of num_blocks on top of prev_block diff --git a/chain/chain/src/types.rs b/chain/chain/src/types.rs index 1c42e79744a..df929e38a49 100644 --- a/chain/chain/src/types.rs +++ b/chain/chain/src/types.rs @@ -236,6 +236,20 @@ pub struct ChainGenesis { pub protocol_version: ProtocolVersion, } +pub struct ChainConfig { + /// Whether to save `TrieChanges` on disk or not. + pub save_trie_changes: bool, + /// Number of threads to execute background migration work. + /// Currently used for flat storage background creation. + pub background_migration_threads: usize, +} + +impl ChainConfig { + pub fn test() -> Self { + Self { save_trie_changes: true, background_migration_threads: 1 } + } +} + impl ChainGenesis { pub fn new(genesis: &Genesis) -> Self { Self { diff --git a/chain/client/src/client.rs b/chain/client/src/client.rs index e929513e89a..80f61baeb37 100644 --- a/chain/client/src/client.rs +++ b/chain/client/src/client.rs @@ -20,7 +20,7 @@ use near_chain::chain::{ OrphanMissingChunks, StateSplitRequest, TX_ROUTING_HEIGHT_HORIZON, }; use near_chain::test_utils::format_hash; -use near_chain::types::LatestKnown; +use near_chain::types::{ChainConfig, LatestKnown}; use near_chain::{ BlockProcessingArtifact, BlockStatus, Chain, ChainGenesis, ChainStoreAccess, DoneApplyChunkCallback, Doomslug, DoomslugThresholdMode, Provenance, RuntimeAdapter, @@ -188,7 +188,10 @@ impl Client { runtime_adapter.clone(), &chain_genesis, doomslug_threshold_mode, - !config.archive, + ChainConfig { + save_trie_changes: !config.archive, + background_migration_threads: config.client_background_migration_threads, + }, )?; let me = validator_signer.as_ref().map(|x| x.validator_id().clone()); let shards_mgr = ShardsManager::new( diff --git a/chain/client/src/info.rs b/chain/client/src/info.rs index 8e02dbaa179..69bb8384e6c 100644 --- a/chain/client/src/info.rs +++ b/chain/client/src/info.rs @@ -491,6 +491,7 @@ mod tests { use super::*; use assert_matches::assert_matches; use near_chain::test_utils::{KeyValueRuntime, ValidatorSchedule}; + use near_chain::types::ChainConfig; use near_chain::{Chain, ChainGenesis, DoomslugThresholdMode}; use near_network::test_utils::peer_id_from_seed; use near_primitives::version::PROTOCOL_VERSION; @@ -539,8 +540,13 @@ mod tests { protocol_version: PROTOCOL_VERSION, }; let doomslug_threshold_mode = DoomslugThresholdMode::TwoThirds; - let chain = - Chain::new(runtime.clone(), &chain_genesis, doomslug_threshold_mode, true).unwrap(); + let chain = Chain::new( + runtime.clone(), + &chain_genesis, + doomslug_threshold_mode, + ChainConfig::test(), + ) + .unwrap(); let telemetry = info_helper.telemetry_info( &chain.head().unwrap(), diff --git a/chain/client/src/test_utils.rs b/chain/client/src/test_utils.rs index 814e97ef9c0..f4c2a07b319 100644 --- a/chain/client/src/test_utils.rs +++ b/chain/client/src/test_utils.rs @@ -19,6 +19,7 @@ use near_chain::test_utils::{ wait_for_all_blocks_in_processing, wait_for_block_in_processing, KeyValueRuntime, ValidatorSchedule, }; +use near_chain::types::ChainConfig; use near_chain::{ Chain, ChainGenesis, ChainStoreAccess, DoomslugThresholdMode, Provenance, RuntimeAdapter, }; @@ -217,8 +218,13 @@ pub fn setup( } else { DoomslugThresholdMode::NoApprovals }; - let chain = - Chain::new(runtime.clone(), &chain_genesis, doomslug_threshold_mode, !archive).unwrap(); + let chain = Chain::new( + runtime.clone(), + &chain_genesis, + doomslug_threshold_mode, + ChainConfig { save_trie_changes: !archive, background_migration_threads: 1 }, + ) + .unwrap(); let genesis_block = chain.get_block(&chain.genesis().hash().clone()).unwrap(); let signer = Arc::new(InMemoryValidatorSigner::from_seed( @@ -302,7 +308,13 @@ pub fn setup_only_view( } else { DoomslugThresholdMode::NoApprovals }; - Chain::new(runtime.clone(), &chain_genesis, doomslug_threshold_mode, !archive).unwrap(); + Chain::new( + runtime.clone(), + &chain_genesis, + doomslug_threshold_mode, + ChainConfig { save_trie_changes: !archive, background_migration_threads: 1 }, + ) + .unwrap(); let signer = Arc::new(InMemoryValidatorSigner::from_seed( account_id.clone(), diff --git a/core/chain-configs/src/client_config.rs b/core/chain-configs/src/client_config.rs index 2c45141b2e2..dee06c4b7d8 100644 --- a/core/chain-configs/src/client_config.rs +++ b/core/chain-configs/src/client_config.rs @@ -156,6 +156,8 @@ pub struct ClientConfig { pub max_gas_burnt_view: Option, /// Re-export storage layer statistics as prometheus metrics. pub enable_statistics_export: bool, + /// Number of threads to execute background migration work in client. + pub client_background_migration_threads: usize, } impl ClientConfig { @@ -215,6 +217,7 @@ impl ClientConfig { trie_viewer_state_size_limit: None, max_gas_burnt_view: None, enable_statistics_export: true, + client_background_migration_threads: 1, } } } diff --git a/core/store/src/config.rs b/core/store/src/config.rs index 3e33206f43c..e2385fc1c20 100644 --- a/core/store/src/config.rs +++ b/core/store/src/config.rs @@ -82,6 +82,11 @@ pub struct StoreConfig { /// be copied between the databases. #[serde(skip_serializing_if = "MigrationSnapshot::is_default")] pub migration_snapshot: MigrationSnapshot, + + /// Number of threads to execute storage background migrations. + /// Needed to create flat storage which need to happen in parallel + /// with block processing. + pub background_migration_threads: usize, } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -210,6 +215,10 @@ impl Default for StoreConfig { ], migration_snapshot: Default::default(), + + // We checked that this number of threads doesn't impact + // regular block processing significantly. + background_migration_threads: 8, } } } diff --git a/integration-tests/src/genesis_helpers.rs b/integration-tests/src/genesis_helpers.rs index fc85dd82834..256f15506ee 100644 --- a/integration-tests/src/genesis_helpers.rs +++ b/integration-tests/src/genesis_helpers.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use tempfile::tempdir; +use near_chain::types::ChainConfig; use near_chain::{Chain, ChainGenesis, DoomslugThresholdMode}; use near_chain_configs::Genesis; use near_primitives::block::{Block, BlockHeader}; @@ -21,7 +22,8 @@ pub fn genesis_header(genesis: &Genesis) -> BlockHeader { let chain_genesis = ChainGenesis::new(genesis); let runtime = Arc::new(NightshadeRuntime::test(dir.path(), store, genesis)); let chain = - Chain::new(runtime, &chain_genesis, DoomslugThresholdMode::TwoThirds, true).unwrap(); + Chain::new(runtime, &chain_genesis, DoomslugThresholdMode::TwoThirds, ChainConfig::test()) + .unwrap(); chain.genesis().clone() } @@ -32,6 +34,7 @@ pub fn genesis_block(genesis: &Genesis) -> Block { let chain_genesis = ChainGenesis::new(genesis); let runtime = Arc::new(NightshadeRuntime::test(dir.path(), store, genesis)); let chain = - Chain::new(runtime, &chain_genesis, DoomslugThresholdMode::TwoThirds, true).unwrap(); + Chain::new(runtime, &chain_genesis, DoomslugThresholdMode::TwoThirds, ChainConfig::test()) + .unwrap(); chain.get_block(&chain.genesis().hash().clone()).unwrap() } diff --git a/nearcore/src/config.rs b/nearcore/src/config.rs index 8b61ca92a9c..b964d2dc744 100644 --- a/nearcore/src/config.rs +++ b/nearcore/src/config.rs @@ -605,6 +605,7 @@ impl NearConfig { trie_viewer_state_size_limit: config.trie_viewer_state_size_limit, max_gas_burnt_view: config.max_gas_burnt_view, enable_statistics_export: config.store.enable_statistics_export, + client_background_migration_threads: config.store.background_migration_threads, }, network_config: NetworkConfig::new( config.network, diff --git a/tools/speedy_sync/src/main.rs b/tools/speedy_sync/src/main.rs index 9e9d256046d..4a16dcc4bc0 100644 --- a/tools/speedy_sync/src/main.rs +++ b/tools/speedy_sync/src/main.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use borsh::{BorshDeserialize, BorshSerialize}; use clap::Parser; -use near_chain::types::Tip; +use near_chain::types::{ChainConfig, Tip}; use near_chain::{Chain, ChainGenesis, DoomslugThresholdMode}; use near_chain_configs::GenesisValidationMode; use near_epoch_manager::types::EpochInfoAggregator; @@ -237,7 +237,10 @@ fn load_snapshot(load_cmd: LoadCmd) { runtime.clone(), &chain_genesis, DoomslugThresholdMode::TwoThirds, - !config.client_config.archive, + ChainConfig { + save_trie_changes: !config.client_config.archive, + background_migration_threads: 1, + }, ) .unwrap(); From f0da0c6be335a51fd182380ad03e9b136ff79cb3 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Mon, 21 Nov 2022 17:57:33 +0100 Subject: [PATCH 016/188] =?UTF-8?q?Introduce=20=E2=80=98stdx=E2=80=99=20cr?= =?UTF-8?q?ate=20with=20split=20array=20polyfills=20(#8087)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This consolidates all the places where array splitting happens into a single module. In the future, once split_array_ref feature gets stabilised, we’ll be able to get rid of those polyfills but for now it’s nice to have them all in one place. The new `stdx` crate also provides a place where other common utility features can be put so that the same function doesn’t need to be implemented in multiple crates or put in a crate it doesn’t fit in and made public just because some other crate depends on it. Crucially, the crate must not depend on any crates from the workspace. --- Cargo.lock | 7 + Cargo.toml | 3 + core/crypto/Cargo.toml | 1 + core/crypto/src/util.rs | 29 ++-- core/store/Cargo.toml | 1 + core/store/src/db/refcount.rs | 7 +- runtime/near-vm-logic/Cargo.toml | 1 + runtime/near-vm-logic/src/alt_bn128.rs | 20 +-- runtime/near-vm-logic/src/array_utils.rs | 30 ---- utils/stdx/Cargo.toml | 16 ++ utils/stdx/LICENSE-APACHE | 201 +++++++++++++++++++++++ utils/stdx/LICENSE-MIT | 19 +++ utils/stdx/src/lib.rs | 91 ++++++++++ 13 files changed, 363 insertions(+), 63 deletions(-) create mode 100644 utils/stdx/Cargo.toml create mode 100644 utils/stdx/LICENSE-APACHE create mode 100644 utils/stdx/LICENSE-MIT create mode 100644 utils/stdx/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index f49d1313d09..ef579558b28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3033,6 +3033,7 @@ dependencies = [ "ed25519-dalek", "hex-literal", "near-account-id", + "near-stdx", "once_cell", "primitive-types", "rand 0.7.3", @@ -3516,6 +3517,10 @@ dependencies = [ name = "near-stable-hasher" version = "0.0.0" +[[package]] +name = "near-stdx" +version = "0.0.0" + [[package]] name = "near-store" version = "0.0.0" @@ -3537,6 +3542,7 @@ dependencies = [ "near-crypto", "near-o11y", "near-primitives", + "near-stdx", "num_cpus", "once_cell", "rand 0.8.5", @@ -3605,6 +3611,7 @@ dependencies = [ "near-o11y", "near-primitives", "near-primitives-core", + "near-stdx", "near-vm-errors", "ripemd", "serde", diff --git a/Cargo.toml b/Cargo.toml index f38c828d593..a8ae607527f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ members = [ "tools/themis", "utils/mainnet-res", "utils/near-cache", + "utils/stdx", ] [workspace.metadata.workspaces] @@ -202,6 +203,8 @@ wat = "1.0.40" xshell = "0.2.1" xz2 = "0.1.6" +stdx = { package = "near-stdx", path = "utils/stdx" } + [patch.crates-io] # Note that "bench" profile inherits from "release" profile and diff --git a/core/crypto/Cargo.toml b/core/crypto/Cargo.toml index b7044ae3162..a3834fbb4b5 100644 --- a/core/crypto/Cargo.toml +++ b/core/crypto/Cargo.toml @@ -25,6 +25,7 @@ rand = "0.7" secp256k1.workspace = true serde.workspace = true serde_json.workspace = true +stdx.workspace = true subtle.workspace = true thiserror.workspace = true diff --git a/core/crypto/src/util.rs b/core/crypto/src/util.rs index 61d7862daf9..9b939a617d9 100644 --- a/core/crypto/src/util.rs +++ b/core/crypto/src/util.rs @@ -58,18 +58,12 @@ impl, T2: Packable> Packable type Packed = [u8; 64]; fn unpack(data: &[u8; 64]) -> Option { - // TODO(mina86): Use split_array_ref once stabilised. - let d1 = unpack(data[..32].try_into().unwrap())?; - let d2 = unpack(data[32..].try_into().unwrap())?; - Some((d1, d2)) + let (d1, d2) = stdx::split_array::<64, 32, 32>(data); + Some((unpack(d1)?, unpack(d2)?)) } fn pack(&self) -> [u8; 64] { - let mut res = [0; 64]; - // TODO(mina86): Use split_array_mut once stabilised. - *<&mut [u8; 32]>::try_from(&mut res[..32]).unwrap() = self.0.pack(); - *<&mut [u8; 32]>::try_from(&mut res[32..]).unwrap() = self.1.pack(); - res + stdx::join_array(self.0.pack(), self.1.pack()) } } @@ -82,19 +76,18 @@ impl< type Packed = [u8; 96]; fn unpack(data: &[u8; 96]) -> Option { - // TODO(mina86): Use split_array_ref once stabilised. - let d1 = unpack(data[..32].try_into().unwrap())?; - let d2 = unpack(data[32..64].try_into().unwrap())?; - let d3 = unpack(data[64..].try_into().unwrap())?; - Some((d1, d2, d3)) + let (d1, d2) = stdx::split_array::<96, 32, 64>(data); + let (d2, d3) = stdx::split_array::<64, 32, 32>(d2); + Some((unpack(d1)?, unpack(d2)?, unpack(d3)?)) } fn pack(&self) -> [u8; 96] { let mut res = [0; 96]; - // TODO(mina86): Use split_array_mut once stabilised. - *<&mut [u8; 32]>::try_from(&mut res[..32]).unwrap() = self.0.pack(); - *<&mut [u8; 32]>::try_from(&mut res[32..64]).unwrap() = self.1.pack(); - *<&mut [u8; 32]>::try_from(&mut res[64..]).unwrap() = self.2.pack(); + let (d1, d2) = stdx::split_array_mut::<96, 32, 64>(&mut res); + let (d2, d3) = stdx::split_array_mut::<64, 32, 32>(d2); + *d1 = self.0.pack(); + *d2 = self.1.pack(); + *d3 = self.2.pack(); res } } diff --git a/core/store/Cargo.toml b/core/store/Cargo.toml index 58b52404d86..ae6f2a0c88b 100644 --- a/core/store/Cargo.toml +++ b/core/store/Cargo.toml @@ -25,6 +25,7 @@ rlimit.workspace = true rocksdb.workspace = true serde.workspace = true serde_json.workspace = true +stdx.workspace = true strum.workspace = true tempfile.workspace = true thiserror.workspace = true diff --git a/core/store/src/db/refcount.rs b/core/store/src/db/refcount.rs index 88400b51068..4c1d74a8a05 100644 --- a/core/store/src/db/refcount.rs +++ b/core/store/src/db/refcount.rs @@ -39,11 +39,8 @@ pub fn decode_value_with_rc(bytes: &[u8]) -> (Option<&[u8]>, i64) { debug_assert!(bytes.is_empty()); return (None, 0); } - // TODO(mina86): Use rsplit_array_ref once split_array feature is stabilised - // let (head, tail) = bytes.rsplit_array_ref<8>(); - // let rc = i64::from_le_bytes(tail); - let (head, tail) = bytes.split_at(bytes.len() - 8); - let rc = i64::from_le_bytes(tail.try_into().unwrap()); + let (head, tail) = stdx::rsplit_slice::<8>(bytes); + let rc = i64::from_le_bytes(*tail); if rc <= 0 { (None, rc) } else { diff --git a/runtime/near-vm-logic/Cargo.toml b/runtime/near-vm-logic/Cargo.toml index 110668393f6..d64c1d4306c 100644 --- a/runtime/near-vm-logic/Cargo.toml +++ b/runtime/near-vm-logic/Cargo.toml @@ -23,6 +23,7 @@ ripemd.workspace = true serde.workspace = true sha2.workspace = true sha3.workspace = true +stdx.workspace = true tracing = { workspace = true, optional = true } near-crypto = { path = "../../core/crypto" } diff --git a/runtime/near-vm-logic/src/alt_bn128.rs b/runtime/near-vm-logic/src/alt_bn128.rs index fffe5c18998..2b3e652b6eb 100644 --- a/runtime/near-vm-logic/src/alt_bn128.rs +++ b/runtime/near-vm-logic/src/alt_bn128.rs @@ -1,4 +1,4 @@ -use crate::array_utils::{join_array, split_array, ArrayChunks}; +use crate::array_utils::ArrayChunks; use bn::Group; use near_vm_errors::{HostError, VMLogicError}; @@ -40,7 +40,7 @@ pub(crate) fn g1_multiexp( ) -> Result<[u8; POINT_SIZE], InvalidInput> { let elements: Vec<(bn::G1, bn::Fr)> = elements .map(|chunk| { - let (g1, fr) = split_array(chunk); + let (g1, fr) = stdx::split_array(chunk); let g1 = decode_g1(g1)?; let fr = decode_fr(fr)?; Ok((g1, fr)) @@ -60,7 +60,7 @@ pub(crate) fn g1_sum( let elements: Vec<(bool, bn::G1)> = { elements .map(|chunk| { - let (sign, g1) = split_array(chunk); + let (sign, g1) = stdx::split_array(chunk); let sign = decode_bool(sign)?; let g1 = decode_g1(g1)?; Ok((sign, g1)) @@ -82,7 +82,7 @@ pub(crate) fn pairing_check( ) -> Result { let elements: Vec<(bn::G1, bn::G2)> = elements .map(|chunk| { - let (g1, g2) = split_array(chunk); + let (g1, g2) = stdx::split_array(chunk); let g1 = decode_g1(g1)?; let g2 = decode_g2(g2)?; Ok((g1, g2)) @@ -100,7 +100,7 @@ fn encode_g1(val: bn::G1) -> [u8; POINT_SIZE] { .unwrap_or_else(|| (bn::Fq::zero(), bn::Fq::zero())); let x = encode_fq(x); let y = encode_fq(y); - join_array(x, y) + stdx::join_array(x, y) } fn encode_fq(val: bn::Fq) -> [u8; SCALAR_SIZE] { @@ -109,11 +109,11 @@ fn encode_fq(val: bn::Fq) -> [u8; SCALAR_SIZE] { fn encode_u256(val: bn::arith::U256) -> [u8; SCALAR_SIZE] { let [lo, hi] = val.0; - join_array(lo.to_le_bytes(), hi.to_le_bytes()) + stdx::join_array(lo.to_le_bytes(), hi.to_le_bytes()) } fn decode_g1(raw: &[u8; POINT_SIZE]) -> Result { - let (x, y) = split_array(raw); + let (x, y) = stdx::split_array(raw); let x = decode_fq(x)?; let y = decode_fq(y)?; if x.is_zero() && y.is_zero() { @@ -131,7 +131,7 @@ fn decode_fq(raw: &[u8; SCALAR_SIZE]) -> Result { } fn decode_g2(raw: &[u8; 2 * POINT_SIZE]) -> Result { - let (x, y) = split_array(raw); + let (x, y) = stdx::split_array(raw); let x = decode_fq2(x)?; let y = decode_fq2(y)?; if x.is_zero() && y.is_zero() { @@ -144,7 +144,7 @@ fn decode_g2(raw: &[u8; 2 * POINT_SIZE]) -> Result { } fn decode_fq2(raw: &[u8; 2 * SCALAR_SIZE]) -> Result { - let (real, imaginary) = split_array(raw); + let (real, imaginary) = stdx::split_array(raw); let real = decode_fq(real)?; let imaginary = decode_fq(imaginary)?; Ok(bn::Fq2::new(real, imaginary)) @@ -156,7 +156,7 @@ fn decode_fr(raw: &[u8; SCALAR_SIZE]) -> Result { } fn decode_u256(raw: &[u8; SCALAR_SIZE]) -> bn::arith::U256 { - let (lo, hi) = split_array(raw); + let (lo, hi) = stdx::split_array(raw); let lo = u128::from_le_bytes(*lo); let hi = u128::from_le_bytes(*hi); bn::arith::U256([lo, hi]) diff --git a/runtime/near-vm-logic/src/array_utils.rs b/runtime/near-vm-logic/src/array_utils.rs index ee62a4020c8..df86d4d7413 100644 --- a/runtime/near-vm-logic/src/array_utils.rs +++ b/runtime/near-vm-logic/src/array_utils.rs @@ -2,30 +2,6 @@ use std::slice::ChunksExact; -/// Splits `&[u8; A + B]` into `(&[u8; A], &[u8; B])`. -pub(crate) fn split_array( - xs: &[u8; N], -) -> (&[u8; L], &[u8; R]) { - let () = AssertEqSum::::OK; - - let (left, right) = xs.split_at(L); - (left.try_into().unwrap(), right.try_into().unwrap()) -} - -/// Joins `[u8; A]` and `[u8; B]` into `[u8; A + B]`. -pub(crate) fn join_array( - left: [u8; L], - right: [u8; R], -) -> [u8; N] { - let () = AssertEqSum::::OK; - - let mut res = [0; N]; - let (l, r) = res.split_at_mut(L); - l.copy_from_slice(&left); - r.copy_from_slice(&right); - res -} - /// Converts an `&[u8]` slice of length `N * k` into iterator of `k` `[u8; N]` /// chunks. pub(crate) struct ArrayChunks<'a, const N: usize> { @@ -55,9 +31,3 @@ impl<'a, const N: usize> Iterator for ArrayChunks<'a, N> { } impl<'a, const N: usize> ExactSizeIterator for ArrayChunks<'a, N> {} - -/// Asserts, at compile time, that `S == A + B`. -struct AssertEqSum; -impl AssertEqSum { - const OK: () = [()][A + B - S]; -} diff --git a/utils/stdx/Cargo.toml b/utils/stdx/Cargo.toml new file mode 100644 index 00000000000..c4eca4778ae --- /dev/null +++ b/utils/stdx/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "near-stdx" +version = "0.0.0" +authors.workspace = true +edition.workspace = true +publish = true +rust-version.workspace = true +license = "MIT OR Apache-2.0" +repository = "https://github.com/near/nearcore" +description = """ +This crate contains polyfills which should really be in std, but currently aren't for one reason or another. +""" + +[dependencies] +# Absolutely must not depend on any crates from nearcore workspace, +# and should have as few dependencies as possible otherwise. diff --git a/utils/stdx/LICENSE-APACHE b/utils/stdx/LICENSE-APACHE new file mode 100644 index 00000000000..f49a4e16e68 --- /dev/null +++ b/utils/stdx/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/utils/stdx/LICENSE-MIT b/utils/stdx/LICENSE-MIT new file mode 100644 index 00000000000..749aa1ecd9b --- /dev/null +++ b/utils/stdx/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/utils/stdx/src/lib.rs b/utils/stdx/src/lib.rs new file mode 100644 index 00000000000..45e1ad31c25 --- /dev/null +++ b/utils/stdx/src/lib.rs @@ -0,0 +1,91 @@ +//! `stdx` crate contains polyfills which should really be in std, +//! but currently aren't for one reason or another. + +// TODO(mina86): Replace usage of the split functions by split_array_ref et al +// methods of array and slice types once those are stabilised. + +/// Splits `&[u8; L + R]` into `(&[u8; L], &[u8; R])`. +pub fn split_array( + xs: &[u8; N], +) -> (&[u8; L], &[u8; R]) { + let () = AssertEqSum::::OK; + + let (left, right) = xs.split_at(L); + (left.try_into().unwrap(), right.try_into().unwrap()) +} + +/// Splits `&mut [u8; L + R]` into `(&mut [u8; L], &mut [u8; R])`. +pub fn split_array_mut( + xs: &mut [u8; N], +) -> (&mut [u8; L], &mut [u8; R]) { + let () = AssertEqSum::::OK; + + let (left, right) = xs.split_at_mut(L); + (left.try_into().unwrap(), right.try_into().unwrap()) +} + +/// Splits `&[u8]` into `(&[u8; N], &[u8])`. **Panics** if slice is shorter +/// than `N`. +pub fn split_slice(slice: &[u8]) -> (&[u8; N], &[u8]) { + let (head, tail) = slice.split_at(N); + (head.try_into().unwrap(), tail) +} + +/// Splits `&[u8]` into `(&[u8], &[u8; N])`. **Panics** if slice is shorter +/// than `N`. +pub fn rsplit_slice(slice: &[u8]) -> (&[u8], &[u8; N]) { + let index = slice.len().checked_sub(N).expect("len to be ≥ N"); + let (head, tail) = slice.split_at(index); + (head, tail.try_into().unwrap()) +} + +/// Splits `&[u8]` into `(&[u8; N], &[u8])`. **Panics** if slice is shorter +/// than `N`. +pub fn split_slice_mut(slice: &mut [u8]) -> (&mut [u8; N], &mut [u8]) { + let (head, tail) = slice.split_at_mut(N); + (head.try_into().unwrap(), tail) +} + +/// Splits `&[u8]` into `(&[u8], &[u8; N])`. **Panics** if slice is shorter +/// than `N`. +pub fn rsplit_slice_mut(slice: &mut [u8]) -> (&mut [u8], &mut [u8; N]) { + let index = slice.len().checked_sub(N).expect("len to be ≥ N"); + let (head, tail) = slice.split_at_mut(index); + (head, tail.try_into().unwrap()) +} + +#[test] +fn test_split() { + assert_eq!((&[0, 1], &[2, 3, 4]), split_array(&[0, 1, 2, 3, 4])); + assert_eq!((&mut [0, 1], &mut [2, 3, 4]), split_array_mut(&mut [0, 1, 2, 3, 4])); + + assert_eq!((&[0, 1], &[2, 3, 4][..]), split_slice(&[0, 1, 2, 3, 4])); + assert_eq!((&[0, 1][..], &[2, 3, 4]), rsplit_slice(&[0, 1, 2, 3, 4])); + assert_eq!((&mut [0, 1], &mut [2, 3, 4][..]), split_slice_mut(&mut [0, 1, 2, 3, 4])); + assert_eq!((&mut [0, 1][..], &mut [2, 3, 4]), rsplit_slice_mut(&mut [0, 1, 2, 3, 4])); +} + +/// Joins `[u8; L]` and `[u8; R]` into `[u8; L + R]`. +pub fn join_array( + left: [u8; L], + right: [u8; R], +) -> [u8; N] { + let () = AssertEqSum::::OK; + + let mut res = [0; N]; + let (l, r) = res.split_at_mut(L); + l.copy_from_slice(&left); + r.copy_from_slice(&right); + res +} + +#[test] +fn test_join() { + assert_eq!([0, 1, 2, 3], join_array([0, 1], [2, 3])); +} + +/// Asserts, at compile time, that `S == A + B`. +struct AssertEqSum; +impl AssertEqSum { + const OK: () = [()][A + B - S]; +} From fdacf1e306601cffbc6a5d1798bed288ba2f29c3 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Mon, 21 Nov 2022 18:33:34 +0100 Subject: [PATCH 017/188] store: detect spurious bytes at the end of RawTrieNode encoding (#8096) * store: detect spurious bytes at the end of RawTrieNode encoding * fmt --- core/store/src/trie/mod.rs | 64 +++++++++++++++----------- docs/architecture/how/serialization.md | 13 +++--- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/core/store/src/trie/mod.rs b/core/store/src/trie/mod.rs index 7461ee4433e..2fa30b7ec9f 100644 --- a/core/store/src/trie/mod.rs +++ b/core/store/src/trie/mod.rs @@ -1,6 +1,6 @@ use std::cell::RefCell; use std::collections::HashMap; -use std::io::{Cursor, Read}; +use std::io::Read; use borsh::{BorshDeserialize, BorshSerialize}; use byteorder::{LittleEndian, ReadBytesExt}; @@ -338,15 +338,15 @@ const BRANCH_NODE_NO_VALUE: u8 = 1; const BRANCH_NODE_WITH_VALUE: u8 = 2; const EXTENSION_NODE: u8 = 3; -fn decode_children(cursor: &mut Cursor<&[u8]>) -> Result<[Option; 16], std::io::Error> { +fn decode_children(bytes: &mut &[u8]) -> Result<[Option; 16], std::io::Error> { let mut children: [Option; 16] = Default::default(); - let bitmap = cursor.read_u16::()?; + let bitmap = bytes.read_u16::()?; let mut pos = 1; for child in &mut children { if bitmap & pos != 0 { let mut arr = [0; 32]; - cursor.read_exact(&mut arr)?; - *child = Some(CryptoHash::try_from(&arr[..]).unwrap()); + bytes.read_exact(&mut arr)?; + *child = Some(CryptoHash(arr)); } pos <<= 1; } @@ -399,40 +399,44 @@ impl RawTrieNode { } } - fn decode(bytes: &[u8]) -> Result { - let mut cursor = Cursor::new(bytes); - match cursor.read_u8()? { + fn decode(mut bytes: &[u8]) -> Result { + let node = match bytes.read_u8()? { LEAF_NODE => { - let key_length = cursor.read_u32::()?; + let key_length = bytes.read_u32::()?; let mut key = vec![0; key_length as usize]; - cursor.read_exact(&mut key)?; - let value_length = cursor.read_u32::()?; + bytes.read_exact(&mut key)?; + let value_length = bytes.read_u32::()?; let mut arr = [0; 32]; - cursor.read_exact(&mut arr)?; + bytes.read_exact(&mut arr)?; let value_hash = CryptoHash(arr); - Ok(RawTrieNode::Leaf(key, value_length, value_hash)) + RawTrieNode::Leaf(key, value_length, value_hash) } BRANCH_NODE_NO_VALUE => { - let children = decode_children(&mut cursor)?; - Ok(RawTrieNode::Branch(children, None)) + let children = decode_children(&mut bytes)?; + RawTrieNode::Branch(children, None) } BRANCH_NODE_WITH_VALUE => { - let value_length = cursor.read_u32::()?; + let value_length = bytes.read_u32::()?; let mut arr = [0; 32]; - cursor.read_exact(&mut arr)?; + bytes.read_exact(&mut arr)?; let value_hash = CryptoHash(arr); - let children = decode_children(&mut cursor)?; - Ok(RawTrieNode::Branch(children, Some((value_length, value_hash)))) + let children = decode_children(&mut bytes)?; + RawTrieNode::Branch(children, Some((value_length, value_hash))) } EXTENSION_NODE => { - let key_length = cursor.read_u32::()?; + let key_length = bytes.read_u32::()?; let mut key = vec![0; key_length as usize]; - cursor.read_exact(&mut key)?; + bytes.read_exact(&mut key)?; let mut child = [0; 32]; - cursor.read_exact(&mut child)?; - Ok(RawTrieNode::Extension(key, CryptoHash(child))) + bytes.read_exact(&mut child)?; + RawTrieNode::Extension(key, CryptoHash(child)) } - _ => Err(std::io::Error::new(std::io::ErrorKind::Other, "Wrong type")), + _ => return Err(std::io::Error::new(std::io::ErrorKind::Other, "Wrong type")), + }; + if bytes.is_empty() { + Ok(node) + } else { + Err(std::io::Error::new(std::io::ErrorKind::Other, "Spurious data at end")) } } } @@ -453,10 +457,9 @@ impl RawTrieNodeWithSize { if bytes.len() < 8 { return Err(std::io::Error::new(std::io::ErrorKind::Other, "Wrong type")); } - let node = RawTrieNode::decode(&bytes[0..bytes.len() - 8])?; - let mut arr: [u8; 8] = Default::default(); - arr.copy_from_slice(&bytes[bytes.len() - 8..]); - let memory_usage = u64::from_le_bytes(arr); + let (bytes, memory_usage) = stdx::rsplit_slice(bytes); + let node = RawTrieNode::decode(bytes)?; + let memory_usage = u64::from_le_bytes(*memory_usage); Ok(RawTrieNodeWithSize { node, memory_usage }) } } @@ -1062,6 +1065,11 @@ mod tests { node.encode_into(&mut buf); assert_eq!(encoded, buf.as_slice()); assert_eq!(node, RawTrieNode::decode(&buf).unwrap()); + + // Test that adding garbage at the end fails decoding. + buf.push(b'!'); + let got = RawTrieNode::decode(&buf); + assert!(got.is_err(), "got: {got:?}"); } let value_length = 3; diff --git a/docs/architecture/how/serialization.md b/docs/architecture/how/serialization.md index 4b1f4738273..3da46aa5bdb 100644 --- a/docs/architecture/how/serialization.md +++ b/docs/architecture/how/serialization.md @@ -143,17 +143,16 @@ similar to Borsh, but a little different): RawTrieNode If you look into store/src/trie/mod.rs, you’ll be able to find a method called ‘encode_into’: ```rust - fn encode_into(&self, out: &mut Vec) -> Result<(), std::io::Error> { - let mut cursor = Cursor::new(out); + fn encode_into(&self, out: &mut Vec) { // size in state_parts = size + 8 for RawTrieNodeWithSize + 8 for borsh vector length match &self { // size <= 1 + 4 + 4 + 32 + key_length + value_length RawTrieNode::Leaf(key, value_length, value_hash) => { - cursor.write_u8(LEAF_NODE)?; - cursor.write_u32::(key.len() as u32)?; - cursor.write_all(key)?; - cursor.write_u32::(*value_length)?; - cursor.write_all(value_hash.as_ref())?; + out.push(LEAF_NODE); + out.extend((key.len() as u32).to_le_bytes()); + out.extend(key); + out.extend((*value_length as u32).to_le_bytes()); + out.extend(value_hash.as_bytes()); } //... more code ``` From 1afa70aa314e1d9d816f13a4ae2ff8337b0097d6 Mon Sep 17 00:00:00 2001 From: nikurt <86772482+nikurt@users.noreply.github.com> Date: Mon, 21 Nov 2022 19:05:54 +0100 Subject: [PATCH 018/188] A tool to dump a single or all state parts of the state (#8095) Tested by running on a testnet node. `--block-hash` can be obtained by running `neard view-state epoch-info current`. https://pagodaplatform.atlassian.net/browse/ND-254 cc: @mm-near Dumping all parts of `shard 3` took 8 hours. Will optimize this tool in a follow-up PR. --- Cargo.lock | 1 + chain/chain/src/chain.rs | 7 +++ core/primitives/Cargo.toml | 1 + core/primitives/src/state_part.rs | 2 +- core/store/src/trie/iterator.rs | 4 ++ nearcore/src/runtime/mod.rs | 7 +++ tools/state-viewer/src/cli.rs | 34 +++++++++++ tools/state-viewer/src/dump_state_parts.rs | 67 ++++++++++++++++++++++ tools/state-viewer/src/lib.rs | 1 + 9 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 tools/state-viewer/src/dump_state_parts.rs diff --git a/Cargo.lock b/Cargo.lock index ef579558b28..31bcee94268 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3431,6 +3431,7 @@ dependencies = [ "smart-default", "strum", "thiserror", + "tracing", ] [[package]] diff --git a/chain/chain/src/chain.rs b/chain/chain/src/chain.rs index 0f09e893bec..8598a7f2938 100644 --- a/chain/chain/src/chain.rs +++ b/chain/chain/src/chain.rs @@ -2859,6 +2859,13 @@ impl Chain { part_id: u64, sync_hash: CryptoHash, ) -> Result, Error> { + let _span = tracing::debug_span!( + target: "sync", + "get_state_response_part", + shard_id, + part_id, + %sync_hash) + .entered(); // Check cache let key = StatePartKey(sync_hash, shard_id, part_id).try_to_vec()?; if let Ok(Some(state_part)) = self.store.store().get(DBCol::StateParts, &key) { diff --git a/core/primitives/Cargo.toml b/core/primitives/Cargo.toml index ae758168f57..c6663b07e4d 100644 --- a/core/primitives/Cargo.toml +++ b/core/primitives/Cargo.toml @@ -32,6 +32,7 @@ serde_json.workspace = true smart-default.workspace = true strum.workspace = true thiserror.workspace = true +tracing.workspace = true near-crypto = { path = "../crypto" } near-o11y = { path = "../o11y" } diff --git a/core/primitives/src/state_part.rs b/core/primitives/src/state_part.rs index b62c12d19da..18fdb207c06 100644 --- a/core/primitives/src/state_part.rs +++ b/core/primitives/src/state_part.rs @@ -1,5 +1,5 @@ // to specify a part we always specify both part_id and num_parts together -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub struct PartId { pub idx: u64, pub total: u64, diff --git a/core/store/src/trie/iterator.rs b/core/store/src/trie/iterator.rs index 3a508af68d0..e43629cec15 100644 --- a/core/store/src/trie/iterator.rs +++ b/core/store/src/trie/iterator.rs @@ -301,6 +301,10 @@ impl<'a> TrieIterator<'a> { path_begin: &[u8], path_end: &[u8], ) -> Result, StorageError> { + let _span = tracing::debug_span!( + target: "runtime", + "visit_nodes_interval") + .entered(); let path_begin_encoded = NibbleSlice::encode_nibbles(path_begin, true); let last_hash = self.seek_nibble_slice(NibbleSlice::from_encoded(&path_begin_encoded).0, false)?; diff --git a/nearcore/src/runtime/mod.rs b/nearcore/src/runtime/mod.rs index 64e902eb869..4209f768664 100644 --- a/nearcore/src/runtime/mod.rs +++ b/nearcore/src/runtime/mod.rs @@ -1203,6 +1203,13 @@ impl RuntimeAdapter for NightshadeRuntime { state_root: &StateRoot, part_id: PartId, ) -> Result, Error> { + let _span = tracing::debug_span!( + target: "runtime", + "obtain_state_part", + shard_id, + %block_hash, + ?part_id) + .entered(); let epoch_id = self.get_epoch_id(block_hash)?; let shard_uid = self.get_shard_uid_from_epoch_id(shard_id, &epoch_id)?; let trie = self.tries.get_view_trie_for_shard(shard_uid, state_root.clone()); diff --git a/tools/state-viewer/src/cli.rs b/tools/state-viewer/src/cli.rs index 968a475ed0f..0a1715a8d1d 100644 --- a/tools/state-viewer/src/cli.rs +++ b/tools/state-viewer/src/cli.rs @@ -1,4 +1,5 @@ use crate::commands::*; +use crate::dump_state_parts::dump_state_parts; use crate::epoch_info; use crate::rocksdb_stats::get_rocksdb_stats; use clap::{Args, Parser, Subcommand}; @@ -70,6 +71,8 @@ pub enum StateViewerSubCommand { /// View trie structure. #[clap(alias = "view_trie")] ViewTrie(ViewTrieCmd), + /// Dump all or a single state part of a shard. + DumpStateParts(DumpStatePartsCmd), } impl StateViewerSubCommand { @@ -84,6 +87,7 @@ impl StateViewerSubCommand { StateViewerSubCommand::Peers => peers(store), StateViewerSubCommand::State => state(home_dir, near_config, hot), StateViewerSubCommand::DumpState(cmd) => cmd.run(home_dir, near_config, hot), + StateViewerSubCommand::DumpStateParts(cmd) => cmd.run(home_dir, near_config, hot), StateViewerSubCommand::DumpStateRedis(cmd) => cmd.run(home_dir, near_config, hot), StateViewerSubCommand::DumpTx(cmd) => cmd.run(home_dir, near_config, hot), StateViewerSubCommand::Chain(cmd) => cmd.run(home_dir, near_config, hot), @@ -470,3 +474,33 @@ impl ViewTrieCmd { view_trie(store, hash, self.shard_id, self.shard_version, self.max_depth).unwrap(); } } + +#[derive(Parser)] +pub struct DumpStatePartsCmd { + /// Last block of a previous epoch. + #[clap(long)] + block_hash: CryptoHash, + /// Shard id. + #[clap(long)] + shard_id: ShardId, + /// State part id. Leave empty to go through every part in the shard. + #[clap(long)] + part_id: Option, + /// Where to write the state parts to. + #[clap(long)] + output_dir: PathBuf, +} + +impl DumpStatePartsCmd { + pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { + dump_state_parts( + self.block_hash, + self.shard_id, + self.part_id, + home_dir, + near_config, + store, + &self.output_dir, + ); + } +} diff --git a/tools/state-viewer/src/dump_state_parts.rs b/tools/state-viewer/src/dump_state_parts.rs new file mode 100644 index 00000000000..50d1eaaddec --- /dev/null +++ b/tools/state-viewer/src/dump_state_parts.rs @@ -0,0 +1,67 @@ +use near_chain::{ChainStore, ChainStoreAccess, RuntimeAdapter}; +use near_primitives::state_part::PartId; +use near_primitives::syncing::get_num_state_parts; +use near_primitives_core::hash::CryptoHash; +use near_primitives_core::types::ShardId; +use near_store::Store; +use nearcore::{NearConfig, NightshadeRuntime}; +use std::path::Path; +use std::sync::Arc; + +pub(crate) fn dump_state_parts( + sync_prev_hash: CryptoHash, + shard_id: ShardId, + part_id: Option, + home_dir: &Path, + near_config: NearConfig, + store: Store, + output_dir: &Path, +) { + let runtime_adapter: Arc = + Arc::new(NightshadeRuntime::from_config(home_dir, store.clone(), &near_config)); + + let chain_store = ChainStore::new( + store.clone(), + near_config.genesis.config.genesis_height, + !near_config.client_config.archive, + ); + + let sync_prev_block = chain_store.get_block(&sync_prev_hash).unwrap(); + + assert!(runtime_adapter.is_next_block_epoch_start(&sync_prev_hash).unwrap()); + + assert!( + shard_id < sync_prev_block.chunks().len() as u64, + "shard_id: {}, #shards: {}", + shard_id, + sync_prev_block.chunks().len() + ); + let state_root = sync_prev_block.chunks()[shard_id as usize].prev_state_root(); + let state_root_node = + runtime_adapter.get_state_root_node(shard_id, &sync_prev_hash, &state_root).unwrap(); + + let num_parts = get_num_state_parts(state_root_node.memory_usage); + tracing::debug!(num_parts); + + std::fs::create_dir_all(output_dir).unwrap(); + for part_id in if let Some(part_id) = part_id { part_id..part_id + 1 } else { 0..num_parts } { + assert!(part_id < num_parts, "part_id: {}, num_parts: {}", part_id, num_parts); + let state_part = runtime_adapter + .obtain_state_part( + shard_id, + &sync_prev_hash, + &state_root, + PartId::new(part_id, num_parts), + ) + .unwrap(); + let filename = output_dir.join(format!("state_part_{:06}", part_id)); + let len = state_part.len(); + std::fs::write(&filename, state_part).unwrap(); + tracing::debug!( + "part_id: {}, result length: {}, wrote {}", + part_id, + len, + filename.display() + ); + } +} diff --git a/tools/state-viewer/src/lib.rs b/tools/state-viewer/src/lib.rs index f84ff570f58..555c65b8ff3 100644 --- a/tools/state-viewer/src/lib.rs +++ b/tools/state-viewer/src/lib.rs @@ -4,6 +4,7 @@ mod apply_chain_range; mod apply_chunk; pub mod cli; mod commands; +mod dump_state_parts; mod epoch_info; mod rocksdb_stats; mod state_dump; From e20a8f783fd915939766b27bddc2628b70705485 Mon Sep 17 00:00:00 2001 From: Aleksandr Logunov Date: Mon, 21 Nov 2022 23:46:02 +0400 Subject: [PATCH 019/188] feat: finish code for background flat storage creation (#8053) Co-authored-by: Jakob Meier --- chain/chain/src/flat_storage_creator.rs | 299 ++++++++++++++++-- core/store/src/columns.rs | 7 +- core/store/src/flat_state.rs | 89 ++++-- core/store/src/lib.rs | 5 +- core/store/src/trie/iterator.rs | 2 +- core/store/src/trie/mod.rs | 2 +- core/store/src/trie/state_parts.rs | 2 +- .../src/tests/client/flat_storage.rs | 56 +++- nearcore/src/migrations.rs | 4 +- 9 files changed, 410 insertions(+), 56 deletions(-) diff --git a/chain/chain/src/flat_storage_creator.rs b/chain/chain/src/flat_storage_creator.rs index 518a3bd295c..c5db36333cb 100644 --- a/chain/chain/src/flat_storage_creator.rs +++ b/chain/chain/src/flat_storage_creator.rs @@ -1,14 +1,37 @@ +//! Logic for creating flat storage in parallel to chain processing. +//! +//! The main struct responsible is `FlatStorageShardCreator`. +//! After its creation, `update_status` is called periodically, which executes some part of flat storage creation +//! depending on what the current status is: +//! `SavingDeltas`: checks if we moved chain final head forward enough to have all flat storage deltas written on disk. +//! `FetchingState`: spawns threads for fetching some range state parts, waits for receiving results, writes key-value +//! parts to flat storage column on disk and spawns threads for new range once current range is finished. +//! `CatchingUp`: moves flat storage head forward, so it may reach chain final head. +//! `Ready`: flat storage is created and it is up-to-date. + use crate::{ChainStore, ChainStoreAccess, RuntimeAdapter}; #[cfg(feature = "protocol_feature_flat_state")] use assert_matches::assert_matches; use crossbeam_channel::{unbounded, Receiver, Sender}; use near_chain_primitives::Error; -use near_primitives::types::{BlockHeight, ShardId}; -#[cfg(feature = "protocol_feature_flat_state")] -use near_store::flat_state::store_helper; +use near_primitives::shard_layout::ShardUId; +use near_primitives::state::ValueRef; +use near_primitives::state_part::PartId; +use near_primitives::types::{BlockHeight, ShardId, StateRoot}; use near_store::flat_state::FlatStorageStateStatus; -use std::sync::Arc; #[cfg(feature = "protocol_feature_flat_state")] +use near_store::flat_state::{store_helper, FetchingStateStatus}; +#[cfg(feature = "protocol_feature_flat_state")] +use near_store::flat_state::{NUM_PARTS_IN_ONE_STEP, STATE_PART_MEMORY_LIMIT}; +use near_store::migrations::BatchedStoreUpdate; +#[cfg(feature = "protocol_feature_flat_state")] +use near_store::DBCol; +#[cfg(feature = "protocol_feature_flat_state")] +use near_store::FlatStateDelta; +use near_store::Store; +use near_store::{Trie, TrieDBStorage, TrieTraversalItem}; +use std::sync::atomic::AtomicU64; +use std::sync::Arc; use tracing::debug; use tracing::info; @@ -23,18 +46,25 @@ pub struct FlatStorageShardCreator { start_height: BlockHeight, #[allow(unused)] runtime_adapter: Arc, - /// Tracks number of traversed state parts during a single step. + /// Tracks number of state parts which are not fetched yet during a single step. + /// Stores Some(parts) if threads for fetching state were spawned and None otherwise. #[allow(unused)] - fetched_state_parts: Option, + remaining_state_parts: Option, /// Used by threads which traverse state parts to tell that traversal is finished. #[allow(unused)] fetched_parts_sender: Sender, /// Used by main thread to update the number of traversed state parts. #[allow(unused)] fetched_parts_receiver: Receiver, + #[allow(unused)] + visited_trie_items: u64, } impl FlatStorageShardCreator { + /// Maximal number of blocks which can be caught up during one step. + #[cfg(feature = "protocol_feature_flat_state")] + const CATCH_UP_BLOCKS: usize = 50; + pub fn new( shard_id: ShardId, start_height: BlockHeight, @@ -45,30 +75,107 @@ impl FlatStorageShardCreator { shard_id, start_height, runtime_adapter, - fetched_state_parts: None, + remaining_state_parts: None, fetched_parts_sender, fetched_parts_receiver, + visited_trie_items: 0, + } + } + + #[allow(unused)] + fn nibbles_to_hex(key_nibbles: &[u8]) -> String { + let path_prefix = match key_nibbles.last() { + Some(16) => &key_nibbles[..key_nibbles.len() - 1], + _ => &key_nibbles, + }; + path_prefix + .iter() + .map(|&n| char::from_digit(n as u32, 16).expect("nibble should be <16")) + .collect() + } + + /// Fetch state part, write all state items to flat storage and send the number of items to the given channel. + #[allow(unused)] + fn fetch_state_part( + store: Store, + shard_uid: ShardUId, + state_root: StateRoot, + part_id: PartId, + progress: Arc, + result_sender: Sender, + ) { + let trie_storage = TrieDBStorage::new(store.clone(), shard_uid); + let trie = Trie::new(Box::new(trie_storage), state_root, None); + let path_begin = trie.find_path_for_part_boundary(part_id.idx, part_id.total).unwrap(); + let path_end = trie.find_path_for_part_boundary(part_id.idx + 1, part_id.total).unwrap(); + let hex_path_begin = Self::nibbles_to_hex(&path_begin); + debug!(target: "store", "Preload state part from {hex_path_begin}"); + let mut trie_iter = trie.iter().unwrap(); + + let mut store_update = BatchedStoreUpdate::new(&store, 10_000_000); + let mut num_items = 0; + for TrieTraversalItem { hash, key } in + trie_iter.visit_nodes_interval(&path_begin, &path_end).unwrap() + { + match key { + None => {} + Some(key) => { + let value = trie.storage.retrieve_raw_bytes(&hash).unwrap(); + let value_ref = ValueRef::new(&value); + #[cfg(feature = "protocol_feature_flat_state")] + store_update + .set_ser(DBCol::FlatState, &key, &value_ref) + .expect("Failed to put value in FlatState"); + #[cfg(not(feature = "protocol_feature_flat_state"))] + let (_, _) = (key, value_ref); + + num_items += 1; + } + } } + store_update.finish().unwrap(); + + let processed_parts = progress.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1; + + debug!(target: "store", + "Preload subtrie at {hex_path_begin} done, \ + loaded {num_items} state items, \ + proccessed parts: {processed_parts}" + ); + + result_sender.send(num_items).unwrap(); } + /// Checks current flat storage creation status, execute work related to it and possibly switch to next status. + /// Creates flat storage when all intermediate steps are finished. #[cfg(feature = "protocol_feature_flat_state")] - pub(crate) fn update_status(&mut self, chain_store: &ChainStore) -> Result<(), Error> { + pub(crate) fn update_status( + &mut self, + chain_store: &ChainStore, + thread_pool: &rayon::ThreadPool, + ) -> Result<(), Error> { let current_status = store_helper::get_flat_storage_state_status(chain_store.store(), self.shard_id); - match current_status { + let shard_id = self.shard_id; + match ¤t_status { FlatStorageStateStatus::SavingDeltas => { let final_head = chain_store.final_head()?; - let shard_id = self.shard_id; + let final_height = final_head.height; - if final_head.height > self.start_height { + if final_height > self.start_height { // If it holds, deltas for all blocks after final head are saved to disk, because they have bigger // heights than one on which we launched a node. Check that it is true: - for height in final_head.height + 1..=chain_store.head()?.height { - for (_, hashes) in - chain_store.get_all_block_hashes_by_height(height)?.iter() + for height in final_height + 1..=chain_store.head()?.height { + // We skip heights for which there are no blocks, because certain heights can be skipped. + // TODO (#8057): make `get_all_block_hashes_by_height` return empty hashmap instead of error + // in such case. + for (_, hashes) in chain_store + .get_all_block_hashes_by_height(height) + .unwrap_or_default() + .iter() { for hash in hashes { - debug!(target: "chain", %shard_id, %height, %hash, "Checking delta existence"); + debug!(target: "store", %shard_id, %height, %hash, "Checking delta existence"); assert_matches!( store_helper::get_delta( chain_store.store(), @@ -83,19 +190,169 @@ impl FlatStorageShardCreator { // We continue saving deltas, and also start fetching state. let block_hash = final_head.last_block_hash; + let store = self.runtime_adapter.store().clone(); + let epoch_id = self.runtime_adapter.get_epoch_id(&block_hash)?; + let shard_uid = self.runtime_adapter.shard_id_to_uid(shard_id, &epoch_id)?; + let trie_storage = TrieDBStorage::new(store.clone(), shard_uid); + let state_root = + chain_store.get_chunk_extra(&block_hash, &shard_uid)?.state_root().clone(); + let trie = Trie::new(Box::new(trie_storage), state_root, None); + let root_node = trie.retrieve_root_node().unwrap(); + let num_state_parts = + root_node.memory_usage / STATE_PART_MEMORY_LIMIT.as_u64() + 1; + let status = FetchingStateStatus { + part_id: 0, + num_parts_in_step: NUM_PARTS_IN_ONE_STEP, + num_parts: num_state_parts, + }; + info!(target: "store", %shard_id, %final_height, ?status, "Switching status to fetching state"); + let mut store_update = chain_store.store().store_update(); store_helper::set_flat_head(&mut store_update, shard_id, &block_hash); - store_helper::set_fetching_state_step(&mut store_update, shard_id, 0u64); + store_helper::set_fetching_state_status(&mut store_update, shard_id, status); store_update.commit()?; } Ok(()) } - FlatStorageStateStatus::FetchingState((_block_hash, _fetching_state_step)) => { - // TODO: spawn threads and collect results + FlatStorageStateStatus::FetchingState(fetching_state_status) => { + let store = self.runtime_adapter.store().clone(); + let block_hash = store_helper::get_flat_head(&store, shard_id).unwrap(); + let start_part_id = fetching_state_status.part_id; + let num_parts_in_step = fetching_state_status.num_parts_in_step; + let num_parts = fetching_state_status.num_parts; + let next_start_part_id = num_parts.min(start_part_id + num_parts_in_step); + + match self.remaining_state_parts.clone() { + None => { + // We need to spawn threads to fetch state parts and fill flat storage data. + let epoch_id = self.runtime_adapter.get_epoch_id(&block_hash)?; + let shard_uid = + self.runtime_adapter.shard_id_to_uid(shard_id, &epoch_id)?; + let state_root = chain_store + .get_chunk_extra(&block_hash, &shard_uid)? + .state_root() + .clone(); + let progress = Arc::new(std::sync::atomic::AtomicU64::new(0)); + debug!( + target: "store", %shard_id, %block_hash, %start_part_id, %next_start_part_id, %num_parts, + "Spawning threads to fetch state parts for flat storage" + ); + + for part_id in start_part_id..next_start_part_id { + let inner_store = store.clone(); + let inner_state_root = state_root.clone(); + let inner_progress = progress.clone(); + let inner_sender = self.fetched_parts_sender.clone(); + thread_pool.spawn(move || { + Self::fetch_state_part( + inner_store, + shard_uid, + inner_state_root, + PartId::new(part_id, num_parts), + inner_progress, + inner_sender, + ); + }) + } + + self.remaining_state_parts = Some(next_start_part_id - start_part_id); + Ok(()) + } + Some(state_parts) if state_parts > 0 => { + // If not all state parts were fetched, try receiving new results. + let mut updated_state_parts = state_parts; + while let Ok(n) = self.fetched_parts_receiver.try_recv() { + updated_state_parts -= 1; + self.visited_trie_items += n; + } + self.remaining_state_parts = Some(updated_state_parts); + Ok(()) + } + Some(_) => { + // Mark that we don't wait for new state parts. + self.remaining_state_parts = None; + + let mut store_update = chain_store.store().store_update(); + if next_start_part_id < num_parts { + // If there are still remaining state parts, switch status to the new range of state parts. + // We will spawn new rayon tasks on the next status update. + let new_status = FetchingStateStatus { + part_id: next_start_part_id, + num_parts_in_step, + num_parts, + }; + debug!(target: "chain", %shard_id, %block_hash, ?new_status); + store_helper::set_fetching_state_status( + &mut store_update, + shard_id, + new_status, + ); + } else { + // If all parts were fetched, we can start catchup. + info!(target: "chain", %shard_id, %block_hash, "Finished fetching state"); + store_helper::remove_fetching_state_status(&mut store_update, shard_id); + store_helper::start_catchup(&mut store_update, shard_id); + } + store_update.commit()?; + + Ok(()) + } + } + } + FlatStorageStateStatus::CatchingUp => { + let store = self.runtime_adapter.store(); + let old_flat_head = store_helper::get_flat_head(store, shard_id).unwrap(); + let mut flat_head = old_flat_head.clone(); + let chain_final_head = chain_store.final_head()?; + let mut merged_delta = FlatStateDelta::default(); + + // Merge up to 50 deltas of the next blocks until we reach chain final head. + // TODO: consider merging 10 deltas at once to limit memory usage + for _ in 0..Self::CATCH_UP_BLOCKS { + let height = chain_store.get_block_height(&flat_head).unwrap(); + if height > chain_final_head.height { + panic!("New flat head moved too far: new head = {flat_head}, height = {height}, final block height = {}", chain_final_head.height); + } + // Stop if we reached chain final head. + if flat_head == chain_final_head.last_block_hash { + break; + } + flat_head = chain_store.get_next_block_hash(&flat_head).unwrap(); + let delta = + store_helper::get_delta(store, shard_id, flat_head).unwrap().unwrap(); + merged_delta.merge(delta.as_ref()); + } + + if old_flat_head != flat_head { + // If flat head changes, save all changes to store. + let old_height = chain_store.get_block_height(&old_flat_head).unwrap(); + let height = chain_store.get_block_height(&flat_head).unwrap(); + debug!(target: "chain", %shard_id, %old_flat_head, %old_height, %flat_head, %height, "Catching up flat head"); + let mut store_update = self.runtime_adapter.store().store_update(); + store_helper::set_flat_head(&mut store_update, shard_id, &flat_head); + merged_delta.apply_to_flat_state(&mut store_update); + + if flat_head == chain_final_head.last_block_hash { + // If we reached chain final head, we can finish catchup and finally create flat storage. + store_helper::finish_catchup(&mut store_update, shard_id); + store_update.commit()?; + let status = self.runtime_adapter.try_create_flat_storage_state_for_shard( + shard_id, + chain_store.head().unwrap().height, + chain_store, + ); + assert_eq!(status, FlatStorageStateStatus::Ready); + info!(target: "chain", %shard_id, %flat_head, %height, "Flat storage creation done"); + } else { + store_update.commit()?; + } + } + Ok(()) } - _ => { - panic!("Status {:?} is not supported yet", current_status); + FlatStorageStateStatus::Ready => Ok(()), + FlatStorageStateStatus::DontCreate => { + panic!("We initiated flat storage creation for shard {shard_id} but according to flat storage state status in db it cannot be created"); } } } @@ -161,7 +418,7 @@ impl FlatStorageCreator { } #[cfg(feature = "protocol_feature_flat_state")] - self.shard_creators[shard_id as usize].update_status(chain_store)?; + self.shard_creators[shard_id as usize].update_status(chain_store, &self.pool)?; Ok(()) } diff --git a/core/store/src/columns.rs b/core/store/src/columns.rs index 91af62993fd..5111d2cca08 100644 --- a/core/store/src/columns.rs +++ b/core/store/src/columns.rs @@ -252,9 +252,10 @@ pub enum DBCol { /// - *Column type*: `FlatStateDelta` #[cfg(feature = "protocol_feature_flat_state")] FlatStateDeltas, - /// Miscellaneous data for flat state. Currently stores flat state head for each shard. - /// - *Rows*: shard id - /// - *Column type*: block hash (CryptoHash) + /// Miscellaneous data for flat state. Stores intermediate flat storage creation statuses and flat + /// state heads for each shard. + /// - *Rows*: Unique key prefix (e.g. `FLAT_STATE_HEAD_KEY_PREFIX`) + ShardId + /// - *Column type*: FetchingStateStatus || flat storage catchup status (bool) || flat storage head (CryptoHash) // TODO (#7327): use only during testing, come up with proper format. #[cfg(feature = "protocol_feature_flat_state")] FlatStateMisc, diff --git a/core/store/src/flat_state.rs b/core/store/src/flat_state.rs index d8c0161f613..2415786eeb5 100644 --- a/core/store/src/flat_state.rs +++ b/core/store/src/flat_state.rs @@ -443,14 +443,24 @@ struct FlatStorageStateInner { deltas: HashMap>, } -/// Number of parts to which we divide shard state for parallel traversal. -// TODO: consider changing it for different shards, ensure that shard memory usage / `NUM_PARTS` < X MiB. +/// Number of traversed parts during a single step of fetching state. #[allow(unused)] -const NUM_FETCHED_STATE_PARTS: u64 = 4_000; +pub const NUM_PARTS_IN_ONE_STEP: u64 = 20; -/// Number of traversed parts during a single step of fetching state. +/// Memory limit for state part being fetched. #[allow(unused)] -pub const NUM_PARTS_IN_ONE_STEP: u64 = 50; +pub const STATE_PART_MEMORY_LIMIT: bytesize::ByteSize = bytesize::ByteSize(10 * bytesize::MIB); + +/// Current step of fetching state to fill flat storage. +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq, Eq)] +pub struct FetchingStateStatus { + /// Number of the first state part to be fetched in this step. + pub part_id: u64, + /// Number of parts fetched in one step. + pub num_parts_in_step: u64, + /// Total number of state parts. + pub num_parts: u64, +} /// If a node has flat storage enabled but it didn't have flat storage data on disk, its creation should be initiated. /// Because this is a heavy work requiring ~5h for testnet rpc node and ~10h for testnet archival node, we do it on @@ -463,13 +473,14 @@ pub enum FlatStorageStateStatus { /// final chain head moves after saved chain head. SavingDeltas, /// Flat storage state misses key-value pairs. We need to fetch Trie state to fill flat storage for some final chain - /// head. It is the heaviest step, so it is done in `NUM_FETCHED_STATE_PARTS` / `NUM_PARTS_IN_ONE_STEP` steps. + /// head. It is the heaviest work, so it is done in multiple steps, see comment for `FetchingStateStatus` for more + /// details. /// During each step we spawn background threads to fill some contiguous range of state keys. /// Status contains block hash for which we fetch the shard state and number of current step. Progress of each step /// is saved to disk, so if creation is interrupted during some step, we don't repeat previous steps, starting from /// the saved step again. #[allow(unused)] - FetchingState((CryptoHash, u64)), + FetchingState(FetchingStateStatus), /// Flat storage data exists on disk but its head is too far away from chain final head. We apply deltas from disk /// until the head reaches final head. #[allow(unused)] @@ -482,7 +493,9 @@ pub enum FlatStorageStateStatus { #[cfg(feature = "protocol_feature_flat_state")] pub mod store_helper { - use crate::flat_state::{FlatStorageError, FlatStorageStateStatus, KeyForFlatStateDelta}; + use crate::flat_state::{ + FetchingStateStatus, FlatStorageError, FlatStorageStateStatus, KeyForFlatStateDelta, + }; use crate::{FlatStateDelta, Store, StoreUpdate}; use borsh::BorshSerialize; use near_primitives::hash::CryptoHash; @@ -490,7 +503,9 @@ pub mod store_helper { use near_primitives::types::ShardId; use std::sync::Arc; + pub const FLAT_STATE_HEAD_KEY_PREFIX: &[u8; 4] = b"HEAD"; pub const FETCHING_STATE_STEP_KEY_PREFIX: &[u8; 4] = b"STEP"; + pub const CATCHUP_KEY_PREFIX: &[u8; 7] = b"CATCHUP"; pub fn get_delta( store: &Store, @@ -521,20 +536,26 @@ pub mod store_helper { store_update.delete(crate::DBCol::FlatStateDeltas, &key.try_to_vec().unwrap()); } + fn flat_head_key(shard_id: ShardId) -> Vec { + let mut fetching_state_step_key = FLAT_STATE_HEAD_KEY_PREFIX.to_vec(); + fetching_state_step_key.extend_from_slice(&shard_id.try_to_vec().unwrap()); + fetching_state_step_key + } + pub fn get_flat_head(store: &Store, shard_id: ShardId) -> Option { store - .get_ser(crate::DBCol::FlatStateMisc, &shard_id.try_to_vec().unwrap()) + .get_ser(crate::DBCol::FlatStateMisc, &flat_head_key(shard_id)) .expect("Error reading flat head from storage") } pub fn set_flat_head(store_update: &mut StoreUpdate, shard_id: ShardId, val: &CryptoHash) { store_update - .set_ser(crate::DBCol::FlatStateMisc, &shard_id.try_to_vec().unwrap(), val) + .set_ser(crate::DBCol::FlatStateMisc, &flat_head_key(shard_id), val) .expect("Error writing flat head from storage") } pub fn remove_flat_head(store_update: &mut StoreUpdate, shard_id: ShardId) { - store_update.delete(crate::DBCol::FlatStateMisc, &shard_id.try_to_vec().unwrap()); + store_update.delete(crate::DBCol::FlatStateMisc, &flat_head_key(shard_id)); } pub(crate) fn get_ref(store: &Store, key: &[u8]) -> Result, FlatStorageError> { @@ -562,31 +583,43 @@ pub mod store_helper { } } - fn fetching_state_step_key(shard_id: ShardId) -> Vec { + fn fetching_state_status_key(shard_id: ShardId) -> Vec { let mut fetching_state_step_key = FETCHING_STATE_STEP_KEY_PREFIX.to_vec(); fetching_state_step_key.extend_from_slice(&shard_id.try_to_vec().unwrap()); fetching_state_step_key } - fn get_fetching_state_step(store: &Store, shard_id: ShardId) -> Option { - store.get_ser(crate::DBCol::FlatStateMisc, &fetching_state_step_key(shard_id)).expect( + fn get_fetching_state_status(store: &Store, shard_id: ShardId) -> Option { + store.get_ser(crate::DBCol::FlatStateMisc, &fetching_state_status_key(shard_id)).expect( format!("Error reading fetching step for flat state for shard {shard_id}").as_str(), ) } - pub fn set_fetching_state_step(store_update: &mut StoreUpdate, shard_id: ShardId, value: u64) { + pub fn set_fetching_state_status( + store_update: &mut StoreUpdate, + shard_id: ShardId, + value: FetchingStateStatus, + ) { store_update - .set_ser(crate::DBCol::FlatStateMisc, &fetching_state_step_key(shard_id), &value) + .set_ser(crate::DBCol::FlatStateMisc, &fetching_state_status_key(shard_id), &value) .expect( - format!("Error setting fetching step for shard {shard_id} to {value}").as_str(), + format!("Error setting fetching step for shard {shard_id} to {:?}", value).as_str(), ); } - fn get_catchup_status(store: &Store, shard_id: ShardId) -> bool { - let mut catchup_status_key = FETCHING_STATE_STEP_KEY_PREFIX.to_vec(); + pub fn remove_fetching_state_status(store_update: &mut StoreUpdate, shard_id: ShardId) { + store_update.delete(crate::DBCol::FlatStateMisc, &fetching_state_status_key(shard_id)); + } + + fn catchup_status_key(shard_id: ShardId) -> Vec { + let mut catchup_status_key = CATCHUP_KEY_PREFIX.to_vec(); catchup_status_key.extend_from_slice(&shard_id.try_to_vec().unwrap()); + catchup_status_key + } + + fn get_catchup_status(store: &Store, shard_id: ShardId) -> bool { let status: Option = - store.get_ser(crate::DBCol::FlatStateMisc, &catchup_status_key).expect( + store.get_ser(crate::DBCol::FlatStateMisc, &catchup_status_key(shard_id)).expect( format!("Error reading catchup status for flat state for shard {shard_id}") .as_str(), ); @@ -603,15 +636,25 @@ pub mod store_helper { } } + pub fn start_catchup(store_update: &mut StoreUpdate, shard_id: ShardId) { + store_update + .set_ser(crate::DBCol::FlatStateMisc, &catchup_status_key(shard_id), &true) + .expect(format!("Error setting catchup status for shard {shard_id}").as_str()); + } + + pub fn finish_catchup(store_update: &mut StoreUpdate, shard_id: ShardId) { + store_update.delete(crate::DBCol::FlatStateMisc, &catchup_status_key(shard_id)); + } + pub fn get_flat_storage_state_status( store: &Store, shard_id: ShardId, ) -> FlatStorageStateStatus { match get_flat_head(store, shard_id) { None => FlatStorageStateStatus::SavingDeltas, - Some(block_hash) => { - if let Some(fetching_state_step) = get_fetching_state_step(store, shard_id) { - FlatStorageStateStatus::FetchingState((block_hash, fetching_state_step)) + Some(_) => { + if let Some(fetching_state_status) = get_fetching_state_status(store, shard_id) { + FlatStorageStateStatus::FetchingState(fetching_state_status) } else if get_catchup_status(store, shard_id) { FlatStorageStateStatus::CatchingUp } else { diff --git a/core/store/src/lib.rs b/core/store/src/lib.rs index 29893ddc712..7b93e22c0ae 100644 --- a/core/store/src/lib.rs +++ b/core/store/src/lib.rs @@ -28,12 +28,13 @@ use crate::db::{ refcount, DBIterator, DBOp, DBSlice, DBTransaction, Database, StoreStatistics, GENESIS_JSON_HASH_KEY, GENESIS_STATE_ROOTS_KEY, }; -pub use crate::trie::iterator::TrieIterator; +pub use crate::trie::iterator::{TrieIterator, TrieTraversalItem}; pub use crate::trie::update::{TrieUpdate, TrieUpdateIterator, TrieUpdateValuePtr}; pub use crate::trie::{ estimator, split_state, ApplyStatePartResult, KeyForStateChanges, KeyLookupMode, NibbleSlice, PartialStorage, PrefetchApi, RawTrieNode, RawTrieNodeWithSize, ShardTries, Trie, TrieAccess, - TrieCache, TrieCachingStorage, TrieChanges, TrieConfig, TrieStorage, WrappedTrieChanges, + TrieCache, TrieCachingStorage, TrieChanges, TrieConfig, TrieDBStorage, TrieStorage, + WrappedTrieChanges, }; pub use flat_state::FlatStateDelta; diff --git a/core/store/src/trie/iterator.rs b/core/store/src/trie/iterator.rs index e43629cec15..837b66db33d 100644 --- a/core/store/src/trie/iterator.rs +++ b/core/store/src/trie/iterator.rs @@ -296,7 +296,7 @@ impl<'a> TrieIterator<'a> { /// Visits all nodes belonging to the interval [path_begin, path_end) in depth-first search /// order and return TrieTraversalItem for each visited node. /// Used to generate and apply state parts for state sync. - pub(crate) fn visit_nodes_interval( + pub fn visit_nodes_interval( &mut self, path_begin: &[u8], path_end: &[u8], diff --git a/core/store/src/trie/mod.rs b/core/store/src/trie/mod.rs index 2fa30b7ec9f..b3b97a18660 100644 --- a/core/store/src/trie/mod.rs +++ b/core/store/src/trie/mod.rs @@ -23,7 +23,7 @@ use crate::trie::iterator::TrieIterator; pub use crate::trie::nibble_slice::NibbleSlice; pub use crate::trie::prefetching_trie_storage::PrefetchApi; pub use crate::trie::shard_tries::{KeyForStateChanges, ShardTries, WrappedTrieChanges}; -pub use crate::trie::trie_storage::{TrieCache, TrieCachingStorage, TrieStorage}; +pub use crate::trie::trie_storage::{TrieCache, TrieCachingStorage, TrieDBStorage, TrieStorage}; use crate::trie::trie_storage::{TrieMemoryPartialStorage, TrieRecordingStorage}; use crate::StorageError; pub use near_primitives::types::TrieNodesCount; diff --git a/core/store/src/trie/state_parts.rs b/core/store/src/trie/state_parts.rs index 751f3148117..a4a263ebaf3 100644 --- a/core/store/src/trie/state_parts.rs +++ b/core/store/src/trie/state_parts.rs @@ -64,7 +64,7 @@ impl Trie { /// Part part_id has nodes with paths `[path(part_id), path(part_id + 1))` /// path is returned as nibbles, last path is `vec![16]`, previous paths end /// in nodes - pub(crate) fn find_path_for_part_boundary( + pub fn find_path_for_part_boundary( &self, part_id: u64, num_parts: u64, diff --git a/integration-tests/src/tests/client/flat_storage.rs b/integration-tests/src/tests/client/flat_storage.rs index 76076a7f673..a60acc74841 100644 --- a/integration-tests/src/tests/client/flat_storage.rs +++ b/integration-tests/src/tests/client/flat_storage.rs @@ -3,11 +3,16 @@ use near_chain::{ChainGenesis, RuntimeAdapter}; use near_chain_configs::Genesis; use near_client::test_utils::TestEnv; use near_o11y::testonly::init_test_logger; -use near_store::flat_state::{store_helper, FlatStorageStateStatus}; +use near_primitives_core::types::BlockHeight; +use near_store::flat_state::{ + store_helper, FetchingStateStatus, FlatStorageStateStatus, NUM_PARTS_IN_ONE_STEP, +}; use near_store::test_utils::create_test_store; use nearcore::config::GenesisExt; use std::path::Path; use std::sync::Arc; +use std::thread; +use std::time::Duration; /// Check correctness of flat storage creation. #[test] @@ -78,6 +83,7 @@ fn test_flat_storage_creation() { for i in 4..6 { env.produce_block(0, i); } + assert!(env.clients[0].runtime_adapter.get_flat_storage_state_for_shard(0).is_none()); if !cfg!(feature = "protocol_feature_flat_state") { assert_eq!( @@ -104,10 +110,54 @@ fn test_flat_storage_creation() { // We started the node from height 3, and now final head should move to height 4. env.produce_block(0, 6); let final_block_hash = env.clients[0].chain.get_block_hash_by_height(4).unwrap(); + assert_eq!(store_helper::get_flat_head(&store, 0), Some(final_block_hash)); assert_eq!( store_helper::get_flat_storage_state_status(&store, 0), - FlatStorageStateStatus::FetchingState((final_block_hash, 0)) + FlatStorageStateStatus::FetchingState(FetchingStateStatus { + part_id: 0, + num_parts_in_step: NUM_PARTS_IN_ONE_STEP, + num_parts: 1, + }) ); - // TODO: support next statuses once their logic is implemented. + // Run chain for a couple of blocks and check that statuses switch to `CatchingUp` and then to `Ready`. + // State is being fetched in rayon threads, but we expect it to finish in <30s because state is small and there is + // only one state part. + const BLOCKS_TIMEOUT: BlockHeight = 30; + let start_height = 8; + let mut next_height = start_height; + let mut was_catching_up = false; + while next_height < start_height + BLOCKS_TIMEOUT { + env.produce_block(0, next_height); + next_height += 1; + match store_helper::get_flat_storage_state_status(&store, 0) { + FlatStorageStateStatus::FetchingState(..) => { + assert!(!was_catching_up, "Flat storage state status inconsistency: it was catching up before fetching state"); + } + FlatStorageStateStatus::CatchingUp => { + was_catching_up = true; + } + FlatStorageStateStatus::Ready => { + assert!( + was_catching_up, + "Flat storage state is ready but there was no flat storage catchup observed" + ); + break; + } + status @ _ => { + panic!( + "Unexpected flat storage state status for height {next_height}: {:?}", + status + ); + } + } + thread::sleep(Duration::from_secs(1)); + } + if next_height == start_height + BLOCKS_TIMEOUT { + let status = store_helper::get_flat_storage_state_status(&store, 0); + panic!("Apparently, node didn't fetch the whole state in {BLOCKS_TIMEOUT} blocks. Current status: {:?}", status); + } + + // Finally, check that flat storage state was created. + assert!(env.clients[0].runtime_adapter.get_flat_storage_state_for_shard(0).is_some()); } diff --git a/nearcore/src/migrations.rs b/nearcore/src/migrations.rs index b13d4e55145..db0d3f8d824 100644 --- a/nearcore/src/migrations.rs +++ b/nearcore/src/migrations.rs @@ -159,7 +159,9 @@ impl<'a> near_store::StoreMigrator for Migrator<'a> { } #[cfg(feature = "protocol_feature_flat_state")] 34 => { - panic!("Trying to use a DB that has no been migrated to flat state. This binary does not support migrating.") + tracing::info!(target: "migrations", "Migrating DB version from 34 to 35. Flat storage data will be created on disk."); + tracing::info!(target: "migrations", "It will happen in parallel with regular block processing. ETA is 5h for RPC node and 10h for archival node."); + Ok(()) } DB_VERSION.. => unreachable!(), } From 824880f5c4d651b6aac928d32f0e72918123aea7 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Tue, 22 Nov 2022 09:58:54 +0100 Subject: [PATCH 020/188] revamped TIER1 discovery protocol (#8085) * added some extra fields to AccountData, renamed a bunch of them for better readability. This PR is not backward compatible in a sense that the old binary won't accept new AccountData and vice versa (protobuf compatibility is preserved, however the custom validation logic will treat different fields as required). That's ok as the broadcasted data is not interpreted yet. * changed identifier of AccountData from (EpochId,AccountId) to AccountKey, which simplifies a lot of stuff and generally makes more sense * changed version ID of AccountData from an UTC timestamp to an integer: this allows to avoid bad impact of a wall clock time shift, which can be especially dangerous since we do not version AccountData by EpochId any more. The creation timestamp is still present in AccountData to be able to debug data freshness. We can additionally implement an expiration policy in the future based on the timestamp. * rearranged the code related to broadcasting AccountData to align with the TIER1-related code which comes next after this PR --- chain/client/src/client.rs | 38 +++--- chain/network/src/accounts_data/mod.rs | 82 ++++++------- chain/network/src/accounts_data/tests.rs | 98 ++++++---------- chain/network/src/config.rs | 99 +++++++++++----- chain/network/src/config_json.rs | 5 + chain/network/src/network_protocol/mod.rs | 17 +-- .../src/network_protocol/network.proto | 37 ++++-- .../proto_conv/account_key.rs | 24 ++-- .../src/network_protocol/proto_conv/crypto.rs | 22 +++- .../network_protocol/proto_conv/handshake.rs | 4 +- .../src/network_protocol/proto_conv/net.rs | 2 +- .../network/src/network_protocol/testonly.rs | 53 ++++----- chain/network/src/network_protocol/tests.rs | 8 +- chain/network/src/peer/peer_actor.rs | 3 - .../src/peer_manager/connection/mod.rs | 4 +- .../src/peer_manager/network_state/mod.rs | 2 + .../src/peer_manager/network_state/tier1.rs | 80 +++++++++++++ .../src/peer_manager/peer_manager_actor.rs | 108 +++++++----------- chain/network/src/peer_manager/testonly.rs | 55 +++++---- .../src/peer_manager/tests/accounts_data.rs | 87 +++++--------- chain/network/src/types.rs | 6 +- nearcore/src/config.rs | 2 - 22 files changed, 450 insertions(+), 386 deletions(-) create mode 100644 chain/network/src/peer_manager/network_state/tier1.rs diff --git a/chain/client/src/client.rs b/chain/client/src/client.rs index 80f61baeb37..8a532724fe5 100644 --- a/chain/client/src/client.rs +++ b/chain/client/src/client.rs @@ -1707,7 +1707,7 @@ impl Client { } } - /// Collects block approvals. Returns false if block approval is invalid. + /// Collects block approvals. /// /// We send the approval to doomslug given the epoch of the current tip iff: /// 1. We are the block producer for the target height in the tip's epoch; @@ -2255,30 +2255,32 @@ impl Client { // require some tuning in the future. In particular, if we decide that connecting to // block & chunk producers of the next expoch is too expensive, we can postpone it // till almost the end of this epoch. - let mut accounts = HashMap::new(); + let mut account_keys = AccountKeys::new(); for epoch_id in [&tip.epoch_id, &tip.next_epoch_id] { // We assume here that calls to get_epoch_chunk_producers and get_epoch_block_producers_ordered // are cheaper than block processing (and that they will work with both this and // the next epoch). The caching on top of that (in tier1_accounts_cache field) is just // a defence in depth, based on the previous experience with expensive // RuntimeAdapter::get_validators_info call. - accounts.extend( - self.runtime_adapter.get_epoch_chunk_producers(epoch_id)?.iter().map(|it| { - ((epoch_id.clone(), it.account_id().clone()), it.public_key().clone()) - }), - ); - accounts.extend( - self.runtime_adapter - .get_epoch_block_producers_ordered(epoch_id, &tip.last_block_hash)? - .iter() - .map(|(it, _)| { - ((epoch_id.clone(), it.account_id().clone()), it.public_key().clone()) - }), - ); + for cp in self.runtime_adapter.get_epoch_chunk_producers(epoch_id)? { + account_keys + .entry(cp.account_id().clone()) + .or_default() + .insert(cp.public_key().clone()); + } + for (bp, _) in self + .runtime_adapter + .get_epoch_block_producers_ordered(epoch_id, &tip.last_block_hash)? + { + account_keys + .entry(bp.account_id().clone()) + .or_default() + .insert(bp.public_key().clone()); + } } - let accounts = Arc::new(accounts); - self.tier1_accounts_cache = Some((tip.epoch_id.clone(), accounts.clone())); - Ok(accounts) + let account_keys = Arc::new(account_keys); + self.tier1_accounts_cache = Some((tip.epoch_id.clone(), account_keys.clone())); + Ok(account_keys) } /// send_network_chain_info sends ChainInfo to PeerManagerActor. diff --git a/chain/network/src/accounts_data/mod.rs b/chain/network/src/accounts_data/mod.rs index 2c2d5587213..af5c87738e2 100644 --- a/chain/network/src/accounts_data/mod.rs +++ b/chain/network/src/accounts_data/mod.rs @@ -13,7 +13,7 @@ //! //! Strategy: //! - handling of SyncAccountsData should be throttled by PeerActor/PeerManagerActor. -//! - synchronously select interesting AccountData (i.e. those with never timestamp than any +//! - synchronously select interesting AccountData (i.e. those with newer version than any //! previously seen for the given (account_id,epoch_id) pair. //! - asynchronously verify signatures, until an invalid signature is encountered. //! - if any signature is invalid, drop validation of the remaining signature and ban the peer @@ -29,7 +29,7 @@ use crate::concurrency::arc_mutex::ArcMutex; use crate::network_protocol; use crate::network_protocol::SignedAccountData; use crate::types::AccountKeys; -use near_primitives::types::{AccountId, EpochId}; +use near_crypto::PublicKey; use rayon::iter::ParallelBridge; use std::collections::HashMap; use std::sync::Arc; @@ -49,29 +49,35 @@ pub(crate) enum Error { #[derive(Clone)] pub struct CacheSnapshot { - pub keys: Arc, + /// Map from account ID to account key. + /// Used only for selecting target when routing a message to a TIER1 peer. + /// TODO(gprusak): In fact, since the account key assigned to a given account ID can change + /// between epochs, Client should rather send messages to node with a specific account key, + /// rather than with a specific account ID. + pub keys_by_id: Arc, + /// Set of account keys allowed on TIER1 network. + pub keys: im::HashSet, /// Current state of knowledge about an account. - /// key is the public key of the account in the given epoch. - /// It will be used to verify new incoming versions of SignedAccountData - /// for this account. - pub data: im::HashMap<(EpochId, AccountId), Arc>, + /// `data.keys()` is a subset of `keys` at all times, + /// as cache is collecting data only about the accounts from `keys`, + /// and data about the particular account might be not known at the given moment. + pub data: im::HashMap>, } impl CacheSnapshot { fn is_new(&self, d: &SignedAccountData) -> bool { - let id = (d.epoch_id.clone(), d.account_id.clone()); - self.keys.contains_key(&id) - && match self.data.get(&id) { - Some(old) if old.timestamp >= d.timestamp => false, + self.keys.contains(&d.account_key) + && match self.data.get(&d.account_key) { + Some(old) if old.version >= d.version => false, _ => true, } } + fn try_insert(&mut self, d: Arc) -> Option> { if !self.is_new(&d) { return None; } - let id = (d.epoch_id.clone(), d.account_id.clone()); - self.data.insert(id, d.clone()); + self.data.insert(d.account_key.clone(), d.clone()); Some(d) } } @@ -81,7 +87,8 @@ pub(crate) struct Cache(ArcMutex); impl Cache { pub fn new() -> Self { Self(ArcMutex::new(CacheSnapshot { - keys: Arc::new(AccountKeys::default()), + keys_by_id: Arc::new(AccountKeys::default()), + keys: im::HashSet::new(), data: im::HashMap::new(), })) } @@ -89,19 +96,16 @@ impl Cache { /// Updates the set of important accounts and their public keys. /// The AccountData which is no longer important is dropped. /// Returns true iff the set of accounts actually changed. - pub fn set_keys(&self, keys: Arc) -> bool { + pub fn set_keys(&self, keys_by_id: Arc) -> bool { self.0.update(|inner| { // Skip further processing if the key set didn't change. // NOTE: if T implements Eq, then Arc short circuits equality for x == x. - if keys == inner.keys { + if keys_by_id == inner.keys_by_id { return false; } - for (k, v) in std::mem::take(&mut inner.data) { - if keys.contains_key(&k) { - inner.data.insert(k, v); - } - } - inner.keys = keys; + inner.keys_by_id = keys_by_id; + inner.keys = inner.keys_by_id.values().flatten().cloned().collect(); + inner.data.retain(|k, _| inner.keys.contains(k)); true }) } @@ -110,14 +114,14 @@ impl Cache { /// Returns the verified new data and an optional error. /// Note that even if error has been returned the partially validated output is returned /// anyway. - fn verify( + async fn verify( &self, data: Vec>, ) -> (Vec>, Option) { // Filter out non-interesting data, so that we never check signatures for valid non-interesting data. // Bad peers may force us to check signatures for fake data anyway, but we will ban them after first invalid signature. // It locks epochs for reading for a short period. - let mut data_and_keys = HashMap::new(); + let mut new_data = HashMap::new(); let inner = self.0.load(); for d in data { // There is a limit on the amount of RAM occupied by per-account datasets. @@ -125,34 +129,30 @@ impl Cache { if d.payload().len() > network_protocol::MAX_ACCOUNT_DATA_SIZE_BYTES { return (vec![], Some(Error::DataTooLarge)); } - let id = (d.epoch_id.clone(), d.account_id.clone()); // We want the communication needed for broadcasting per-account data to be minimal. // Therefore broadcasting multiple datasets per account is considered malicious // behavior, since all but one are obviously outdated. - if data_and_keys.contains_key(&id) { + if new_data.contains_key(&d.account_key) { return (vec![], Some(Error::SingleAccountMultipleData)); } // It is fine to broadcast data we already know about. - if !inner.is_new(&d) { - continue; - } // It is fine to broadcast account data that we don't care about. - let key = match inner.keys.get(&id) { - Some(key) => key.clone(), - None => continue, - }; - data_and_keys.insert(id, (d, key)); + if inner.is_new(&d) { + new_data.insert(d.account_key.clone(), d); + } } // Verify the signatures in parallel. // Verification will stop at the first encountered error. - let (data, ok) = - concurrency::rayon::try_map(data_and_keys.into_values().par_bridge(), |(d, key)| { - if d.payload().verify(&key).is_ok() { - return Some(d); + let (data, ok) = concurrency::rayon::run(move || { + concurrency::rayon::try_map(new_data.into_values().par_bridge(), |d| { + match d.payload().verify(&d.account_key) { + Ok(()) => Some(d), + Err(()) => None, } - return None; - }); + }) + }) + .await; if !ok { return (data, Some(Error::InvalidSignature)); } @@ -168,7 +168,7 @@ impl Cache { ) -> (Vec>, Option) { let this = self.clone(); // Execute verification on the rayon threadpool. - let (data, err) = concurrency::rayon::run(move || this.verify(data)).await; + let (data, err) = this.verify(data).await; // Insert the successfully verified data, even if an error has been encountered. let inserted = self.0.update(|inner| data.into_iter().filter_map(|d| inner.try_insert(d)).collect()); diff --git a/chain/network/src/accounts_data/tests.rs b/chain/network/src/accounts_data/tests.rs index 32d38a41e3c..389c6787d42 100644 --- a/chain/network/src/accounts_data/tests.rs +++ b/chain/network/src/accounts_data/tests.rs @@ -3,28 +3,20 @@ use crate::network_protocol::testonly as data; use crate::network_protocol::SignedAccountData; use crate::testonly::{assert_is_superset, make_rng, AsSet as _, Rng}; use crate::time; -use crate::types::AccountKeys; -use near_primitives::types::EpochId; -use near_primitives::validator_signer::{InMemoryValidatorSigner, ValidatorSigner as _}; +use near_primitives::validator_signer::InMemoryValidatorSigner; use pretty_assertions::assert_eq; use std::sync::Arc; -struct Signer { - epoch_id: EpochId, - signer: InMemoryValidatorSigner, -} - -impl Signer { - fn make_account_data(&self, rng: &mut Rng, timestamp: time::Utc) -> SignedAccountData { - data::make_account_data( - rng, - timestamp, - self.epoch_id.clone(), - self.signer.validator_id().clone(), - ) - .sign(&self.signer) +fn make_account_data( + rng: &mut Rng, + clock: &time::Clock, + version: u64, + signer: &InMemoryValidatorSigner, +) -> SignedAccountData { + let peer_id = data::make_peer_id(rng); + data::make_account_data(rng, version, clock.now_utc(), signer.public_key(), peer_id) + .sign(signer) .unwrap() - } } fn unwrap<'a, T: std::hash::Hash + std::cmp::Eq, E: std::fmt::Debug>( @@ -36,27 +28,8 @@ fn unwrap<'a, T: std::hash::Hash + std::cmp::Eq, E: std::fmt::Debug>( &v.0 } -fn make_signers(rng: &mut Rng, n: usize) -> Vec { - (0..n) - .map(|_| Signer { - epoch_id: data::make_epoch_id(rng), - signer: data::make_validator_signer(rng), - }) - .collect() -} - -fn make_account_keys(signers: &[Signer]) -> Arc { - Arc::new( - signers - .iter() - .map(|s| { - ( - (s.epoch_id.clone(), s.signer.validator_id().clone()), - s.signer.public_key().clone(), - ) - }) - .collect(), - ) +fn make_signers(rng: &mut Rng, n: usize) -> Vec { + (0..n).map(|_| data::make_validator_signer(rng)).collect() } #[tokio::test] @@ -64,11 +37,10 @@ async fn happy_path() { let mut rng = make_rng(2947294234); let rng = &mut rng; let clock = time::FakeClock::default(); - let now = clock.now_utc(); let signers: Vec<_> = make_signers(rng, 7); - let e0 = make_account_keys(&signers[0..5]); - let e1 = make_account_keys(&signers[2..7]); + let e0 = Arc::new(data::make_account_keys(&signers[0..5])); + let e1 = Arc::new(data::make_account_keys(&signers[2..7])); let cache = Arc::new(Cache::new()); assert_eq!(cache.load().data.values().count(), 0); // initially empty @@ -76,17 +48,17 @@ async fn happy_path() { assert_eq!(cache.load().data.values().count(), 0); // empty after initial set_keys. // initial insert - let a0 = Arc::new(signers[0].make_account_data(rng, now)); - let a1 = Arc::new(signers[1].make_account_data(rng, now)); + let a0 = Arc::new(make_account_data(rng, &clock.clock(), 1, &signers[0])); + let a1 = Arc::new(make_account_data(rng, &clock.clock(), 1, &signers[1])); let res = cache.clone().insert(vec![a0.clone(), a1.clone()]).await; assert_eq!([&a0, &a1].as_set(), unwrap(&res).as_set()); assert_eq!([&a0, &a1].as_set(), cache.load().data.values().collect()); // entries of various types - let a0new = Arc::new(signers[0].make_account_data(rng, now + time::Duration::seconds(1))); - let a1old = Arc::new(signers[1].make_account_data(rng, now - time::Duration::seconds(1))); - let a2 = Arc::new(signers[2].make_account_data(rng, now)); - let a5 = Arc::new(signers[5].make_account_data(rng, now)); + let a0new = Arc::new(make_account_data(rng, &clock.clock(), 2, &signers[0])); + let a1old = Arc::new(make_account_data(rng, &clock.clock(), 0, &signers[1])); + let a2 = Arc::new(make_account_data(rng, &clock.clock(), 1, &signers[2])); + let a5 = Arc::new(make_account_data(rng, &clock.clock(), 1, &signers[5])); let res = cache .clone() .insert(vec![ @@ -123,16 +95,16 @@ async fn data_too_large() { let mut rng = make_rng(2947294234); let rng = &mut rng; let clock = time::FakeClock::default(); - let now = clock.now_utc(); let signers = make_signers(rng, 3); - let e = make_account_keys(&signers); + let e = Arc::new(data::make_account_keys(&signers)); let cache = Arc::new(Cache::new()); cache.set_keys(e); - let a0 = Arc::new(signers[0].make_account_data(rng, now)); - let a1 = Arc::new(signers[1].make_account_data(rng, now)); - let mut a2_too_large: SignedAccountData = signers[2].make_account_data(rng, now); + let a0 = Arc::new(make_account_data(rng, &clock.clock(), 1, &signers[0])); + let a1 = Arc::new(make_account_data(rng, &clock.clock(), 1, &signers[1])); + let mut a2_too_large: SignedAccountData = + make_account_data(rng, &clock.clock(), 1, &signers[2]); *a2_too_large.payload_mut() = (0..crate::network_protocol::MAX_ACCOUNT_DATA_SIZE_BYTES + 1).map(|_| 17).collect(); let a2_too_large = Arc::new(a2_too_large); @@ -158,16 +130,15 @@ async fn invalid_signature() { let mut rng = make_rng(2947294234); let rng = &mut rng; let clock = time::FakeClock::default(); - let now = clock.now_utc(); let signers = make_signers(rng, 3); - let e = make_account_keys(&signers); + let e = Arc::new(data::make_account_keys(&signers)); let cache = Arc::new(Cache::new()); cache.set_keys(e); - let a0 = Arc::new(signers[0].make_account_data(rng, now)); - let mut a1 = signers[1].make_account_data(rng, now); - let mut a2_invalid_sig = signers[2].make_account_data(rng, now); + let a0 = Arc::new(make_account_data(rng, &clock.clock(), 1, &signers[0])); + let mut a1 = make_account_data(rng, &clock.clock(), 1, &signers[1]); + let mut a2_invalid_sig = make_account_data(rng, &clock.clock(), 1, &signers[2]); *a2_invalid_sig.signature_mut() = a1.signature_mut().clone(); let a1 = Arc::new(a1); let a2_invalid_sig = Arc::new(a2_invalid_sig); @@ -193,17 +164,16 @@ async fn single_account_multiple_data() { let mut rng = make_rng(2947294234); let rng = &mut rng; let clock = time::FakeClock::default(); - let now = clock.now_utc(); let signers = make_signers(rng, 3); - let e = make_account_keys(&signers); + let e = Arc::new(data::make_account_keys(&signers)); let cache = Arc::new(Cache::new()); cache.set_keys(e); - let a0 = Arc::new(signers[0].make_account_data(rng, now)); - let a1 = Arc::new(signers[1].make_account_data(rng, now)); - let a2old = Arc::new(signers[2].make_account_data(rng, now)); - let a2new = Arc::new(signers[2].make_account_data(rng, now + time::Duration::seconds(1))); + let a0 = Arc::new(make_account_data(rng, &clock.clock(), 1, &signers[0])); + let a1 = Arc::new(make_account_data(rng, &clock.clock(), 1, &signers[1])); + let a2old = Arc::new(make_account_data(rng, &clock.clock(), 1, &signers[2])); + let a2new = Arc::new(make_account_data(rng, &clock.clock(), 2, &signers[2])); // 2 entries for the same (epoch_id,account_id) => SingleAccountMultipleData let res = diff --git a/chain/network/src/config.rs b/chain/network/src/config.rs index e7bbe5af08c..570d6352453 100644 --- a/chain/network/src/config.rs +++ b/chain/network/src/config.rs @@ -24,26 +24,39 @@ pub const MAX_ROUTES_TO_STORE: usize = 5; /// Maximum number of PeerAddts in the ValidatorConfig::endpoints field. pub const MAX_PEER_ADDRS: usize = 10; -/// ValidatorEndpoints are the endpoints that peers should connect to, to send messages to this -/// validator. Validator will sign the endpoints and broadcast them to the network. -/// For a static setup (a static IP, or a list of relay nodes with static IPs) use PublicAddrs. -/// For a dynamic setup (with a single dynamic/ephemeral IP), use TrustedStunServers. +/// Address of the format ":" of STUN servers. +// TODO(gprusak): turn into a proper struct implementing Display and FromStr. +pub type StunServerAddr = String; + +/// ValidatorProxies are nodes with public IP (aka proxies) that this validator trusts to be honest +/// and willing to forward traffic to this validator. Whenever this node is a TIER1 validator +/// (i.e. whenever it is a block producer/chunk producer/approver for the given epoch), +/// it will connect to all the proxies in this config and advertise a signed list of proxies that +/// it has established a connection to. +/// +/// Once other TIER1 nodes learn the list of proxies, they will maintain a connection to a random +/// proxy on this list. This way a message from any TIER1 node to this node will require at most 2 +/// hops. +/// +/// neard supports 2 modes for configuring proxy addresses: +/// * [recommended] `Static` list of proxies (public SocketAddr + PeerId), supports up to 10 proxies. +/// It is a totally valid setup for a TIER1 validator to be its own (perahaps only) proxy: +/// to achieve that, add an entry with the public address of this node to the Static list. +/// * [discouraged] `Dynamic` proxy - in case you want this validator to be its own and only proxy, +/// instead of adding the public address explicitly to the `Static` list, you can specify a STUN +/// server address (or a couple of them) which will be used to dynamically resolve the public IP +/// of this validator. Note that in this case the validator trusts the STUN servers to correctly +/// resolve the public IP. #[derive(Clone)] -pub enum ValidatorEndpoints { - /// Single public address of this validator, or a list of public addresses of trusted nodes - /// willing to route messages to this validator. Validator will connect to the listed relay - /// nodes on startup. - PublicAddrs(Vec), - /// Addresses of the format ":" of STUN servers. - /// The IP of the validator will be determined dynamically by querying all the STUN servers on - /// the list. - TrustedStunServers(Vec), +pub enum ValidatorProxies { + Static(Vec), + Dynamic(Vec), } #[derive(Clone)] pub struct ValidatorConfig { pub signer: Arc, - pub endpoints: ValidatorEndpoints, + pub proxies: ValidatorProxies, } impl ValidatorConfig { @@ -53,8 +66,13 @@ impl ValidatorConfig { } #[derive(Clone)] -pub struct Features { - pub enable_tier1: bool, +pub struct Features {} + +#[derive(Clone)] +pub struct Tier1 { + /// Interval between broacasts of the list of validator's proxies. + /// Before the broadcast, validator tries to establish all the missing connections to proxies. + pub advertise_proxies_interval: time::Duration, } /// Validated configuration for the peer-to-peer manager. @@ -117,8 +135,8 @@ pub struct NetworkConfig { pub accounts_data_broadcast_rate_limit: demux::RateLimit, /// Maximal rate at which RoutingTableUpdate can be sent out. pub routing_table_update_rate_limit: demux::RateLimit, - /// features - pub features: Features, + /// Config of the TIER1 network. + pub tier1: Option, // Whether to ignore tombstones some time after startup. // @@ -139,7 +157,6 @@ impl NetworkConfig { node_key: SecretKey, validator_signer: Option>, archive: bool, - features: Features, ) -> anyhow::Result { if cfg.public_addrs.len() > MAX_PEER_ADDRS { anyhow::bail!( @@ -150,14 +167,34 @@ impl NetworkConfig { if cfg.public_addrs.len() > 0 && cfg.trusted_stun_servers.len() > 0 { anyhow::bail!("you cannot specify both public_addrs and trusted_stun_servers"); } + for proxy in &cfg.public_addrs { + let ip = proxy.addr.ip(); + if cfg.allow_private_ip_in_public_addrs { + if ip.is_unspecified() { + anyhow::bail!("public_addrs: {ip} is not a valid IP. If you wanted to specify a loopback IP, use 127.0.0.1 instead."); + } + } else { + // TODO(gprusak): use !ip.is_global() instead, once it is stable. + if ip.is_loopback() + || ip.is_unspecified() + || match ip { + std::net::IpAddr::V4(ip) => ip.is_private(), + // TODO(gprusak): use ip.is_unique_local() once stable. + std::net::IpAddr::V6(_) => false, + } + { + anyhow::bail!("public_addrs: {ip} is not a public IP."); + } + } + } let this = Self { node_key, validator: validator_signer.map(|signer| ValidatorConfig { signer, - endpoints: if cfg.public_addrs.len() > 0 { - ValidatorEndpoints::PublicAddrs(cfg.public_addrs) + proxies: if cfg.public_addrs.len() > 0 { + ValidatorProxies::Static(cfg.public_addrs) } else { - ValidatorEndpoints::TrustedStunServers(cfg.trusted_stun_servers) + ValidatorProxies::Dynamic(cfg.trusted_stun_servers) }, }), node_addr: match cfg.addr.as_str() { @@ -219,7 +256,7 @@ impl NetworkConfig { archive, accounts_data_broadcast_rate_limit: demux::RateLimit { qps: 0.1, burst: 1 }, routing_table_update_rate_limit: demux::RateLimit { qps: 0.5, burst: 1 }, - features, + tier1: Some(Tier1 { advertise_proxies_interval: time::Duration::minutes(15) }), inbound_disabled: cfg.experimental.inbound_disabled, skip_tombstones: if cfg.experimental.skip_sending_tombstones_seconds > 0 { Some(time::Duration::seconds(cfg.experimental.skip_sending_tombstones_seconds)) @@ -246,7 +283,7 @@ impl NetworkConfig { KeyType::ED25519, seed, )), - endpoints: ValidatorEndpoints::PublicAddrs(vec![PeerAddr { + proxies: ValidatorProxies::Static(vec![PeerAddr { addr: node_addr, peer_id: PeerId::new(node_key.public_key()), }]), @@ -284,7 +321,11 @@ impl NetworkConfig { archive: false, accounts_data_broadcast_rate_limit: demux::RateLimit { qps: 100., burst: 1000000 }, routing_table_update_rate_limit: demux::RateLimit { qps: 100., burst: 1000000 }, - features: Features { enable_tier1: true }, + tier1: Some(Tier1 { + // Interval is very large, so that it doesn't happen spontaneously in tests. + // It should rather be triggered manually in tests. + advertise_proxies_interval: time::Duration::hours(1000), + }), skip_tombstones: None, event_sink: Sink::null(), } @@ -362,7 +403,6 @@ mod test { use crate::network_protocol::AccountData; use crate::testonly::make_rng; use crate::time; - use near_primitives::validator_signer::ValidatorSigner; #[test] fn test_network_config() { @@ -395,15 +435,16 @@ mod test { let signer = data::make_validator_signer(&mut rng); let ad = AccountData { - peers: (0..config::MAX_PEER_ADDRS) + proxies: (0..config::MAX_PEER_ADDRS) .map(|_| { // Using IPv6 gives maximal size of the resulting config. let ip = data::make_ipv6(&mut rng); data::make_peer_addr(&mut rng, ip) }) .collect(), - account_id: signer.validator_id().clone(), - epoch_id: data::make_epoch_id(&mut rng), + account_key: signer.public_key(), + peer_id: data::make_peer_id(&mut rng), + version: 0, timestamp: clock.now_utc(), }; let sad = ad.sign(&signer).unwrap(); diff --git a/chain/network/src/config_json.rs b/chain/network/src/config_json.rs index 1155e3ce450..a86c446a576 100644 --- a/chain/network/src/config_json.rs +++ b/chain/network/src/config_json.rs @@ -158,6 +158,10 @@ pub struct Config { /// This setup is not reliable in presence of byzantine peers. #[serde(default)] pub public_addrs: Vec, + /// For local tests only (localnet). Allows specifying IPs from private range + /// (which are not visible from the public internet) in public_addrs field. + #[serde(default)] + pub allow_private_ip_in_public_addrs: bool, /// List of endpoints of trusted [STUN servers](https://datatracker.ietf.org/doc/html/rfc8489). /// /// Used only if this node is a validator and public_ips is empty (see @@ -222,6 +226,7 @@ impl Default for Config { monitor_peers_max_period: default_monitor_peers_max_period(), peer_expiration_duration: default_peer_expiration_duration(), public_addrs: vec![], + allow_private_ip_in_public_addrs: false, trusted_stun_servers: vec![], experimental: Default::default(), } diff --git a/chain/network/src/network_protocol/mod.rs b/chain/network/src/network_protocol/mod.rs index 9b2f5442919..ad1455863ae 100644 --- a/chain/network/src/network_protocol/mod.rs +++ b/chain/network/src/network_protocol/mod.rs @@ -37,7 +37,7 @@ use near_primitives::sharding::{ }; use near_primitives::syncing::{ShardStateSyncResponse, ShardStateSyncResponseV1}; use near_primitives::transaction::SignedTransaction; -use near_primitives::types::{AccountId, EpochId}; +use near_primitives::types::AccountId; use near_primitives::types::{BlockHeight, ShardId}; use near_primitives::validator_signer::ValidatorSigner; use near_primitives::views::FinalExecutionOutcomeView; @@ -92,9 +92,10 @@ impl std::str::FromStr for PeerAddr { #[derive(PartialEq, Eq, Debug, Hash)] pub struct AccountData { - pub peers: Vec, - pub account_id: AccountId, - pub epoch_id: EpochId, + pub peer_id: PeerId, + pub proxies: Vec, + pub account_key: PublicKey, + pub version: u64, pub timestamp: time::Utc, } @@ -115,9 +116,9 @@ impl AccountData { /// consistute a cleaner never-panicking interface. pub fn sign(self, signer: &dyn ValidatorSigner) -> anyhow::Result { assert_eq!( - &self.account_id, - signer.validator_id(), - "AccountData.account_id doesn't match the signer's account_id" + self.account_key, + signer.public_key(), + "AccountData.account_key doesn't match the signer's account_key" ); let payload = proto::AccountKeyPayload::from(&self).write_to_bytes().unwrap(); if payload.len() > MAX_ACCOUNT_DATA_SIZE_BYTES { @@ -135,7 +136,7 @@ impl AccountData { } } -#[derive(PartialEq, Eq, Debug, Hash)] +#[derive(Clone, PartialEq, Eq, Debug, Hash)] pub struct AccountKeySignedPayload { payload: Vec, signature: near_crypto::Signature, diff --git a/chain/network/src/network_protocol/network.proto b/chain/network/src/network_protocol/network.proto index 6a077bf644b..488ca198f13 100644 --- a/chain/network/src/network_protocol/network.proto +++ b/chain/network/src/network_protocol/network.proto @@ -189,19 +189,32 @@ message PeerAddr { } message AccountData { - string account_id = 1; // required - // Either address of the node handling the account (if it has a public IP), - // or a list of peers known to be connected to the validator. + reserved 1,3; + + // PeerId of the node owning the account_key. + // Used to route the message over TIER1. + // TODO(gprusak): it should be possible to add support for routing + // messages to an account_id directly (for TIER1), instead of routing + // to a specific peer_id. Then this field won't be necessary. + // Unless we use it instead of AnnounceAccount. + PublicKey peer_id = 5; // required. + + PublicKey account_key = 6; // required. + + // List of nodes which + // - are trusted by the validator and + // - are connected to the validator directly + // - are willing to proxy traffic to the validator. + // It may include the validator node itself, if it has a public IP. // If empty, the validator explicitly declares that it has no public IP - // and the P2P routing should be used instead (discouraged, might be disallowed in the future). - repeated PeerAddr peers = 2; - - // Epoch for which this data is valid. - // This AccountData should be signed with the account key assigned to - // for epoch . - CryptoHash epoch_id = 3; - // If there are multiple signed AccountData messages for the same - // account_id for the same epoch, the one with the most recent timestamp is valid. + // and the TIER2 routing should be used instead (discouraged, might be disallowed in the future). + repeated PeerAddr proxies = 2; + + // Version of the AccountData. A node can override a previous version, + // by broadcasting a never version. + uint64 version = 7; + // Time of creation of this AccountData. + // TODO(gprusak): consider expiring the AccountData based on this field. google.protobuf.Timestamp timestamp = 4; } diff --git a/chain/network/src/network_protocol/proto_conv/account_key.rs b/chain/network/src/network_protocol/proto_conv/account_key.rs index d04f1749e9c..77787795c98 100644 --- a/chain/network/src/network_protocol/proto_conv/account_key.rs +++ b/chain/network/src/network_protocol/proto_conv/account_key.rs @@ -4,20 +4,18 @@ use super::*; use crate::network_protocol::proto; use crate::network_protocol::proto::account_key_payload::Payload_type as ProtoPT; use crate::network_protocol::{AccountData, AccountKeySignedPayload, SignedAccountData}; -use near_primitives::account::id::ParseAccountError; -use near_primitives::types::EpochId; use protobuf::{Message as _, MessageField as MF}; #[derive(thiserror::Error, Debug)] pub enum ParseAccountDataError { #[error("bad payload type")] BadPayloadType, - #[error("account_id: {0}")] - AccountId(ParseAccountError), + #[error("peer_id: {0}")] + PeerId(ParseRequiredError), + #[error("account_key: {0}")] + AccountKey(ParseRequiredError), #[error("peers: {0}")] Peers(ParseVecError), - #[error("epoch_id: {0}")] - EpochId(ParseRequiredError), #[error("timestamp: {0}")] Timestamp(ParseRequiredError), } @@ -29,9 +27,10 @@ impl From<&AccountData> for proto::AccountKeyPayload { fn from(x: &AccountData) -> Self { Self { payload_type: Some(ProtoPT::AccountData(proto::AccountData { - account_id: x.account_id.to_string(), - peers: x.peers.iter().map(Into::into).collect(), - epoch_id: MF::some((&x.epoch_id.0).into()), + peer_id: MF::some((&x.peer_id).into()), + account_key: MF::some((&x.account_key).into()), + proxies: x.proxies.iter().map(Into::into).collect(), + version: x.version, timestamp: MF::some(utc_to_proto(&x.timestamp)), ..Default::default() })), @@ -49,9 +48,10 @@ impl TryFrom<&proto::AccountKeyPayload> for AccountData { _ => return Err(Self::Error::BadPayloadType), }; Ok(Self { - account_id: x.account_id.clone().try_into().map_err(Self::Error::AccountId)?, - peers: try_from_slice(&x.peers).map_err(Self::Error::Peers)?, - epoch_id: EpochId(try_from_required(&x.epoch_id).map_err(Self::Error::EpochId)?), + peer_id: try_from_required(&x.peer_id).map_err(Self::Error::PeerId)?, + account_key: try_from_required(&x.account_key).map_err(Self::Error::AccountKey)?, + proxies: try_from_slice(&x.proxies).map_err(Self::Error::Peers)?, + version: x.version, timestamp: map_from_required(&x.timestamp, utc_from_proto) .map_err(Self::Error::Timestamp)?, }) diff --git a/chain/network/src/network_protocol/proto_conv/crypto.rs b/chain/network/src/network_protocol/proto_conv/crypto.rs index 81731bb648f..cbadf92b1b3 100644 --- a/chain/network/src/network_protocol/proto_conv/crypto.rs +++ b/chain/network/src/network_protocol/proto_conv/crypto.rs @@ -1,6 +1,7 @@ /// Conversion functions for messages representing crypto primitives. use crate::network_protocol::proto; use borsh::{BorshDeserialize as _, BorshSerialize as _}; +use near_crypto::PublicKey; use near_primitives::hash::CryptoHash; use near_primitives::network::PeerId; @@ -25,18 +26,31 @@ impl TryFrom<&proto::CryptoHash> for CryptoHash { ////////////////////////////////////////// -pub type ParsePeerIdError = borsh::maybestd::io::Error; +pub type ParsePublicKeyError = borsh::maybestd::io::Error; + +impl From<&PublicKey> for proto::PublicKey { + fn from(x: &PublicKey) -> Self { + Self { borsh: x.try_to_vec().unwrap(), ..Self::default() } + } +} + +impl TryFrom<&proto::PublicKey> for PublicKey { + type Error = ParsePublicKeyError; + fn try_from(p: &proto::PublicKey) -> Result { + Self::try_from_slice(&p.borsh) + } +} impl From<&PeerId> for proto::PublicKey { fn from(x: &PeerId) -> Self { - Self { borsh: x.try_to_vec().unwrap(), ..Self::default() } + x.public_key().into() } } impl TryFrom<&proto::PublicKey> for PeerId { - type Error = ParsePeerIdError; + type Error = ParsePublicKeyError; fn try_from(p: &proto::PublicKey) -> Result { - Self::try_from_slice(&p.borsh) + Ok(PeerId::new(PublicKey::try_from(p)?)) } } diff --git a/chain/network/src/network_protocol/proto_conv/handshake.rs b/chain/network/src/network_protocol/proto_conv/handshake.rs index a8484929346..95b1bd83427 100644 --- a/chain/network/src/network_protocol/proto_conv/handshake.rs +++ b/chain/network/src/network_protocol/proto_conv/handshake.rs @@ -66,9 +66,9 @@ impl TryFrom<&proto::PeerChainInfo> for PeerChainInfoV2 { #[derive(thiserror::Error, Debug)] pub enum ParseHandshakeError { #[error("sender_peer_id {0}")] - SenderPeerId(ParseRequiredError), + SenderPeerId(ParseRequiredError), #[error("target_peer_id {0}")] - TargetPeerId(ParseRequiredError), + TargetPeerId(ParseRequiredError), #[error("sender_listen_port {0}")] SenderListenPort(std::num::TryFromIntError), #[error("sender_chain_info {0}")] diff --git a/chain/network/src/network_protocol/proto_conv/net.rs b/chain/network/src/network_protocol/proto_conv/net.rs index ae9a2e62fb6..8fc16aef16b 100644 --- a/chain/network/src/network_protocol/proto_conv/net.rs +++ b/chain/network/src/network_protocol/proto_conv/net.rs @@ -50,7 +50,7 @@ pub enum ParsePeerAddrError { #[error("addr: {0}")] Addr(ParseRequiredError), #[error("peer_id: {0}")] - PeerId(ParseRequiredError), + PeerId(ParseRequiredError), } impl From<&PeerAddr> for proto::PeerAddr { diff --git a/chain/network/src/network_protocol/testonly.rs b/chain/network/src/network_protocol/testonly.rs index b9014692464..7f1518d75e2 100644 --- a/chain/network/src/network_protocol/testonly.rs +++ b/chain/network/src/network_protocol/testonly.rs @@ -229,15 +229,19 @@ impl ChunkSet { } } -pub fn make_epoch_id(rng: &mut R) -> EpochId { - EpochId(CryptoHash::hash_bytes(&rng.gen::<[u8; 19]>())) +pub fn make_account_keys(signers: &[InMemoryValidatorSigner]) -> AccountKeys { + let mut account_keys = AccountKeys::new(); + for s in signers { + account_keys.entry(s.validator_id().clone()).or_default().insert(s.public_key()); + } + account_keys } pub struct Chain { pub genesis_id: GenesisId, pub blocks: Vec, pub chunks: HashMap, - pub tier1_accounts: Vec<(EpochId, InMemoryValidatorSigner)>, + pub tier1_accounts: Vec, } impl Chain { @@ -256,9 +260,7 @@ impl Chain { hash: Default::default(), }, blocks, - tier1_accounts: (0..10) - .map(|_| (make_epoch_id(rng), make_validator_signer(rng))) - .collect(), + tier1_accounts: (0..10).map(|_| make_validator_signer(rng)).collect(), chunks: chunks.chunks, } } @@ -272,12 +274,7 @@ impl Chain { } pub fn get_tier1_accounts(&self) -> AccountKeys { - self.tier1_accounts - .iter() - .map(|(epoch_id, v)| { - ((epoch_id.clone(), v.validator_id().clone()), v.public_key().clone()) - }) - .collect() + make_account_keys(&self.tier1_accounts) } pub fn get_chain_info(&self) -> ChainInfo { @@ -318,16 +315,12 @@ impl Chain { ) -> Vec> { self.tier1_accounts .iter() - .map(|(epoch_id, v)| { + .map(|v| { + let peer_id = make_peer_id(rng); Arc::new( - make_account_data( - rng, - clock.now_utc(), - epoch_id.clone(), - v.validator_id().clone(), - ) - .sign(v) - .unwrap(), + make_account_data(rng, 1, clock.now_utc(), v.public_key(), peer_id) + .sign(v) + .unwrap(), ) }) .collect() @@ -377,12 +370,13 @@ pub fn make_peer_addr(rng: &mut impl Rng, ip: net::IpAddr) -> PeerAddr { pub fn make_account_data( rng: &mut impl Rng, + version: u64, timestamp: time::Utc, - epoch_id: EpochId, - account_id: AccountId, + account_key: PublicKey, + peer_id: PeerId, ) -> AccountData { AccountData { - peers: vec![ + proxies: vec![ // Can't inline make_ipv4/ipv6 calls, because 2-phase borrow // doesn't work. { @@ -398,18 +392,17 @@ pub fn make_account_data( make_peer_addr(rng, ip) }, ], - account_id, - epoch_id, + peer_id, + account_key, + version, timestamp, } } pub fn make_signed_account_data(rng: &mut impl Rng, clock: &time::Clock) -> SignedAccountData { let signer = make_validator_signer(rng); - let epoch_id = make_epoch_id(rng); - make_account_data(rng, clock.now_utc(), epoch_id, signer.validator_id().clone()) - .sign(&signer) - .unwrap() + let peer_id = make_peer_id(rng); + make_account_data(rng, 1, clock.now_utc(), signer.public_key(), peer_id).sign(&signer).unwrap() } // Accessors for creating malformed SignedAccountData diff --git a/chain/network/src/network_protocol/tests.rs b/chain/network/src/network_protocol/tests.rs index 2382ffd9986..d6c4d7a192e 100644 --- a/chain/network/src/network_protocol/tests.rs +++ b/chain/network/src/network_protocol/tests.rs @@ -7,6 +7,7 @@ use crate::types::{HandshakeFailureReason, PeerMessage}; use crate::types::{PartialEncodedChunkRequestMsg, PartialEncodedChunkResponseMsg}; use anyhow::{bail, Context as _}; use itertools::Itertools as _; +use rand::Rng as _; #[test] fn deduplicate_edges() { @@ -39,14 +40,15 @@ fn bad_account_data_size() { let signer = data::make_validator_signer(&mut rng); let ad = AccountData { - peers: (0..1000) + proxies: (0..1000) .map(|_| { let ip = data::make_ipv6(&mut rng); data::make_peer_addr(&mut rng, ip) }) .collect(), - account_id: signer.validator_id().clone(), - epoch_id: data::make_epoch_id(&mut rng), + account_key: signer.public_key(), + peer_id: data::make_peer_id(&mut rng), + version: rng.gen(), timestamp: clock.now_utc(), }; assert!(ad.sign(&signer).is_err()); diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index ca44c8fea19..b007697e448 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -110,9 +110,6 @@ pub(crate) struct PeerActor { peer_addr: SocketAddr, /// Peer type. peer_type: PeerType, - /// OUTBOUND-ONLY: Handshake specification. For outbound connections it is initialized - /// in constructor and then can change as HandshakeFailure and LastEdge messages - /// are received. For inbound connections, handshake is stateless. /// Framed wrapper to send messages through the TCP connection. framed: stream::FramedStream, diff --git a/chain/network/src/peer_manager/connection/mod.rs b/chain/network/src/peer_manager/connection/mod.rs index 7e894e70e6e..ff57a948529 100644 --- a/chain/network/src/peer_manager/connection/mod.rs +++ b/chain/network/src/peer_manager/connection/mod.rs @@ -159,9 +159,9 @@ impl Connection { let res = ds.iter().map(|_| ()).collect(); let mut sum = HashMap::<_, Arc>::new(); for d in ds.into_iter().flatten() { - match sum.entry((d.epoch_id.clone(), d.account_id.clone())) { + match sum.entry(d.account_key.clone()) { Entry::Occupied(mut x) => { - if x.get().timestamp < d.timestamp { + if x.get().version < d.version { x.insert(d); } } diff --git a/chain/network/src/peer_manager/network_state/mod.rs b/chain/network/src/peer_manager/network_state/mod.rs index 1de33f4a32c..2e7a72a10d3 100644 --- a/chain/network/src/peer_manager/network_state/mod.rs +++ b/chain/network/src/peer_manager/network_state/mod.rs @@ -31,6 +31,8 @@ use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; use std::sync::Arc; use tracing::{debug, trace, Instrument}; +mod tier1; + /// Limit number of pending Peer actors to avoid OOM. pub(crate) const LIMIT_PENDING_PEERS: usize = 60; diff --git a/chain/network/src/peer_manager/network_state/tier1.rs b/chain/network/src/peer_manager/network_state/tier1.rs new file mode 100644 index 00000000000..f9ecbe6e486 --- /dev/null +++ b/chain/network/src/peer_manager/network_state/tier1.rs @@ -0,0 +1,80 @@ +use crate::accounts_data; +use crate::config; +use crate::network_protocol::{AccountData, PeerMessage, SignedAccountData, SyncAccountsData}; +use crate::peer_manager::peer_manager_actor::Event; +use crate::time; +use std::sync::Arc; + +impl super::NetworkState { + // Returns ValidatorConfig of this node iff it belongs to TIER1 according to `accounts_data`. + pub fn tier1_validator_config( + &self, + accounts_data: &accounts_data::CacheSnapshot, + ) -> Option<&config::ValidatorConfig> { + if self.config.tier1.is_none() { + return None; + } + self.config + .validator + .as_ref() + .filter(|cfg| accounts_data.keys.contains(&cfg.signer.public_key())) + } + + /// Tries to connect to ALL trusted proxies from the config, then broadcasts AccountData with + /// the set of proxies it managed to connect to. This way other TIER1 nodes can just connect + /// to ANY proxy of this node. + pub async fn tier1_advertise_proxies( + self: &Arc, + clock: &time::Clock, + ) -> Vec> { + let accounts_data = self.accounts_data.load(); + let Some(vc) = self.tier1_validator_config(&accounts_data) else { + return vec![]; + }; + // TODO(gprusak): for now we just blindly broadcast the static list of proxies, however + // here we should try to connect to the TIER1 proxies, before broadcasting them. + let my_proxies = match &vc.proxies { + config::ValidatorProxies::Dynamic(_) => vec![], + config::ValidatorProxies::Static(proxies) => proxies.clone(), + }; + let now = clock.now_utc(); + let version = + self.accounts_data.load().data.get(&vc.signer.public_key()).map_or(0, |d| d.version) + + 1; + // This unwrap is safe, because we did signed a sample payload during + // config validation. See config::Config::new(). + let my_data = Arc::new( + AccountData { + peer_id: self.config.node_id(), + account_key: vc.signer.public_key(), + proxies: my_proxies.clone(), + timestamp: now, + version, + } + .sign(vc.signer.as_ref()) + .unwrap(), + ); + let (new_data, err) = self.accounts_data.insert(vec![my_data]).await; + // Inserting node's own AccountData should never fail. + if let Some(err) = err { + panic!("inserting node's own AccountData to self.state.accounts_data: {err}"); + } + if new_data.is_empty() { + // If new_data is empty, it means that accounts_data contains entry newer than `version`. + // This means that this node has been restarted and forgot what was the latest `version` + // of accounts_data published AND it just learned about it from a peer. + // TODO(gprusak): for better resiliency, consider persisting latest version in storage. + // TODO(gprusak): consider broadcasting a new version immediately after learning about + // conflicting version. + tracing::info!("received a conflicting version of AccountData (expected, iff node has been just restarted)"); + return vec![]; + } + self.tier2.broadcast_message(Arc::new(PeerMessage::SyncAccountsData(SyncAccountsData { + incremental: true, + requesting_full_sync: true, + accounts_data: new_data.clone(), + }))); + self.config.event_sink.push(Event::Tier1AdvertiseProxies(new_data.clone())); + new_data + } +} diff --git a/chain/network/src/peer_manager/peer_manager_actor.rs b/chain/network/src/peer_manager/peer_manager_actor.rs index 2e61acab01a..f23a404e81b 100644 --- a/chain/network/src/peer_manager/peer_manager_actor.rs +++ b/chain/network/src/peer_manager/peer_manager_actor.rs @@ -2,8 +2,8 @@ use crate::client; use crate::config; use crate::debug::{DebugStatus, GetDebugStatus}; use crate::network_protocol::{ - AccountData, AccountOrPeerIdOrHash, Edge, PeerMessage, Ping, Pong, RawRoutedMessage, - RoutedMessageBody, StateResponseInfo, SyncAccountsData, + AccountOrPeerIdOrHash, Edge, PeerMessage, Ping, Pong, RawRoutedMessage, RoutedMessageBody, + SignedAccountData, StateResponseInfo, }; use crate::peer::peer_actor::PeerActor; use crate::peer_manager::connection; @@ -122,6 +122,8 @@ pub enum Event { // actually complete. Currently this event is reported only for some message types, // feel free to add support for more. MessageProcessed(PeerMessage), + // Reported every time a new list of proxies has been constructed. + Tier1AdvertiseProxies(Vec>), // Reported when a handshake has been started. HandshakeStarted(crate::peer::peer_actor::HandshakeStartedEvent), // Reported when a handshake has been successfully completed. @@ -261,6 +263,20 @@ impl PeerManagerActor { client, whitelist_nodes, )); + if let Some(cfg) = state.config.tier1.clone() { + // Connect to TIER1 proxies and broadcast the list those connections periodically. + arbiter.spawn({ + let clock = clock.clone(); + let state = state.clone(); + let mut interval = time::Interval::new(clock.now(), cfg.advertise_proxies_interval); + async move { + loop { + interval.tick(&clock).await; + state.tier1_advertise_proxies(&clock).await; + } + } + }); + } Ok(Self::start_in_arbiter(&arbiter, move |_ctx| Self { my_peer_id: my_peer_id.clone(), started_connect_attempts: false, @@ -944,93 +960,51 @@ impl Handler> for PeerManagerActor { } } -impl Handler> for PeerManagerActor { +impl actix::Handler> for PeerManagerActor { type Result = (); fn handle(&mut self, msg: WithSpanContext, ctx: &mut Self::Context) { let (_span, info) = handler_trace_span!(target: "network", msg); let _timer = metrics::PEER_MANAGER_MESSAGES_TIME.with_label_values(&["SetChainInfo"]).start_timer(); - let now = self.clock.now_utc(); let SetChainInfo(info) = info; let state = self.state.clone(); // We set state.chain_info and call accounts_data.set_keys // synchronously, therefore, assuming actix in-order delivery, // there will be no race condition between subsequent SetChainInfo // calls. - // TODO(gprusak): if we could make handle() async, then we could - // just require the caller to await for completion before calling - // SetChainInfo again. Alternatively we could have an async mutex - // on the handler. state.chain_info.store(Arc::new(Some(info.clone()))); - // If enable_tier1 is false, we skip set_keys() call. + // If tier1 is not enabled, we skip set_keys() call. // This way self.state.accounts_data is always empty, hence no data // will be collected or broadcasted. - if !state.config.features.enable_tier1 { + if state.config.tier1.is_none() { + state.config.event_sink.push(Event::SetChainInfo); return; } // If the key set didn't change, early exit. if !state.accounts_data.set_keys(info.tier1_accounts.clone()) { + state.config.event_sink.push(Event::SetChainInfo); return; } - ctx.spawn(wrap_future(async move { - // If the set of keys has changed, and the node is a validator, - // we should try to sign data and broadcast it. However, this is - // also a trigger for a full sync, so a dedicated broadcast is - // not required. - // - // TODO(gprusak): For dynamic self-IP-discovery, add a STUN daemon which - // will add new AccountData and trigger an incremental broadcast. - if let Some(vc) = &state.config.validator { - let my_account_id = vc.signer.validator_id(); - let my_public_key = vc.signer.public_key(); - // TODO(gprusak): STUN servers should be queried periocally by a daemon - // so that the my_peers list is always resolved. - // Note that currently we will broadcast an empty list. - // It won't help us to connect the the validator BUT it - // will indicate that a validator is misconfigured, which - // is could be useful for debugging. Consider keeping this - // behavior for situations when the IPs are not known. - let my_peers = match &vc.endpoints { - config::ValidatorEndpoints::TrustedStunServers(_) => vec![], - config::ValidatorEndpoints::PublicAddrs(peer_addrs) => peer_addrs.clone(), - }; - let my_data = info.tier1_accounts.iter().filter_map(|((epoch_id,account_id),key)| { - if account_id != my_account_id{ - return None; - } - if key != &my_public_key { - warn!(target: "network", "node's account_id found in TIER1 accounts, but the public keys do not match"); - return None; - } - // This unwrap is safe, because we did signed a sample payload during - // config validation. See config::Config::new(). - Some(Arc::new(AccountData { - epoch_id: epoch_id.clone(), - account_id: my_account_id.clone(), - timestamp: now, - peers: my_peers.clone(), - }.sign(vc.signer.as_ref()).unwrap())) - }).collect(); - // Insert node's own AccountData should never fail. - // We ignore the new data, because we trigger a full sync anyway. - if let (_, Some(err)) = state.accounts_data.insert(my_data).await { - panic!("inserting node's own AccountData to self.state.accounts_data: {err}"); - } + let clock = self.clock.clone(); + ctx.spawn(wrap_future( + async move { + // This node might have become a TIER1 node due to the change of the key set. + // If so we should recompute and readvertise the list of proxies. + // This is mostly important in case a node is its own proxy. In all other cases + // (when proxies are different nodes) the update of the key set happens asynchronously + // and this node won't be able to connect to proxies until it happens (and only the + // connected proxies are included in the advertisement). We run tier1_advertise_proxies + // periodically in the background anyway to cover those cases. + // + // The set of tier1 accounts has changed, so we might be missing some accounts_data + // that our peers know about. tier1_advertise_proxies() has a side effect + // of asking peers for a full sync of the accounts_data with the TIER2 peers. + state.tier1_advertise_proxies(&clock).await; + state.config.event_sink.push(Event::SetChainInfo); } - // The set of tier1 accounts has changed. - // We might miss some data, so we start a full sync with the connected peers. - // TODO(gprusak): add a daemon which does a periodic full sync in case some messages - // are lost (at a frequency which makes the additional network load negligible). - state.tier2.broadcast_message(Arc::new(PeerMessage::SyncAccountsData( - SyncAccountsData { - incremental: false, - requesting_full_sync: true, - accounts_data: state.accounts_data.load().data.values().cloned().collect(), - }, - ))); - state.config.event_sink.push(Event::SetChainInfo); - }.in_current_span())); + .in_current_span(), + )); } } diff --git a/chain/network/src/peer_manager/testonly.rs b/chain/network/src/peer_manager/testonly.rs index 4928e1e769a..95af4eb7257 100644 --- a/chain/network/src/peer_manager/testonly.rs +++ b/chain/network/src/peer_manager/testonly.rs @@ -2,7 +2,7 @@ use crate::broadcast; use crate::config; use crate::network_protocol::testonly as data; use crate::network_protocol::{ - EdgeState, Encoding, PeerAddr, PeerInfo, PeerMessage, SignedAccountData, SyncAccountsData, + EdgeState, Encoding, PeerInfo, PeerMessage, SignedAccountData, SyncAccountsData, }; use crate::peer; use crate::peer::peer_actor::ClosingReason; @@ -15,12 +15,13 @@ use crate::testonly::actix::ActixSystem; use crate::testonly::fake_client; use crate::time; use crate::types::{ - ChainInfo, KnownPeerStatus, NetworkRequests, PeerManagerMessageRequest, SetChainInfo, + AccountKeys, ChainInfo, KnownPeerStatus, NetworkRequests, PeerManagerMessageRequest, + SetChainInfo, }; use crate::PeerManagerActor; use near_o11y::WithSpanContextExt; use near_primitives::network::{AnnounceAccount, PeerId}; -use near_primitives::types::{AccountId, EpochId}; +use near_primitives::types::AccountId; use std::collections::HashSet; use std::future::Future; use std::pin::Pin; @@ -62,21 +63,16 @@ pub fn unwrap_sync_accounts_data_processed(ev: Event) -> Option, -} - -impl From<&Arc> for NormalAccountData { - fn from(d: &Arc) -> Self { - Self { - epoch_id: d.epoch_id.clone(), - account_id: d.account_id.clone(), - peers: d.peers.clone(), - } +pub(crate) fn make_chain_info(chain: &data::Chain, validators: &[&ActorHandler]) -> ChainInfo { + // Construct ChainInfo with tier1_accounts set to `validators`. + let mut chain_info = chain.get_chain_info(); + let mut account_keys = AccountKeys::new(); + for pm in validators { + let s = &pm.cfg.validator.as_ref().unwrap().signer; + account_keys.entry(s.validator_id().clone()).or_default().insert(s.public_key()); } + chain_info.tier1_accounts = Arc::new(account_keys); + chain_info } pub(crate) struct RawConnection { @@ -295,9 +291,10 @@ impl ActorHandler { self.with_state(move |s| async move { s.fix_local_edges(&clock, timeout).await }).await } - pub async fn set_chain_info(&mut self, chain_info: ChainInfo) { + pub async fn set_chain_info(&self, chain_info: ChainInfo) { + let mut events = self.events.from_now(); self.actix.addr.send(SetChainInfo(chain_info).with_span_context()).await.unwrap(); - self.events + events .recv_until(|ev| match ev { Event::PeerManager(PME::SetChainInfo) => Some(()), _ => None, @@ -305,6 +302,14 @@ impl ActorHandler { .await; } + pub async fn tier1_advertise_proxies( + &self, + clock: &time::Clock, + ) -> Vec> { + let clock = clock.clone(); + self.with_state(move |s| async move { s.tier1_advertise_proxies(&clock).await }).await + } + pub async fn announce_account(&self, aa: AnnounceAccount) { self.actix .addr @@ -317,19 +322,21 @@ impl ActorHandler { } // Awaits until the accounts_data state matches `want`. - pub async fn wait_for_accounts_data(&mut self, want: &HashSet) { + pub async fn wait_for_accounts_data(&self, want: &HashSet>) { + let mut events = self.events.from_now(); loop { - let got: HashSet<_> = self - .with_state(|s| async move { - s.accounts_data.load().data.values().map(|d| d.into()).collect() + let got = self + .with_state(move |s| async move { + s.accounts_data.load().data.values().cloned().collect::>() }) .await; + tracing::info!(target:"dupa","got = {:?}",got); if &got == want { break; } // It is important that we wait for the next PeerMessage::SyncAccountsData to get // PROCESSED, not just RECEIVED. Otherwise we would get a race condition. - self.events.recv_until(unwrap_sync_accounts_data_processed).await; + events.recv_until(unwrap_sync_accounts_data_processed).await; } } diff --git a/chain/network/src/peer_manager/tests/accounts_data.rs b/chain/network/src/peer_manager/tests/accounts_data.rs index 47addce7001..e19d179512b 100644 --- a/chain/network/src/peer_manager/tests/accounts_data.rs +++ b/chain/network/src/peer_manager/tests/accounts_data.rs @@ -1,11 +1,10 @@ use crate::concurrency::demux; -use crate::config; use crate::network_protocol::testonly as data; -use crate::network_protocol::{PeerAddr, SyncAccountsData}; +use crate::network_protocol::SyncAccountsData; use crate::peer; use crate::peer_manager; use crate::peer_manager::peer_manager_actor::Event as PME; -use crate::peer_manager::testonly::NormalAccountData; +use crate::peer_manager::testonly; use crate::testonly::{make_rng, AsSet as _}; use crate::time; use crate::types::PeerMessage; @@ -13,17 +12,9 @@ use itertools::Itertools; use near_o11y::testonly::init_test_logger; use pretty_assertions::assert_eq; use rand::seq::SliceRandom as _; +use std::collections::HashSet; use std::sync::Arc; -fn peer_addrs(vc: &config::ValidatorConfig) -> Vec { - match &vc.endpoints { - config::ValidatorEndpoints::PublicAddrs(peer_addrs) => peer_addrs.clone(), - config::ValidatorEndpoints::TrustedStunServers(_) => { - panic!("tests only support PublicAddrs in validator config") - } - } -} - #[tokio::test] async fn broadcast() { init_test_logger(); @@ -34,7 +25,7 @@ async fn broadcast() { let clock = clock.clock(); let clock = &clock; - let mut pm = peer_manager::testonly::start( + let pm = peer_manager::testonly::start( clock.clone(), near_store::db::TestDB::new(), chain.make_config(rng), @@ -56,7 +47,6 @@ async fn broadcast() { }; let data = chain.make_tier1_data(rng, clock); - tracing::info!(target:"test", "Connect peer, expect initial sync to be empty."); let mut peer1 = pm.start_inbound(chain.clone(), chain.make_config(rng)).await.handshake(clock).await; @@ -69,15 +59,15 @@ async fn broadcast() { incremental: true, requesting_full_sync: false, }; - let want = msg.accounts_data.clone(); + let want: HashSet<_> = msg.accounts_data.iter().cloned().collect(); peer1.send(PeerMessage::SyncAccountsData(msg)).await; - pm.wait_for_accounts_data(&want.iter().map(|d| d.into()).collect()).await; + pm.wait_for_accounts_data(&want).await; tracing::info!(target:"test", "Connect another peer and perform initial full sync."); let mut peer2 = pm.start_inbound(chain.clone(), chain.make_config(rng)).await.handshake(clock).await; let got2 = peer2.events.recv_until(take_full_sync).await; - assert_eq!(got2.accounts_data.as_set(), want.as_set()); + assert_eq!(got2.accounts_data.as_set(), want.iter().collect()); tracing::info!(target:"test", "Send a mix of new and old data. Only new data should be broadcasted."); let msg = SyncAccountsData { @@ -135,35 +125,24 @@ async fn gradual_epoch_change() { pms[0].connect_to(&pm1).await; pms[1].connect_to(&pm2).await; - // Validator configs. - let vs: Vec<_> = pms.iter().map(|pm| pm.cfg.validator.clone().unwrap()).collect(); - // For every order of nodes. for ids in (0..pms.len()).permutations(pms.len()) { - // Construct ChainInfo for a new epoch, - // with tier1_accounts containing all validators. - let e = data::make_epoch_id(rng); - let mut chain_info = chain.get_chain_info(); - chain_info.tier1_accounts = Arc::new( - vs.iter() - .map(|v| ((e.clone(), v.signer.validator_id().clone()), v.signer.public_key())) - .collect(), - ); + tracing::info!(target:"test", "permutation {ids:?}"); + clock.advance(time::Duration::hours(1)); + let chain_info = testonly::make_chain_info(&chain, &pms.iter().collect::>()[..]); + let mut want = HashSet::new(); // Advance epoch in the given order. for id in ids { pms[id].set_chain_info(chain_info.clone()).await; + // In this tests each node is its own proxy, so it can immediately + // connect to itself (to verify the public addr) and advertise it. + // If some other node B was a proxy for a node A, then first both + // A and B would have to update their chain_info, and only then A + // would be able to connect to B and advertise B as proxy afterwards. + want.extend(pms[id].tier1_advertise_proxies(&clock.clock()).await); } - // Wait for data to arrive. - let want = vs - .iter() - .map(|v| NormalAccountData { - epoch_id: e.clone(), - account_id: v.signer.validator_id().clone(), - peers: peer_addrs(v), - }) - .collect(); for pm in &mut pms { pm.wait_for_accounts_data(&want).await; } @@ -226,38 +205,24 @@ async fn rate_limiting() { } } - // Validator configs. - let vs: Vec<_> = pms.iter().map(|pm| pm.cfg.validator.clone().unwrap()).collect(); + // Construct ChainInfo with tier1_accounts containing all validators. + let chain_info = testonly::make_chain_info(&chain, &pms.iter().collect::>()[..]); + + clock.advance(time::Duration::hours(1)); - // Construct ChainInfo for a new epoch, - // with tier1_accounts containing all validators. - let e = data::make_epoch_id(rng); - let mut chain_info = chain.get_chain_info(); - chain_info.tier1_accounts = Arc::new( - vs.iter() - .map(|v| ((e.clone(), v.signer.validator_id().clone()), v.signer.public_key())) - .collect(), - ); + // Capture the event streams now, so that we can compute + // the total number of SyncAccountsData messages exchanged in the process. + let events: Vec<_> = pms.iter().map(|pm| pm.events.from_now()).collect(); tracing::info!(target:"test","Advance epoch in random order."); pms.shuffle(rng); + let mut want = HashSet::new(); for pm in &mut pms { pm.set_chain_info(chain_info.clone()).await; + want.extend(pm.tier1_advertise_proxies(&clock.clock()).await); } - // Capture the event streams at the start, so that we can compute - // the total number of SyncAccountsData messages exchanged in the process. - let events: Vec<_> = pms.iter().map(|pm| pm.events.clone()).collect(); - tracing::info!(target:"test","Wait for data to arrive."); - let want = vs - .iter() - .map(|v| NormalAccountData { - epoch_id: e.clone(), - account_id: v.signer.validator_id().clone(), - peers: peer_addrs(&v), - }) - .collect(); for pm in &mut pms { pm.wait_for_accounts_data(&want).await; } diff --git a/chain/network/src/types.rs b/chain/network/src/types.rs index 6c91619cabe..2dc9ad74933 100644 --- a/chain/network/src/types.rs +++ b/chain/network/src/types.rs @@ -16,9 +16,9 @@ use near_primitives::network::{AnnounceAccount, PeerId}; use near_primitives::sharding::PartialEncodedChunkWithArcReceipts; use near_primitives::transaction::SignedTransaction; use near_primitives::types::BlockHeight; -use near_primitives::types::{AccountId, EpochId, ShardId}; +use near_primitives::types::{AccountId, ShardId}; use once_cell::sync::OnceCell; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::net::SocketAddr; use std::sync::Arc; @@ -126,7 +126,7 @@ impl KnownPeerStatus { /// Set of account keys. /// This is information which chain pushes to network to implement tier1. /// See ChainInfo. -pub type AccountKeys = HashMap<(EpochId, AccountId), PublicKey>; +pub type AccountKeys = HashMap>; /// Network-relevant data about the chain. // TODO(gprusak): it is more like node info, or sth. diff --git a/nearcore/src/config.rs b/nearcore/src/config.rs index b964d2dc744..c755a10a4ef 100644 --- a/nearcore/src/config.rs +++ b/nearcore/src/config.rs @@ -612,8 +612,6 @@ impl NearConfig { network_key_pair.secret_key, validator_signer.clone(), config.archive, - // Enable tier1 (currently tier1 discovery only). - near_network::config::Features { enable_tier1: true }, )?, telemetry_config: config.telemetry, #[cfg(feature = "json_rpc")] From 085b6410b6078b78d237a56309f2312ee0ca92a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petar=20Vujovi=C4=87?= Date: Tue, 22 Nov 2022 13:02:32 +0100 Subject: [PATCH 021/188] docs: formatting and sentence structure (#8094) small changes to formatting - making names of components bold minor corrections for using proper articles and plural versions of words **P.S.**: I'll be adding more changes as I am going through the docs which will hopefully help with the understandability and readability of the docs. --- docs/architecture/README.md | 68 +++++++++++++------------- docs/architecture/how/README.md | 58 +++++++++++----------- docs/architecture/how/cross-shard.md | 60 ++++++++++++----------- docs/architecture/how/epoch.md | 42 ++++++++-------- docs/architecture/how/gc.md | 24 ++++----- docs/architecture/how/serialization.md | 50 +++++++++---------- docs/architecture/how/sync.md | 56 ++++++++++----------- docs/architecture/how/tx_receipts.md | 63 ++++++++++++------------ docs/architecture/how/tx_routing.md | 46 +++++++++-------- 9 files changed, 235 insertions(+), 232 deletions(-) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 2e55b2fc495..06a697064aa 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -39,16 +39,16 @@ missing bits of history from more up-to-date peer nodes. Major components of nearcore: -* JSON RPC. This HTTP RPC interface is how `neard` communicates with +* **JSON RPC**. This HTTP RPC interface is how `neard` communicates with non-blockchain outside world. For example, to submit a transaction, some client sends an RPC request with it to some node in the network. From that node, the transaction propagates through the network, until it is included in some block. Similarly, a client can send an HTTP request to a node to learn - about current state of the blockchain. The JSON RPC interface is documented + about current state of the blockchain. The **JSON RPC** interface is documented [here](https://docs.near.org/api/rpc/introduction). -* Network. If RPC is aimed "outside" the blockchain, "network" is how peer - `neard` nodes communicate with each other within blockchain. RPC carries +* **Network**. If RPC is aimed "outside" the blockchain, "network" is how peer + `neard` nodes communicate with each other within the blockchain. RPC carries requests from users of the blockchain, while network carries various messages needed to implement consensus. Two directly connected nodes communicate by sending protobuf-encoded messages over TCP. A node also includes logic to @@ -57,44 +57,44 @@ Major components of nearcore: network participant. From this bootstrap connection, the node learns how to communicate with any other node in the network. -* Client. Somewhat confusingly named, client is the logical state of the - blockchain. After receiving and decoding a request, both RPC and network - usually forward it in the parsed form to the client. Internally, client is - split in two somewhat independent components: chain and runtime. +* **Client**. Somewhat confusingly named, **client** is the logical state of the + blockchain. After receiving and decoding a request, both **RPC** and **network** + usually forward it in the parsed form to the **client**. Internally, **client** is + split in two somewhat independent components: **chain** and **runtime**. -* Chain. The job of chain, in a nutshell, is to determine a global order of - transactions. Chain builds and maintains the blockchain data structure. This +* **Chain**. The job of **chain**, in a nutshell, is to determine a global order of + transactions. **Chain** builds and maintains the blockchain data structure. This includes block and chunk production and processing, consensus, and validator - selection. However, chain is not responsible for actually applying + selection. However, **chain** is not responsible for actually applying transactions and receipts. -* Runtime. If chain selects the _order_ of transactions, Runtime applies - transaction to the state. Chain guarantees that everyone agrees on the order - and content of transactions, and Runtime guarantees that each transaction is +* **Runtime**. If **chain** selects the _order_ of transactions, **Runtime** applies + transaction to the state. **Chain** guarantees that everyone agrees on the order + and content of transactions, and **Runtime** guarantees that each transaction is fully deterministic. It follows that everyone agrees on the "current state" of the blockchain. Some transactions are as simple as "transfer X tokens from Alice to Bob". But a much more powerful class of transactions is supported: "run this arbitrary WebAssembly code in the context of the current state of the chain". Running such "smart contract" transactions securely and - efficiently is a major part of what runtime does. Today, runtime uses a JIT + efficiently is a major part of what **Runtime** does. Today, **Runtime** uses a JIT compiler to do that. -* Storage. Storage is more of a cross-cutting concern, than an isolated +* **Storage**. **Storage** is more of a cross-cutting concern, than an isolated component. Many parts of a node want to durably persist various bits of state to disk. One notable case is the logical state of the blockchain, and, in - particular, data associated with each account. Logically, the state of account + particular, data associated with each account. Logically, the state of an account on a chain is a key-value map: `HashMap, Vec>`. But there is a twist: it should be possible to provide a succinct proof that a particular key indeed holds a particular value. To allow that internally the state is implemented as a persistent (in both senses, "functional" and "on disk") merkle-patricia trie. -* Parameter Estimator. One kind of transaction we support is "run this +* **Parameter Estimator**. One kind of transaction we support is "run this arbitrary, Turing-complete computation". To protect from a `loop {}` - transaction halting the whole network, runtime implements resource limiting: + transaction halting the whole network, **Runtime** implements resource limiting: each transaction runs with a certain finite amount of "gas", and each - operation costs a certain amount of gas to perform. Parameter estimator is - essentially a set of benchmark used to estimate relative gas costs of + operation costs a certain amount of gas to perform. **Parameter estimator** is + essentially a set of benchmarks used to estimate relative gas costs of various operations. ## Entry Points @@ -113,7 +113,7 @@ contract with the outside world -- the peer-to-peer network. `Runtime::apply` in the `runtime` crate is the entry point for transaction processing logic. This is where state transitions actually happen, after chain -decided that, according to distributed consensus, which transitions need to +decided, according to distributed consensus, which transitions need to happen. ## Code Map @@ -141,11 +141,11 @@ This crate contains most of the chain logic (consensus, block processing, etc). happens. **Architecture Invariant**: interface between chain and runtime is defined by -`RuntimeAdapter`. All invocations of runtime goes through `RuntimeAdapter` +`RuntimeAdapter`. All invocations of runtime go through `RuntimeAdapter` **State update** -The blockchain state can be changed in the following two ways: +The blockchain state of a node can be changed in the following two ways: * Applying a chunk. This is how the state is normally updated: through `Runtime::apply`. @@ -181,6 +181,7 @@ orchestrates all the communications from network to other components and from other components to network. `Peer` is responsible for low-level network communications from and to a given peer. Peer manager runs in one thread while each `Peer` runs in its own thread. + **Architecture Invariant**: Network communicates to `Client` through `NetworkClientMessages` and to `ViewClient` through `NetworkViewClientMessages`. @@ -192,7 +193,7 @@ Conversely, `Client` and `ViewClient` communicates to network through This crate is responsible for determining validators and other epoch related information such as epoch id for each epoch. -Note: `EpochManager` is constructed in `NightshadeRuntime` rather than in +**Note**: `EpochManager` is constructed in `NightshadeRuntime` rather than in `Chain`, partially because we had this idea of making epoch manager a smart contract. @@ -210,7 +211,7 @@ the other hand, are sent to `ClientActor` for further processing. This crate contains the main entry point to runtime -- `Runtime::apply`. This function takes `ApplyState`, which contains necessary information passed from -chain to runtime, and a list of `SignedTransaction` and a list of `Receipt`, and +chain to runtime, a list of `SignedTransaction` and a list of `Receipt`, and returns a `ApplyResult`, which includes state changes, execution outcomes, etc. **Architecture Invariant**: The state update is only finalized at the end of @@ -244,10 +245,11 @@ contracts on NEAR. As mentioned before, `neard` is the crate that contains that main entry points. All the actors are spawned in `start_with_config`. It is also worth noting that `NightshadeRuntime` is the struct that implements `RuntimeAdapter`. + ### `core/store/src/db.rs` -This file contains schema (DBCol) of our internal RocksDB storage - a good +This file contains the schema (DBCol) of our internal RocksDB storage - a good starting point when reading the code base. ## Cross Cutting Concerns @@ -262,12 +264,12 @@ more information on the usage. ### Testing Rust has built-in support for writing unit tests by marking functions -with `#[test]` directive. Take full advantage of that! Testing not -only confirms that what was written works the way it was intended but -also help during refactoring since the caught unintended behaviour +with the `#[test]` directive. Take full advantage of that! Testing not +only confirms that what was written works the way it was intended to but +also helps during refactoring since it catches unintended behaviour changes. -Not all tests are created equal though and while some can need only +Not all tests are created equal though and while some may only need milliseconds to run, others may run for several seconds or even minutes. Tests that take a long time should be marked as such with an `expensive_tests` feature, for example: @@ -298,6 +300,6 @@ For more details regarding nightly tests see `nightly/README.md`. Note that what counts as a slow test isn’t exactly defined as of now. If it takes just a couple seconds than it’s probably fine. Anything -slower should probably be classified as expensive test. In +slower should probably be classified as an expensive test. In particular, if libtest complains the test takes more than 60 seconds -than it definitely is. +than it definitely is and expensive test. diff --git a/docs/architecture/how/README.md b/docs/architecture/how/README.md index 15d3c1bb9b8..3677957e61b 100644 --- a/docs/architecture/how/README.md +++ b/docs/architecture/how/README.md @@ -13,37 +13,37 @@ implemented using an [actor framework](https://en.wikipedia.org/wiki/Actor_model) called [actix](https://docs.rs/actix). -**Note:** Using actix was decided in the early days of the implementation of +**Note**: Using actix was decided in the early days of the implementation of nearcore and by no means represents our confidence in actix. On the contrary, we have noticed a number of issues with actix and are considering implementing an actor framework in house. There are several important actors in neard: -* `PeerActor` Each peer is represented by one peer actor and runs in a separate +* `PeerActor` - Each peer is represented by one peer actor and runs in a separate thread. It is responsible for sending messages to and receiving messages from a given peer. After `PeerActor` receives a message, it will route it to `ClientActor`, `ViewClientActor`, or `PeerManagerActor` depending on the type of the message. -* `PeerManagerActor` Peer Manager is responsible for receiving messages to send +* `PeerManagerActor` - Peer Manager is responsible for receiving messages to send to the network from either `ClientActor` or `ViewClientActor` and routing them to - the right `PeerActor` to send the bytes over the wire. It is also responsible to - handle some types of network messages received and routed through `PeerActor`. + the right `PeerActor` to send the bytes over the wire. It is also responsible for + handling some types of network messages received and routed through `PeerActor`. For the purpose of this document, we only need to know that `PeerManagerActor` - handles `RoutedMessage`s. For `RoutedMessage`s, peer manager would decide whether - they should be routed to `ClientActor` or `ViewClientActor`. + handles `RoutedMessage`s. Peer manager would decide whether the `RoutedMessage`s + should be routed to `ClientActor` or `ViewClientActor`. -* `ClientActor` Client actor is the “core” of neard. It contains all the main +* `ClientActor` - Client actor is the “core” of neard. It contains all the main logic including consensus, block and chunk processing, state transition, garbage collection, etc. Client actor is single threaded. -* `ViewClientActor` View client actor can be thought of as a read-only interface - to client. It only accesses data stored in a node’s storage and does not mutate +* `ViewClientActor` - View client actor can be thought of as a read-only interface + to **client**. It only accesses data stored in a node’s storage and does not mutate any state. It is used for two purposes: - * Answering rpc requests by fetching the relevant piece of data from storage - * Handle some network requests that do not require any changes to the + * Answering RPC requests by fetching the relevant piece of data from storage. + * Handling some network requests that do not require any changes to the storage, such as header sync, state sync, and block sync requests. `ViewClientActor` runs in four threads by default but this number is configurable. @@ -74,13 +74,14 @@ do today): not a validator node, it won’t have any chunk parts and therefore won’t have the chunks available. If the node is a validator node, it may already have chunk parts through chunk parts forwarding from other nodes and therefore may - have already reconstructed some chunks. Anyways, if the node doesn’t have all + have already reconstructed some chunks. Regardless, if the node doesn’t have all chunks for all shards, it will request them from peers by parts. 3. The chunk requests are sent and the node waits for enough chunk parts to be received to reconstruct the chunks. For each chunk, 1/3 of all the parts - (100) is sufficient to reconstruct a chunk. In the meantime, if new blocks - arrive, they will be put into a `OrphanPool`, waiting to be processed. If a - chunk part request is not responded to within `chunk_request_retry_period`, + + (100) is sufficient to reconstruct a chunk. If new blocks arrive while waiting + for chunk parts, they will be put into a `OrphanPool`, waiting to be processed. + If a chunk part request is not responded to within `chunk_request_retry_period`, which is set to 400ms by default, then a request for the same chunk part would be sent again. 4. After all chunks are reconstructed, the node processes the current block by @@ -88,8 +89,8 @@ do today): update the head according to the fork choice rule, which only looks at block height. In other words, if the newly processed block is of higher height than the current head of the node, the head is updated. -5. The node checks whether any blocks in the `OrphanPool` is ready to be - processed in a BFS manner and processes all of them until none can be +5. The node checks whether any blocks in the `OrphanPool` are ready to be + processed in a BFS order and processes all of them until none can be processed any more. Note that a block is put into the `OrphanPool` if and only if its previous block is not accepted. 6. Upon acceptance of a block, the node would check whether it needs to run @@ -115,7 +116,7 @@ peer. Once `ClientActor` realizes that it is more than `sync_height_threshold` to sync. The synchronization process is done in three steps: 1. Header sync. The node first identifies the headers it needs to sync through a - [`get_locator`](https://github.com/near/nearcore/blob/279044f09a7e6e5e3f26db4898af3655dae6eda6/chain/client/src/sync.rs#L332) + [`get_locator`](https://github.com/near/nearcore/blob/279044f09a7e6e5e3f26db4898af3655dae6eda6/chain/*client/src/sync.rs#L332) calculation. This is essentially an exponential backoff computation that tries to identify commonly known headers between the node and its peers. Then it would request headers from different peers, at most @@ -160,39 +161,40 @@ to sync. The synchronization process is done in three steps: runs again if head has changed (progress is made) or if a timeout (which is set to 2s) has happened. -**Note:** when a block is received and its height is no more than 500 + the +**Note**: when a block is received and its height is no more than 500 + the node’s current head height, then the node would request its previous block automatically. This is called orphan sync and helps to speed up the syncing process. If, on the other hand, the height is more than 500 + the node’s current head height, the block is simply dropped. + ## How `ClientActor` works ClientActor has some periodically running routines that are worth noting: * [Doomslug - timer](https://github.com/near/nearcore/blob/fa78002a1b4119e5efe277c3073b3f333f451ffc/chain/client/src/client_actor.rs#L1198). + timer](https://github.com/near/nearcore/blob/fa78002a1b4119e5efe277c3073b3f333f451ffc/chain/client/src/client_actor.rs#L1198) - This routine runs every `doosmslug_step_period` (set to 100ms by default) and updates consensus information. If the node is a validator node, it also sends approvals when necessary. * [Block - production](https://github.com/near/nearcore/blob/fa78002a1b4119e5efe277c3073b3f333f451ffc/chain/client/src/client_actor.rs#L991). + production](https://github.com/near/nearcore/blob/fa78002a1b4119e5efe277c3073b3f333f451ffc/chain/client/src/client_actor.rs#L991) - This routine runs every `block_production_tracking_delay` (which is set to 100ms by default) and checks if the node should produce a block. * [Log - summary](https://github.com/near/nearcore/blob/fa78002a1b4119e5efe277c3073b3f333f451ffc/chain/client/src/client_actor.rs#L1790). + summary](https://github.com/near/nearcore/blob/fa78002a1b4119e5efe277c3073b3f333f451ffc/chain/client/src/client_actor.rs#L1790) - Prints a log line that summarizes block rate, average gas used, the height of - the node, etc every 10 seconds. + the node, etc. every 10 seconds. * [Resend chunk - requests](https://github.com/near/nearcore/blob/fa78002a1b4119e5efe277c3073b3f333f451ffc/chain/chunks/src/lib.rs#L910). + requests](https://github.com/near/nearcore/blob/fa78002a1b4119e5efe277c3073b3f333f451ffc/chain/chunks/src/lib.rs#L910) - This routine runs every `chunk_request_retry_period` (which is set to 400ms). It resends the chunk part requests for those that are not yet responded to. -* [Sync](https://github.com/near/nearcore/blob/fa78002a1b4119e5efe277c3073b3f333f451ffc/chain/client/src/client_actor.rs#L1629). +* [Sync](https://github.com/near/nearcore/blob/fa78002a1b4119e5efe277c3073b3f333f451ffc/chain/client/src/client_actor.rs#L1629) - This routine runs every `sync_step_period` (which is set to 10ms by default) - and checks whether the node needs to sync from its peers as well as actually + and checks whether the node needs to sync from its peers and, if needed, also starts the syncing process. * [Catch - up](https://github.com/near/nearcore/blob/fa78002a1b4119e5efe277c3073b3f333f451ffc/chain/client/src/client_actor.rs#L1581). + up](https://github.com/near/nearcore/blob/fa78002a1b4119e5efe277c3073b3f333f451ffc/chain/client/src/client_actor.rs#L1581) - This routine runs every `catchup_step_period` (which is set to 100ms by default) and runs the catch up process. This only applies if a node validates shard A in epoch X and is going to validate a different shard B in epoch X+1. diff --git a/docs/architecture/how/cross-shard.md b/docs/architecture/how/cross-shard.md index ab6f35ae29a..d57ec322ce5 100644 --- a/docs/architecture/how/cross-shard.md +++ b/docs/architecture/how/cross-shard.md @@ -1,12 +1,12 @@ # Cross shard transactions - deep dive In this article, we'll look deeper into how cross-shard transactions are working -on the simple example of user 'shard0' transfering money to user 'shard1'. +on the simple example of user `shard0` transfering money to user `shard1`. These users are on separate shards (`shard0` is on shard 0 and `shard1` is on shard 1). -Imagine, we call the following command line: +Imagine, we run the following command in the command line: ```console $ NEAR_ENV=local near send shard0 shard1 500 @@ -15,10 +15,10 @@ $ NEAR_ENV=local near send shard0 shard1 500 What happens under the hood? How is this transaction changed into receipts and processed by near? - ## From Explorer perspective -If you look at simple token transfer in explorer (for example: -), + +If you look at a simple token transfer in explorer +([example](https://explorer.testnet.near.org/transactions/6cNJpNKWP55YrxrgRzc2gi6BM91fo3V6n2vVAo41MvZv)), you can see that it is broken into three separate sections: * convert transaction into receipt ( executed in block B ) @@ -31,33 +31,32 @@ Let's take a deeper look. ## Internal perspective (Transactions & Receipts) -One important thing to remember, is that NEAR is sharded - so in all our +One important thing to remember is that NEAR is sharded - so in all our designs, we have to assume that each account is on a separate shard. So that the fact that some of them are colocated doesn't give any advantage. - ### Step 1 - Transaction -This is the part that we receive from the user (SignedTransaction) - it has 3 + +This is the part which we receive from the user (`SignedTransaction`) - it has 3 parts: * signer (account + key) who signed the transaction * receiver (in which account context should we execute this) * payload - a.k.a Actions to execute. - As the first step, we want to change this transaction into a Receipt (a.k.a 'internal' message) - but before doing that, we must verify that: * the message signature matches (that is - that this message was actually signed by this key) -* that this key is authorized to act on behalf that account (so it is a full +* that this key is authorized to act on behalf of that account (so it is a full access key to this account - or a valid fuction key). -The last point above means, that we MUST execute this Transaction to Receipt +The last point above means, that we MUST execute this (Transaction to Receipt) transition within the shard that the `signer` belongs to (as other shards don't know the state that belongs to signer - so they don't know which keys it has). -So actually if we look inside the chunk 0 (where `shard0` belongs to) at block +So actually if we look inside the chunk 0 (where `shard0` belongs) at block B, we'll see the transaction: ``` @@ -124,15 +123,16 @@ Chunk: Ok( ) ``` -**Side note:** When we're changing the transaction into receipt, we also use +**Side note**: When we're changing the transaction into a receipt, we also use this moment to deduct prepaid gas fees and transfered tokens from the 'signer' account. The details on how much gas is charged etc will be in a separate article. + ## Step 2 - cross shard receipt -After transaction was changed into the receipt, this receipt must now be sent to -the shard where `receiver` is (in our example `shard1` is on shard 1). +After transaction was changed into a receipt, this receipt must now be sent to +the shard where the `receiver` is (in our example `shard1` is on shard 1). We can actually see this in the chunk of the next block: @@ -204,12 +204,12 @@ Chunk: Ok( ) ``` - -**Side comment:** notice that receipt itself no longer has a `signer` field, but +**Side comment**: notice that the receipt itself no longer has a `signer` field, but a `predecessor_id` one. -Such receipt is sent to the destination shard (we'll explain this process in a +Such a receipt is sent to the destination shard (we'll explain this process in a separate article) where it can be executed. + ## 3. Gas refund. @@ -286,11 +286,12 @@ Chunk: Ok( ) ``` -Such gas refunds receipts are a little bit special - as we'll set the -predecessor_id to be `system` - but the receiver is what we expect (`shard0` -account). (`system` is a special account that doesn't really belong to any shard -- as you can see in this example, the receipt was created within shard 1) +Such gas refund receipts are a little bit special - as we'll set the +`predecessor_id` to be `system` - but the receiver is what we expect (`shard0` +account). +**Note**: `system` is a special account that doesn't really belong to any shard. +As you can see in this example, the receipt was created within shard 1. So putting it all together would look like this: @@ -307,10 +308,12 @@ We'll explain it more in the next section. # Advanced: What's actually going on? -As you could have read in [tx_receipts.md] - the 'receipts' field in chunk is -actually representing 'outgoing' receipts from previous block. +As you could have read in [Transactions And Receipts](./tx_receipts.md) - the +'receipts' field in the chunk is actually representing 'outgoing' receipts +from the previous block. So our image should look more like this: + ![image](https://user-images.githubusercontent.com/91919554/200621066-a5d06f2d-ff43-44ce-a52b-47dc44d6f8ab.png) In this example, the black boxes are representing the 'processing' of the chunk, @@ -319,7 +322,7 @@ and red arrows are cross-shard communication. So when we process Shard 0 from block 1676, we read the transation, and output the receipt - which later becomes the input for shard 1 in block 1677. -But you might still be wondering -- so why didn't we add the Receipt(transfer) +But you might still be wondering - so why didn't we add the Receipt (transfer) to the list of receipts of shard0 1676? That's because the shards & blocks are set BEFORE we do any computation. So the @@ -327,12 +330,13 @@ more correct image would look like this: ![image](https://user-images.githubusercontent.com/91919554/200621808-1ce78047-6968-4af5-9c2a-805a0f1643fc.png) -Here you can see clearly that chunk processing (black box), is happening AFTER -chunk is set. +Here you can clearly see that chunk processing (black box), is happening AFTER +the chunk is set. In this example, the blue arrows are showing the part where we persist the result (receipt) into next block's chunk. -In the future article, we'll discuss how the actual cross-shard communication + +In a future article, we'll discuss how the actual cross-shard communication works (red arrows) in the picture, and how we could guarantee that a given shard really gets all the red arrows, before is starts processing. diff --git a/docs/architecture/how/epoch.md b/docs/architecture/how/epoch.md index 4addfdc404c..3b882dde8e1 100644 --- a/docs/architecture/how/epoch.md +++ b/docs/architecture/how/epoch.md @@ -4,14 +4,15 @@ This short document will tell you all you need to know about Epochs in NEAR protocol. You can also find additional information about epochs in -https://nomicon.io/BlockchainLayer/EpochManager/ +[nomicon](https://nomicon.io/BlockchainLayer/EpochManager/). ## What is an Epoch? -Epoch is a sequence of consecutive blocks - and within one epoch, the set of -validators is fixed, and validator rotation happens at epoch boundaries. +Epoch is a sequence of consecutive blocks. +Within one epoch, the set of validators is fixed, and validator rotation +happens at epoch boundaries. -Basically almost all the changes that we do - are happening at epoch boundaries: +Basically almost all the changes that we do are happening at epoch boundaries: * sharding changes * protocol version changes @@ -21,12 +22,11 @@ Basically almost all the changes that we do - are happening at epoch boundaries: ## Where does the Epoch Id come from? -EpochId for epoch T+2 is the last hash of the block of epoch T. +`EpochId` for epoch T+2 is the last hash of the block of epoch T. ![image](https://user-images.githubusercontent.com/1711539/195907256-c4b1d956-632c-4c11-aa38-17603b1fcc40.png) - -Situation at genesis is interesting. We have three block: +Situation at genesis is interesting. We have three blocks: dummy ← genesis ← first-block @@ -34,45 +34,47 @@ dummy ← genesis ← first-block Epoch length is set in the genesis config. Currently in mainnet it is set to 43200 blocks: -``` +```json "epoch_length": 43200 ``` + See http://go/mainnet-genesis for more details. This means that each epoch lasts around 15 hours. -Important: sometimes there might be ‘troubles’ on the network, that might result +**Important**: sometimes there might be ‘troubles’ on the network, that might result in epoch lasting a little bit longer (if we cannot get enough signatures on the last blocks of the previous epoch). -You can read specific details on our nomicon page: -https://nomicon.io/BlockchainLayer/EpochManager/Epoch +You can read specific details on our +[nomicon page](https://nomicon.io/BlockchainLayer/EpochManager/Epoch). ## How do we pick the next validators? -TL;DR: in the last block of the epoch T, we look at the accounts that have +**TL;DR**: in the last block of the epoch T, we look at the accounts that have highest stake and we pick them to become validators in **T+2**. We are deciding on validators for T+2 (and not T+1) as we want to make sure that validators have enough time to prepare for block production and validation (they have to download the state of shards etc). -For more info on how we pick validators - please look on nomicon. +For more info on how we pick validators please look at +[nomicon](https://nomicon.io/Economics/Economic#validator-selection). ## Epoch and Sharding Sharding changes happen only on epoch boundary - that’s why many of the requests -(like which shard does my account belong to), require also an epoch_id as a +(like which shard does my account belong to), require also an `epoch_id` as a parameter. -Currently (April 2022) - we don’t have dynamic sharding yet, so whole chain is +As of April 2022 we don’t have dynamic sharding yet, so the whole chain is simply using 4 shards. -How can I get more information about current/previous epochs? +### How can I get more information about current/previous epochs? We don’t show much information about Epochs in Explorer. Today, you can use -state_viewer (if you have access to the network database). +`state_viewer` (if you have access to the network database). At the same time, we’re working on a small debug dashboard, to show you the basic information about past epochs - stay tuned. @@ -81,18 +83,18 @@ basic information about past epochs - stay tuned. ### Where do we store epoch info? -We use couple columns in database to store epoch information. +We use a couple columns in the database to store epoch information: * **ColEpochInfo = 11** - is storing the mapping from EpochId to EpochInfo structure that contains all the details. * **ColEpochStart = 23** - has a mapping from EpochId to the first block height of that epoch. * **ColEpochValidatorInfo = 47** - contains validator statistics (blocks - produced etc) for each epoch. + produced etc.) for each epoch. ### How does epoch info look like? -Here’s the example epoch info from the localnet node. As you can see below, +Here’s the example epoch info from a localnet node. As you can see below, EpochInfo mostly contains information about who is the validator and in which order should they produce the blocks. diff --git a/docs/architecture/how/gc.md b/docs/architecture/how/gc.md index da59ca41289..43937d33b97 100644 --- a/docs/architecture/how/gc.md +++ b/docs/architecture/how/gc.md @@ -18,13 +18,13 @@ Imagine the following chain (with 2 forks) In the pictures below, let’s assume that epoch length is 5 and we keep only 3 epochs (rather than 5 that is currently set in production) - otherwise the image -becomes too large 😉 +becomes too large 😉. -If head is in the middle of the epoch, the **gc_stop** will be set to the first -block of epoch T-2, and tail & fork_tail will be sitting at the last block of +If head is in the middle of the epoch, the `gc_stop` will be set to the first +block of epoch T-2, and `tail` & `fork_tail` will be sitting at the last block of epoch T-3. -(and no GC is happening in this round - as tail is next to gc_stop). +(and no GC is happening in this round - as tail is next to `gc_stop`). ![](https://user-images.githubusercontent.com/1711539/195649850-95dee667-b88b-4ef6-b08c-77a17b8d4ae2.png) @@ -33,15 +33,15 @@ happening in this round: ![](https://user-images.githubusercontent.com/1711539/195649879-e29cc826-dfd8-4cbc-a66d-72e42202d26a.png) -Now interesting things will start happening, once head ‘crosses’ over to the +Now interesting things will start happening once head ‘crosses’ over to the next epoch. -First, the gc_stop will jump to the beginning of the next epoch. +First, the `gc_stop` will jump to the beginning of the next epoch. ![](https://user-images.githubusercontent.com/1711539/195649928-0401b221-b6b3-4986-8931-54fbdd1adda0.png) -Then we’ll start the GC of the forks: by first moving the ‘fork_tail’ to match -the gc_stop and going backwards from there. +Then we’ll start the GC of the forks: by first moving the `fork_tail` to match +the `gc_stop` and going backwards from there. ![](https://user-images.githubusercontent.com/1711539/195649966-dac6a4dd-f04b-4131-887a-58efe89d456a.png) @@ -58,7 +58,7 @@ In order not to do too much in one go, we’d only remove up to 2 block in each run (that happens after each head update). Now, the forks are gone, so we can proceed with GCing of the blocks from -canonical chain: +the canonical chain: ![](https://user-images.githubusercontent.com/1711539/195650101-dc6953a7-0d55-4db8-a78b-6a52310410b2.png) @@ -66,7 +66,7 @@ Same as before, we’d remove up to 2 blocks in each run: ![](https://user-images.githubusercontent.com/1711539/195650127-b30865e1-d9c1-4950-8607-67d82a185b76.png) -Until we catchup to the gc_stop. +Until we catch up to the `gc_stop`. -(the original drawings for this document are here: -) +(the original drawings for this document are +[here](https://docs.google.com/document/d/1BiEuJqm4phwQbi-fjzHMZPzDL-94z9Dqkc3XPNnxKJM/edit?usp=sharing)) diff --git a/docs/architecture/how/serialization.md b/docs/architecture/how/serialization.md index 3da46aa5bdb..e474eb25b72 100644 --- a/docs/architecture/how/serialization.md +++ b/docs/architecture/how/serialization.md @@ -30,8 +30,8 @@ message HandshakeFailure { } ``` -Afterwards, such proto file is fed to protoc ‘compiler’ that returns an -auto-generated code (in our case rust code) - that can be directly imported into +Afterwards, such a proto file is fed to protoc ‘compiler’ that returns +auto-generated code (in our case Rust code) - that can be directly imported into your library. The main benefit of protocol buffers is their backwards compatibility (as long @@ -39,19 +39,19 @@ as you adhere to the rules and don’t reuse the same field ids). ## Borsh -Borsh is our custom serializer (), that we use +Borsh is our custom serializer ([link](https://github.com/near/borsh)), that we use mostly for things that have to be hashed. The main feature of Borsh is that, there are no two binary representations that deserialize into the same object. -You can read more on how Borsh serializes the data, by looking at Specification -tab on +You can read more on how Borsh serializes the data, by looking at the Specification +tab on [borsh.io](https://borsh.io). The biggest pitfall/risk of Borsh, is that any change to the structure, might -cause previous data to be no longer parseable. +cause previous data to no longer be parseable. -For example, inserting a new enum ‘in the middle’ : +For example, inserting a new enum ‘in the middle’: ```rust pub enum MyCar { @@ -68,7 +68,6 @@ pub enum MyCar { } ``` - This is especially tricky if we have conditional compilation: ```rust @@ -80,10 +79,10 @@ pub enum MyCar { } ``` -Is such scenario - some of the objects created by binaries with this feature +Is such a scenario - some of the objects created by binaries with this feature enabled, will not be parseable by binaries without this feature. -Also removing and adding fields to structures is also dangerous. +Removing and adding fields to structures is also dangerous. Basically - the only ‘safe’ thing that you can do with Borsh - is add a new Enum value at the end. @@ -101,7 +100,6 @@ Borsh - for things that we hash (and currently also for all the things that we store on disk - but we might move to proto with this in the future). Look for BorshSerialize/BorshDeserialize - ## Questions ### Why don’t you use JSON for everything ? @@ -138,23 +136,23 @@ could lead to some programmatic bugs. ## Advanced section - RawTrieNode But there is one more place in the code, where we use a ‘custom’ encoding (very -similar to Borsh, but a little different): RawTrieNode +similar to Borsh, but a little different): RawTrieNode. -If you look into store/src/trie/mod.rs, you’ll be able to find a method called ‘encode_into’: +If you look into store/src/trie/mod.rs, you’ll be able to find a method called `encode_into`: ```rust - fn encode_into(&self, out: &mut Vec) { - // size in state_parts = size + 8 for RawTrieNodeWithSize + 8 for borsh vector length - match &self { - // size <= 1 + 4 + 4 + 32 + key_length + value_length - RawTrieNode::Leaf(key, value_length, value_hash) => { - out.push(LEAF_NODE); - out.extend((key.len() as u32).to_le_bytes()); - out.extend(key); - out.extend((*value_length as u32).to_le_bytes()); - out.extend(value_hash.as_bytes()); - } - //... more code +fn encode_into(&self, out: &mut Vec) { + // size in state_parts = size + 8 for RawTrieNodeWithSize + 8 for borsh vector length + match &self { + // size <= 1 + 4 + 4 + 32 + key_length + value_length + RawTrieNode::Leaf(key, value_length, value_hash) => { + out.push(LEAF_NODE); + out.extend((key.len() as u32).to_le_bytes()); + out.extend(key); + out.extend((*value_length as u32).to_le_bytes()); + out.extend(value_hash.as_bytes()); + } + //... more code ``` which is responsible for generating this custom encoding. @@ -183,6 +181,6 @@ Borsh: ``` [8 bits - 0 or 1][32 bytes child][8 bits 0 or 1][8 bits ] -[1][0x11][0][1]][0x11][0][0]... +[1][0x11][0][1][0x11][0][0]... // Total size: 16 + 32 + 32 = 80 bytes ``` diff --git a/docs/architecture/how/sync.md b/docs/architecture/how/sync.md index 397b229418b..14dd8630780 100644 --- a/docs/architecture/how/sync.md +++ b/docs/architecture/how/sync.md @@ -20,7 +20,7 @@ have to validate the chunks from all the shards, and normal nodes mostly also track all the shards as this is default. But in the future - we will have more and more people tracking only a subset of -the shards, so the catchup will be more and more important. +the shards, so the catchup will be increasingly important. ## Sync @@ -58,13 +58,13 @@ We plan to enable it in Q4 2022. ![image](https://user-images.githubusercontent.com/1711539/195892336-cc117c08-d3ad-43f7-9304-3233b25e8bb1.png) -notice that in the image above - it is enough to get only ‘last’ header from +Notice that in the image above - it is enough to only get the ‘last’ header from 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 +`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’. The idea of the state sync - is rather than trying to process all the blocks - @@ -79,17 +79,17 @@ history and cannot have any gaps. ![image](https://user-images.githubusercontent.com/1711539/195892354-cf2befed-98e9-40a2-9b81-b5cf738406e0.png) -in this case, we can skip processing transactions that are in the blocks 124 - 128, and start from 129 (after sync state finishes) +In this case, we can skip processing transactions that are in the blocks 124 - 128, and start from 129 (after sync state finishes) ### 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, -hoping to catchup with the chain. +hoping to catch up with the chain. -Block sync will request up to 5 (MAX_BLOCK_REQUESTS) blocks at a time - sending -explicit Network BlockRequest for each one. +Block sync will request up to 5 (`MAX_BLOCK_REQUESTS`) blocks at a time - sending +explicit Network BlockRequests for each one. -After response (Block) is received - the code will execute the ‘standard’ path +After the response (Block) is received - the code will execute the ‘standard’ path that tries to add this block to the chain (see section below). ![image](https://user-images.githubusercontent.com/1711539/195892370-b177228b-2520-486a-94fc-67a91978cb58.png) @@ -107,12 +107,12 @@ A node can receive a Block in two ways: in return. (in case of broadcasting, the node will automatically reject any Blocks that are -more than 500 (BLOCK_HORIZON) blocks away from the current HEAD). +more than 500 (`BLOCK_HORIZON`) blocks away from the current HEAD). When a given block is received, the node checks if it can be added to the current chain. -If block’s “parent” (prev_block) is not in the chain yet - the block gets added +If block’s “parent” (`prev_block`) is not in the chain yet - the block gets added to the orphan list. If the parent is already in the chain - we can try to add the block as the head @@ -122,19 +122,19 @@ Before adding the block, we want to download the chunks for the shards that we are tracking - so in many cases, we’ll call `missing_chunks` functions that will try to go ahead and request those chunks. -Note: as the optimization, we’re also sometimes also trying to fetch chunks for +**Note**: as an optimization, we’re also sometimes trying to fetch chunks for the blocks that are in the orphan pool – but only if they are not more than 3 -(NUM_ORPHAN_ANCESTORS_CHECK) blocks away from our head. +(`NUM_ORPHAN_ANCESTORS_CHECK`) blocks away from our head. We also keep a separate job in client_actor that keeps retrying chunk fetching from other nodes if the original request fails. After all the chunks for a given block are received (we have a separate HashMap -that checks if how many chunks are missing for each block) - we’re ready to +that checks how many chunks are missing for each block) - we’re ready to process the block and attach it to the chain. -Afterwards, we look at other entries in the orphan pool - to see if any of them -is the direct descendant of the block that we just added - and if yes, we repeat +Afterwards, we look at other entries in the orphan pool to see if any of them +are a direct descendant of the block that we just added - and if yes, we repeat the process. ## Catchup @@ -144,11 +144,11 @@ the process. Catchup is needed when not all nodes in the network track all shards and nodes can change the shard they are tracking during different epochs. -For example, if a node tracks shard 0 at epoch T and track shard 1 at epoch T+1, +For example, if a node tracks shard 0 at epoch T and tracks shard 1 at epoch T+1, it actually needs to have the state of shard 1 ready before the beginning of -epoch T+1. How we make sure that in the code is that the node will start -downloading state for shard 1 at the beginning of epoch T, and apply blocks -during epoch T to shard 1’s state. Because downloading state can take time, the +epoch T+1. We make sure this happens by making the node start downloading +the state for shard 1 at the beginning of epoch T and applying blocks during +epoch T to shard 1’s state. Because downloading state can take time, the node may have already processed some blocks (for shard 0 at this epoch), so when the state finishes downloading, the node needs to “catch up” processing these blocks for shard 1. @@ -172,11 +172,11 @@ going to care about in epoch T+1 and catching up blocks that have already been applied. When epoch T starts, the node will start downloading states of shards that it -will track for epoch T+1 that it is not tracking already. Downloading happens in +will track for epoch T+1, which it doesn't track already. Downloading happens in a different thread so `ClientActor` can still process new blocks. Before the shard states for epoch T+1 are ready, processing new blocks only applies chunks for the shards that the node is tracking in epoch T. When the shard states for -epoch T+1 finishes downloading, the catchup process needs to reprocess the +epoch T+1 finish downloading, the catchup process needs to reprocess the blocks that have already been processed in epoch T to apply the chunks for the shards in epoch T+1. @@ -184,8 +184,8 @@ In other words, there are three modes for applying chunks and two code paths, either through the normal `process_block` (blue) or through `catchup_blocks` (orange). When `process_block`, either that the shard states for the next epoch are ready, corresponding to `IsCaughtUp` and all shards the node is tracking in -this epoch or will be tracking in the next epoch will be applied, or that the -states are not ready, corresponding to `NotCaughtup` and only the shards for +this, or will be tracking in the next, epoch will be applied, or when the +states are not ready, corresponding to `NotCaughtUp`, then only the shards for this epoch will be applied. When `catchup_blocks`, shards for the next epoch will be applied. @@ -201,7 +201,7 @@ enum ApplyChunksMode { ### How catchup works The catchup process is initiated by `process_block`, where we check if the block -is caught up and if we need to download states. The logic works as follows +is caught up and if we need to download states. The logic works as follows: * For the first block in an epoch T, we check if the previous block is caught up, which signifies if the state of the new epoch is ready. If the previous @@ -226,16 +226,16 @@ The catchup process is implemented through the function `Client::run_catchup`. can be delayed if ClientActor has a lot of messages in its actix queue. Every time `run_catchup` is called, it checks the store to see if there are any -shard states that should be downloaded (iterate_state_sync_infos). If so, it +shard states that should be downloaded (`iterate_state_sync_infos`). If so, it initiates the syncing process for these shards. After the state is downloaded, `run_catchup` will start to apply blocks that need to be caught up. One thing to note is that `run_catchup` is located at `ClientActor`, but -intensive work such as applying state parts and apply blocks is actually +intensive work such as applying state parts and applying blocks is actually offloaded to `SyncJobActor` in another thread, because we don’t want `ClientActor` to be blocked by this. `run_catchup` is simply responsible for scheduling `SyncJobActor` to do the intensive job. Note that `SyncJobActor` is -state-less, it doesn’t have write access to chain. It will return the changes +state-less, it doesn’t have write access to the chain. It will return the changes that need to be made as part of the response to `ClientActor`, and `ClientActor` is responsible for applying these changes. This is to ensure only one thread (`ClientActor`) has write access to the chain state. However, this also adds a @@ -254,7 +254,7 @@ new epoch. This is unnecessary. Second, even though `run_catchup` is scheduled to run every 100ms, the call can be delayed if ClientActor has messages in its actix queue. A better way to do -this is to move the scheduling of run_catchup to check_triggers. +this is to move the scheduling of `run_catchup` to `check_triggers`. Third, because of how `run_catchup` interacts with `SyncJobActor`, `run_catchup` can catch up at most one block every 100 ms. This is because we don’t want to diff --git a/docs/architecture/how/tx_receipts.md b/docs/architecture/how/tx_receipts.md index 628a1a6347a..1e08a1b0bf2 100644 --- a/docs/architecture/how/tx_receipts.md +++ b/docs/architecture/how/tx_receipts.md @@ -1,17 +1,18 @@ -# Transaction, Receipts adn Chunk Surprises +# Transactions, Receipts and Chunk Surprises -We finished the previous article ([Transaction routing](./tx_routing.md)) on the -part, where transaction was successfully added to the soon-to-be block +We finished the previous article ([Transaction routing](./tx_routing.md)) +where a transaction was successfully added to the soon-to-be block producer’s mempool. -In this article, we’ll cover what happens next: how it is changed into a receipt -and executed, potentially creating even more receipts in the process. +In this article, we’ll cover what happens next: +How it is changed into a receipt and executed, potentially creating even +more receipts in the process. First, let’s look at the ‘high level view’: ![image](https://user-images.githubusercontent.com/1711539/198282472-3883dcc1-77ca-452c-b21e-0a7af1435ede.png) -## Transaction vs receipt +## Transaction vs Receipt As you can see from the image above: @@ -21,21 +22,19 @@ outside. **Receipts** are used for ‘internal’ communication (cross shard, cross contract) - they are created by the block/chunk producers. - ## Life of a Transaction If we ‘zoom-in', the chunk producer's work looks like this: ![image](https://user-images.githubusercontent.com/1711539/198282518-cdeb375e-8f1c-4634-842c-6490020ad9c0.png) - ### Step 1: Process Transaction into receipt Once a chunk producer is ready to produce a chunk, it will fetch the transactions from its mempool, check that they are valid, and if so, prepare to process them into receipts. -**Note:** There are additional restrictions (e.g. making sure that we take them in +**Note**: There are additional restrictions (e.g. making sure that we take them in the right order, that we don’t take too many, etc.) - that you can see in nomicon’s [transaction page](https://nomicon.io/ChainSpec/Transactions). @@ -46,25 +45,25 @@ You can see this part in explorer: ### Step 2: Sending receipt to the proper destination Once we have a receipt, we have to send it to the proper destination - by adding -it to the ‘outgoing_receipt’ list, which will be forwarded to the chunk +it to the `outgoing_receipt` list, which will be forwarded to the chunk producers from the next block. -**Note:** There is a special case here - if the sender of the receipt is the -same as the receiver, then the receipt will be added to the ‘local_receipts' +**Note**: There is a special case here - if the sender of the receipt is the +same as the receiver, then the receipt will be added to the `local_receipts` queue and executed in the same block. -### Step 3: When incoming receipt arrives +### Step 3: When an incoming receipt arrives -(Note: this happens in the ‘next’ block) +(**Note**: this happens in the ‘next’ block) -When chunk producer receives the incoming receipt, it will try to execute its +When a chunk producer receives an incoming receipt, it will try to execute its actions (creating accounts, executing function calls etc). Such actions might generate additional receipts (for example a contract might want to call other contracts). All these outputs are added to the outgoing receipt queue to be executed in the next block. -If there is incoming receipt queue is too large to execute in the current chunk, +If the incoming receipt queue is too large to execute in the current chunk, the producer will put the remaining receipts onto the ‘delayed’ queue. ### Step 4: Profit @@ -74,10 +73,10 @@ consider the transaction to be successful. ### [Advanced] But reality is more complex -**Caution:** In the section below, some things are simplified and do not match exactly -to how the current code works. +**Caution**: In the section below, some things are simplified and do not match exactly +how the current code works. -Let’s quickly also check what’s actually inside a Chunk: +Let’s quickly also check what’s inside a Chunk: ```rust pub struct ShardChunkV2 { @@ -95,7 +94,7 @@ This has to do with performance. #### Simple approach -First, let’s imagine how the system would look like, if chunk contained things +First, let’s imagine how the system would look like, if chunks contained things that we’d expect: * list of transactions @@ -108,7 +107,6 @@ before sending the chunk to other validators. ![image](https://user-images.githubusercontent.com/1711539/198282601-383977f1-08dd-45fe-aa19-70556d585034.png) - Once the other validators receive the chunk, they can start their own processing to verify those outgoing receipts/final state - and then do the signing. Only then, can the next chunk producer start creating the next chunk. @@ -119,26 +117,25 @@ While this approach does work, we can do it faster. What if the chunk didn’t contain the ‘output’ state? This changes our ‘mental’ model a little bit, as now when we’re singing the chunk, we’d actually be -verifying the previous chunk - but that’s the topic for the next article (TODO: -add future link to article about signatures and verification). +verifying the previous chunk - but that’s the topic for the next article (to be added). + For now, imagine if the chunk only had: -* list of transactions -* list of incoming receipts +* a list of transactions +* a list of incoming receipts -In such a case, the chunk producer could send the chunk a lot earlier, and +In this case, the chunk producer could send the chunk a lot earlier, and validators (and chunk producer) could do their processing at the same time: - ![image](https://user-images.githubusercontent.com/1711539/198282641-1e728088-6f2b-4cb9-90c9-5eb09304e72a.png) +Now the last mystery: +Why do we have ‘outgoing’ receipts from previous chunks rather than incoming +to the current one? -Now the last mystery is: why do we have ‘outgoing’ receipts from previous chunk -rather than incoming to the current one? - -This is yet another optimization: this way the chunk producer can send out the +This is yet another optimization. This way the chunk producer can send out the chunk a little bit earlier - without having to wait for all the other shards. -But that’s the topic for another article (TODO: add future link to article about -chunk fragments etc) +But that’s a topic for another article (to be added). + diff --git a/docs/architecture/how/tx_routing.md b/docs/architecture/how/tx_routing.md index 3333b1184eb..20b62f89e6d 100644 --- a/docs/architecture/how/tx_routing.md +++ b/docs/architecture/how/tx_routing.md @@ -3,7 +3,7 @@ We all know that transactions are ‘added’ to the chain - but how do they get there? -Hopefully by the end of this article, the image below would totally make sense. +Hopefully by the end of this article, the image below should make total sense. ![image](https://user-images.githubusercontent.com/1711539/196204937-d6828382-16df-42bd-b59b-50eb2e6f07af.png) @@ -22,73 +22,71 @@ pub struct SignedTransaction { } ``` -With such payload, they can go ahead and send it as JSON-RPC request to ANY node -in the system (you can choose between using ‘sync’ or ‘async’ options). +With such a payload, they can go ahead and send it as a JSON-RPC request to ANY +node in the system (they can choose between using ‘sync’ or ‘async’ options). -From now on, you’ll also be able to query the status of the transaction - by +From now on, they’ll also be able to query the status of the transaction - by using the hash of this object. -**Fun fact:** the ‘Transaction’ object also contains some fields to prevent -attacks: like ‘nonce’ to prevent replay attack, and ‘block_hash’ to limit the +**Fun fact**: the `Transaction` object also contains some fields to prevent +attacks: like `nonce` to prevent replay attack, and `block_hash` to limit the validity of the transaction (it must be added within -transaction_validity_period (defined in genesis) blocks of block_hash). +`transaction_validity_period` (defined in genesis) blocks of `block_hash`). ## Step 2: Inside the node -Our transaction has made it to the node in the system - but most of the nodes +Our transaction has made it to a node in the system - but most of the nodes are not validators - which means that they cannot mutate the chain. That’s why the node has to forward it to someone who can - the upcoming validator. -Roughly node does the following steps: +The node, roughly, does the following steps: -* verify transaction’s metadata - check signatures etc (we want to make sure +* verify transaction’s metadata - check signatures etc. (we want to make sure that we don’t forward bogus data) * forward it to the ‘upcoming’ validator - currently we pick the validators that would be a chunk creator in +2, +3, +4 and +8 blocks (this is controlled by - TX_ROUTING_HEIGHT_HORIZON) - and send the transaction to all of them. + `TX_ROUTING_HEIGHT_HORIZON`) - and send the transaction to all of them. ## Step 3: En-route to validator/producer Great, the node knows to send (forward) the transaction to the validator, but how does the routing work? How do we know which peer is hosting a validator? -Each validator is regularly (every config.ttl_account_id_router/2 seconds == 30 +Each validator is regularly (every `config.ttl_account_id_router`/2 seconds == 30 minutes in production) broadcasting so called `AnnounceAccount`, which is basically a pair of `(account_id, peer_id)`, to the whole network. This way each -node knows which peer_id to send the message to. +node knows which `peer_id` to send the message to. Then it asks the routing table about the shortest path to the peer, and sends -the ‘ForwardTx’ message to the peer. +the `ForwardTx` message to the peer. ## Step 4: Chunk producer -When validator receives such forwarded transaction, it double-checks that it is -about to produce the block, and if yes, it adds the transaction to the mempool -(`TransactionPool`) for this shard, when it waits to be picked up when the chunk +When a validator receives such a forwarded transaction, it double-checks that it is +about to produce the block, and if so, it adds the transaction to the mempool +(`TransactionPool`) for this shard, where it waits to be picked up when the chunk is produced. What happens afterwards will be covered in future episodes/articles. - - ## Additional notes: ### Transaction being added multiple times -But such approach means, that we’re forwarding the same transaction to multiple +But such a approach means, that we’re forwarding the same transaction to multiple validators (currently 4) - so can it be added multiple times? -No: remember that transaction has a concrete hash - that is used as a global +No. Remember that a transaction has a concrete hash which is used as a global identifier. If the validator sees that the transaction is present in the chain, it removes it from its local mempool. ### Can transaction get lost? -Yes - they can and they do. Sometimes node doesn’t have a path to a given -validator or it didn’t receive an AnnouceAccount for it, so it doesn’t know +Yes - they can and they do. Sometimes a node doesn’t have a path to a given +validator or it didn’t receive an `AnnouceAccount` for it, so it doesn’t know where to forward the message. And if this happens to all 4 validators that we -try to send to - then the message can be silently dropped. +try to send to, then the message can be silently dropped. We’re working on adding some monitoring to see how often this happens. From ddd9f886a9e00dd39556b4d907c06f9ed3e5ce42 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Tue, 22 Nov 2022 14:13:56 +0000 Subject: [PATCH 022/188] fix: also update testnet ed25519 gas parameters (#8101) I completely forgot to update it in #8031 --- core/primitives/res/runtime_configs/parameters_testnet.txt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/primitives/res/runtime_configs/parameters_testnet.txt b/core/primitives/res/runtime_configs/parameters_testnet.txt index d8223613ecf..0cad1863c3b 100644 --- a/core/primitives/res/runtime_configs/parameters_testnet.txt +++ b/core/primitives/res/runtime_configs/parameters_testnet.txt @@ -90,9 +90,8 @@ wasm_keccak512_byte: 36_649_701 wasm_ripemd160_base: 853_675_086 wasm_ripemd160_block: 680_107_584 wasm_ecrecover_base: 3_365_369_625_000 -# both ed25519_verify_base and ed25519_verify_byte have NON-FINAL numbers (needs fine tuning) -wasm_ed25519_verify_base: 40_311_888_867 -wasm_ed25519_verify_byte: 423_978_605 +wasm_ed25519_verify_base: 210_000_000_000 +wasm_ed25519_verify_byte: 9_000_000 wasm_log_base: 3_543_313_050 wasm_log_byte: 13_198_791 wasm_storage_write_base: 64_196_736_000 From c91ac5db104bdc36c74b59a62f1e42361063a4cc Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Tue, 22 Nov 2022 15:18:00 +0000 Subject: [PATCH 023/188] doc: move estimator guide to workflows (#8103) Since we now have a workflow chapter, the section on how to run the estimator belong there. Also added a pointer to #8031 as an example for gas parameter analysis. --- docs/SUMMARY.md | 1 + docs/architecture/gas/estimator.md | 117 ++++---------------- docs/practices/workflows/gas_estimations.md | 85 ++++++++++++++ 3 files changed, 109 insertions(+), 94 deletions(-) create mode 100644 docs/practices/workflows/gas_estimations.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index a6c0d0a890a..82ecffa3f1d 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -28,6 +28,7 @@ - [Workflows](./practices/workflows/README.md) - [Run a Node](./practices/workflows/run_a_node.md) - [Deploy a Contract](./practices/workflows/deploy_a_contract.md) + - [Run Gas Estimations](./practices/workflows/gas_estimations.md) - [Code Style](./practices/style.md) - [Documentation](./practices/docs.md) - [Tracking Issues](./practices/tracking_issues.md) diff --git a/docs/architecture/gas/estimator.md b/docs/architecture/gas/estimator.md index 8de1dcbd509..ea3b751c78a 100644 --- a/docs/architecture/gas/estimator.md +++ b/docs/architecture/gas/estimator.md @@ -20,100 +20,9 @@ clients. The estimator code is part of the nearcore repository in the directory [runtime/runtime-params-estimator](https://github.com/near/nearcore/tree/master/runtime/runtime-params-estimator). -## Running the Estimator - -Type this in your console to quickly run estimations on a couple of action costs. - -```bash -cargo run -p runtime-params-estimator --features required -- \ - --accounts-num 20000 --additional-accounts-num 20000 \ - --iters 3 --warmup-iters 1 --metric time \ - --costs=ActionReceiptCreation,ActionTransfer,ActionCreateAccount,ActionFunctionCallBase -``` - -You should get an output like this. - -``` -[elapsed 00:00:17 remaining 00:00:00] Writing into storage ████████████████████ 20000/20000 -ActionReceiptCreation 4_499_673_502_000 gas [ 4.499674ms] (computed in 7.22s) -ActionTransfer 410_122_090_000 gas [ 410.122µs] (computed in 4.71s) -ActionCreateAccount 237_495_890_000 gas [ 237.496µs] (computed in 4.64s) -ActionFunctionCallBase 770_989_128_914 gas [ 770.989µs] (computed in 4.65s) - - -Finished in 40.11s, output saved to: - - /home/you/near/nearcore/costs-2022-11-11T11:11:11Z-e40863c9b.txt -``` - -This shows how much gas a parameter should cost to satisfy the 1ms = 1Tgas rule. -It also shows how much time that corresponds to and how long it took to compute -each of the estimations. - -Note that the above does not produce very accurate results and it can have high -variance as well. It runs an unoptimized binary, the state is small, and the -metric used is wall-clock time which is always prone to variance in hardware and -can be affected by other processes currently running on your system. - -Once your estimation code is ready, it is better to run it with a larger state -and an optimized binary. - -```bash -cargo run --release -p runtime-params-estimator --features required -- \ - --accounts-num 20000 --additional-accounts-num 2000000 \ - --iters 3 --warmup-iters 1 --metric time \ - --costs=ActionReceiptCreation,ActionTransfer,ActionCreateAccount,ActionFunctionCallBase -``` - -You might also want to run a hardware-agnostic estimation using the following -command. It uses `docker` and `qemu` under the hood, so it will be quite a bit -slower. You will need to install `docker` to run this command. - - -```bash -cargo run --release -p runtime-params-estimator --features required -- \ - --accounts-num 20000 --additional-accounts-num 2000000 \ - --iters 3 --warmup-iters 1 --metric icount --docker --full \ - --costs=ActionReceiptCreation,ActionTransfer,ActionCreateAccount,ActionFunctionCallBase -``` - -Note how the output looks a bit different now. The `i`, `r` and `w` values show -instruction count, read IO bytes, and write IO bytes respectively. The IO byte -count is known to be inaccurate. - -``` -+ /host/nearcore/runtime/runtime-params-estimator/emu-cost/counter_plugin/qemu-x86_64 -plugin file=/host/nearcore/runtime/runtime-params-estimator/emu-cost/counter_plugin/libcounter.so -cpu Westmere-v1 /host/nearcore/target/release/runtime-params-estimator --home /.near --accounts-num 20000 --iters 3 --warmup-iters 1 --metric icount --costs=ActionReceiptCreation,ActionTransfer,ActionCreateAccount,ActionFunctionCallBase --skip-build-test-contract --additional-accounts-num 0 --in-memory-db -ActionReceiptCreation 214_581_685_500 gas [ 1716653.48i 0.00r 0.00w] (computed in 6.11s) -ActionTransfer 21_528_212_916 gas [ 172225.70i 0.00r 0.00w] (computed in 4.71s) -ActionCreateAccount 26_608_336_250 gas [ 212866.69i 0.00r 0.00w] (computed in 4.67s) -ActionFunctionCallBase 12_193_364_898 gas [ 97546.92i 0.00r 0.00w] (computed in 2.39s) - - -Finished in 17.92s, output saved to: - - /host/nearcore/costs-2022-11-01T16:27:36Z-e40863c9b.txt -``` - -The difference between the metrics will be discussed in the -section on [Estimation Metrics](#estimation-metrics). - -You should now be all setup for running estimations on your local machine. Also -check `cargo run -p runtime-params-estimator --features required -- --help` for -the list of available options. - -## From Estimations to Parameter Values - -To calculate the final gas parameter values, there is more to be done than just -running a single command. After all, these parameters are part of the protocol -specification. They cannot be changed easily. And setting them to a wrong value -can cause severe system instability. - -Our current strategy is to run estimations -with two different metrics and do so on standardized cloud hardware. The output -is then sanity checked manually by several people. Based on that, the final gas -parameter value is determined. Usually it will be the higher output of the two -metrics rounded up. More details on the process will be added to this document -in due time. +For a practical guide on how to run the estimator, please take a look at +[Running the Estimator](../../practices/workflows/gas_estimations.md) in the +workflows chapter. ## Code Structure The estimator contains a binary and a library module. The @@ -193,6 +102,26 @@ locality and how it can be cached in the OS page cache. But regardless of all these inaccuracies, it can still be useful to compare different implementations both measured using `icount`. +## From Estimations to Parameter Values + +To calculate the final gas parameter values, there is more to be done than just +running a single command. After all, these parameters are part of the protocol +specification. They cannot be changed easily. And setting them to a wrong value +can cause severe system instability. + +Our current strategy is to run estimations +with two different metrics and do so on standardized cloud hardware. The output +is then sanity checked manually by several people. Based on that, the final gas +parameter value is determined. Usually it will be the higher output of the two +metrics rounded up. + +The PR [#8031](https://github.com/near/nearcore/pull/8031) to set the ed25519 +verification gas parameters is a good example for how such an analysis and +report could look. + +More details on the process will be added to this document +in due time. + diff --git a/docs/practices/workflows/gas_estimations.md b/docs/practices/workflows/gas_estimations.md new file mode 100644 index 00000000000..44578719f36 --- /dev/null +++ b/docs/practices/workflows/gas_estimations.md @@ -0,0 +1,85 @@ + +# Running the Estimator + +This workflow describes how to run the gas estimator byzantine-benchmark suite. +To learn about its background and purpose, refer to [Runtime Parameter +Estimator](../../architecture/gas/estimator.md) in the architecture chapter. + +Type this in your console to quickly run estimations on a couple of action costs. + +```bash +cargo run -p runtime-params-estimator --features required -- \ + --accounts-num 20000 --additional-accounts-num 20000 \ + --iters 3 --warmup-iters 1 --metric time \ + --costs=ActionReceiptCreation,ActionTransfer,ActionCreateAccount,ActionFunctionCallBase +``` + +You should get an output like this. + +``` +[elapsed 00:00:17 remaining 00:00:00] Writing into storage ████████████████████ 20000/20000 +ActionReceiptCreation 4_499_673_502_000 gas [ 4.499674ms] (computed in 7.22s) +ActionTransfer 410_122_090_000 gas [ 410.122µs] (computed in 4.71s) +ActionCreateAccount 237_495_890_000 gas [ 237.496µs] (computed in 4.64s) +ActionFunctionCallBase 770_989_128_914 gas [ 770.989µs] (computed in 4.65s) + + +Finished in 40.11s, output saved to: + + /home/you/near/nearcore/costs-2022-11-11T11:11:11Z-e40863c9b.txt +``` + +This shows how much gas a parameter should cost to satisfy the 1ms = 1Tgas rule. +It also shows how much time that corresponds to and how long it took to compute +each of the estimations. + +Note that the above does not produce very accurate results and it can have high +variance as well. It runs an unoptimized binary, the state is small, and the +metric used is wall-clock time which is always prone to variance in hardware and +can be affected by other processes currently running on your system. + +Once your estimation code is ready, it is better to run it with a larger state +and an optimized binary. + +```bash +cargo run --release -p runtime-params-estimator --features required -- \ + --accounts-num 20000 --additional-accounts-num 2000000 \ + --iters 3 --warmup-iters 1 --metric time \ + --costs=ActionReceiptCreation,ActionTransfer,ActionCreateAccount,ActionFunctionCallBase +``` + +You might also want to run a hardware-agnostic estimation using the following +command. It uses `docker` and `qemu` under the hood, so it will be quite a bit +slower. You will need to install `docker` to run this command. + + +```bash +cargo run --release -p runtime-params-estimator --features required -- \ + --accounts-num 20000 --additional-accounts-num 2000000 \ + --iters 3 --warmup-iters 1 --metric icount --docker --full \ + --costs=ActionReceiptCreation,ActionTransfer,ActionCreateAccount,ActionFunctionCallBase +``` + +Note how the output looks a bit different now. The `i`, `r` and `w` values show +instruction count, read IO bytes, and write IO bytes respectively. The IO byte +count is known to be inaccurate. + +``` ++ /host/nearcore/runtime/runtime-params-estimator/emu-cost/counter_plugin/qemu-x86_64 -plugin file=/host/nearcore/runtime/runtime-params-estimator/emu-cost/counter_plugin/libcounter.so -cpu Westmere-v1 /host/nearcore/target/release/runtime-params-estimator --home /.near --accounts-num 20000 --iters 3 --warmup-iters 1 --metric icount --costs=ActionReceiptCreation,ActionTransfer,ActionCreateAccount,ActionFunctionCallBase --skip-build-test-contract --additional-accounts-num 0 --in-memory-db +ActionReceiptCreation 214_581_685_500 gas [ 1716653.48i 0.00r 0.00w] (computed in 6.11s) +ActionTransfer 21_528_212_916 gas [ 172225.70i 0.00r 0.00w] (computed in 4.71s) +ActionCreateAccount 26_608_336_250 gas [ 212866.69i 0.00r 0.00w] (computed in 4.67s) +ActionFunctionCallBase 12_193_364_898 gas [ 97546.92i 0.00r 0.00w] (computed in 2.39s) + + +Finished in 17.92s, output saved to: + + /host/nearcore/costs-2022-11-01T16:27:36Z-e40863c9b.txt +``` + +The difference between the metrics is discussed in the [Estimation +Metrics](../../architecture/gas/estimator.md#estimation-metrics). + +You should now be all setup for running estimations on your local machine. Also +check `cargo run -p runtime-params-estimator --features required -- --help` for +the list of available options. From d162821c4fd02d1c6cbadbbf0c797442eea2f37f Mon Sep 17 00:00:00 2001 From: pompon0 Date: Wed, 23 Nov 2022 14:27:10 +0100 Subject: [PATCH 024/188] moved rate limit to a separate module (#8107) --- chain/network/src/concurrency/demux.rs | 32 ++----------------- chain/network/src/concurrency/mod.rs | 1 + chain/network/src/concurrency/rate.rs | 27 ++++++++++++++++ chain/network/src/concurrency/tests.rs | 7 ++-- chain/network/src/config.rs | 20 ++++++------ .../src/peer_manager/tests/accounts_data.rs | 4 +-- 6 files changed, 47 insertions(+), 44 deletions(-) create mode 100644 chain/network/src/concurrency/rate.rs diff --git a/chain/network/src/concurrency/demux.rs b/chain/network/src/concurrency/demux.rs index f17a819e7ea..251fe5c3a1c 100644 --- a/chain/network/src/concurrency/demux.rs +++ b/chain/network/src/concurrency/demux.rs @@ -5,7 +5,7 @@ //! //! Example usage: //! ` -//! let d = Demux::new(RateLimit(10.,1)); +//! let d = Demux::new(rate::Limit(10.,1)); //! ... //! let res = d.call(arg,|inout| async { //! // Process all inout[i].arg together. @@ -17,6 +17,7 @@ //! of the provided handlers will be executed asynchronously //! (other handlers will be dropped). //! +use crate::concurrency::rate; use crate::time; use futures::future::BoxFuture; use futures::FutureExt; @@ -48,33 +49,6 @@ where Box::new(move |a: Arg| self(a).boxed()) } } -/// Config of a rate limiter algorithm, which behaves like a semaphore -/// - with maximal capacity `burst` -/// - with a new ticket added automatically every 1/qps seconds (qps stands for "queries per -/// second") -/// In case of large load, semaphore will be empty most of the time, -/// letting through requests at frequency `qps`. -/// In case a number of requests come after a period of inactivity, semaphore will immediately -/// let through up to `burst` requests, before going into the previous mode. -#[derive(Copy, Clone)] -pub struct RateLimit { - pub burst: u64, - pub qps: f64, -} - -impl RateLimit { - // TODO(gprusak): consider having a constructor for RateLimit which enforces validation - // and getters for fields, so that they cannot be modified after construction. - pub fn validate(&self) -> anyhow::Result<()> { - if self.qps <= 0. { - anyhow::bail!("qps has to be >0"); - } - if self.burst <= 0 { - anyhow::bail!("burst has to be >0"); - } - Ok(()) - } -} /// A demux handler should be in practice of type `[Arg; n]` → `[Res; n]` for /// arbitrary `n`. We approximate that by a function `Vec` → `Vec`. @@ -129,7 +103,7 @@ impl Demux { // Spawns a subroutine performing the demultiplexing. // Panics if rl is not valid. - pub fn new(rl: RateLimit) -> Demux { + pub fn new(rl: rate::Limit) -> Demux { rl.validate().unwrap(); let (send, mut recv): (Stream, _) = mpsc::unbounded_channel(); // TODO(gprusak): this task should be running as long as Demux object exists. diff --git a/chain/network/src/concurrency/mod.rs b/chain/network/src/concurrency/mod.rs index 551b7cfbdb9..909e82b32f2 100644 --- a/chain/network/src/concurrency/mod.rs +++ b/chain/network/src/concurrency/mod.rs @@ -1,6 +1,7 @@ pub mod arc_mutex; pub mod atomic_cell; pub mod demux; +pub mod rate; pub mod rayon; #[cfg(test)] diff --git a/chain/network/src/concurrency/rate.rs b/chain/network/src/concurrency/rate.rs new file mode 100644 index 00000000000..b03d9a4e313 --- /dev/null +++ b/chain/network/src/concurrency/rate.rs @@ -0,0 +1,27 @@ +/// Config of a rate limiter algorithm, which behaves like a semaphore +/// - with maximal capacity `burst` +/// - with a new ticket added automatically every 1/qps seconds (qps stands for "queries per +/// second") +/// In case of large load, semaphore will be empty most of the time, +/// letting through requests at frequency `qps`. +/// In case a number of requests come after a period of inactivity, semaphore will immediately +/// let through up to `burst` requests, before going into the previous mode. +#[derive(Copy, Clone)] +pub struct Limit { + pub burst: u64, + pub qps: f64, +} + +impl Limit { + // TODO(gprusak): consider having a constructor for RateLimit which enforces validation + // and getters for fields, so that they cannot be modified after construction. + pub fn validate(&self) -> anyhow::Result<()> { + if self.qps <= 0. { + anyhow::bail!("qps has to be >0"); + } + if self.burst <= 0 { + anyhow::bail!("burst has to be >0"); + } + Ok(()) + } +} diff --git a/chain/network/src/concurrency/tests.rs b/chain/network/src/concurrency/tests.rs index 0b687c754ee..f4ebb33241e 100644 --- a/chain/network/src/concurrency/tests.rs +++ b/chain/network/src/concurrency/tests.rs @@ -1,5 +1,6 @@ use crate::concurrency::arc_mutex::ArcMutex; use crate::concurrency::demux; +use crate::concurrency::rate; use crate::concurrency::rayon; // drop a trivial future without completion => panic (in debug mode at least). @@ -17,7 +18,7 @@ async fn must_complete_ok() { #[tokio::test] async fn test_demux() { - let demux = demux::Demux::new(demux::RateLimit { qps: 50., burst: 1 }); + let demux = demux::Demux::new(rate::Limit { qps: 50., burst: 1 }); for _ in 0..5 { let mut handles = vec![]; for i in 0..1000 { @@ -40,7 +41,7 @@ async fn test_demux() { fn demux_runtime_dropped_before_call() { let r1 = tokio::runtime::Runtime::new().unwrap(); let r2 = tokio::runtime::Runtime::new().unwrap(); - let demux = r1.block_on(async { demux::Demux::new(demux::RateLimit { qps: 1., burst: 1000 }) }); + let demux = r1.block_on(async { demux::Demux::new(rate::Limit { qps: 1., burst: 1000 }) }); drop(r1); let call = demux.call(0, |is: Vec| async { is }); assert_eq!(Err(demux::ServiceStoppedError), r2.block_on(call)); @@ -50,7 +51,7 @@ fn demux_runtime_dropped_before_call() { fn demux_runtime_dropped_during_call() { let r1 = tokio::runtime::Runtime::new().unwrap(); let r2 = tokio::runtime::Runtime::new().unwrap(); - let demux = r1.block_on(async { demux::Demux::new(demux::RateLimit { qps: 1., burst: 1000 }) }); + let demux = r1.block_on(async { demux::Demux::new(rate::Limit { qps: 1., burst: 1000 }) }); // Start the call and pause. let (send, recv) = tokio::sync::oneshot::channel(); diff --git a/chain/network/src/config.rs b/chain/network/src/config.rs index 570d6352453..9202b43d28f 100644 --- a/chain/network/src/config.rs +++ b/chain/network/src/config.rs @@ -1,5 +1,5 @@ use crate::blacklist; -use crate::concurrency::demux; +use crate::concurrency::rate; use crate::network_protocol::PeerAddr; use crate::network_protocol::PeerInfo; use crate::peer_manager::peer_manager_actor::Event; @@ -65,9 +65,6 @@ impl ValidatorConfig { } } -#[derive(Clone)] -pub struct Features {} - #[derive(Clone)] pub struct Tier1 { /// Interval between broacasts of the list of validator's proxies. @@ -132,9 +129,9 @@ pub struct NetworkConfig { /// Whether this is an archival node. pub archive: bool, /// Maximal rate at which SyncAccountsData can be broadcasted. - pub accounts_data_broadcast_rate_limit: demux::RateLimit, + pub accounts_data_broadcast_rate_limit: rate::Limit, /// Maximal rate at which RoutingTableUpdate can be sent out. - pub routing_table_update_rate_limit: demux::RateLimit, + pub routing_table_update_rate_limit: rate::Limit, /// Config of the TIER1 network. pub tier1: Option, @@ -254,8 +251,8 @@ impl NetworkConfig { push_info_period: time::Duration::milliseconds(100), outbound_disabled: false, archive, - accounts_data_broadcast_rate_limit: demux::RateLimit { qps: 0.1, burst: 1 }, - routing_table_update_rate_limit: demux::RateLimit { qps: 0.5, burst: 1 }, + accounts_data_broadcast_rate_limit: rate::Limit { qps: 0.1, burst: 1 }, + routing_table_update_rate_limit: rate::Limit { qps: 0.5, burst: 1 }, tier1: Some(Tier1 { advertise_proxies_interval: time::Duration::minutes(15) }), inbound_disabled: cfg.experimental.inbound_disabled, skip_tombstones: if cfg.experimental.skip_sending_tombstones_seconds > 0 { @@ -319,8 +316,8 @@ impl NetworkConfig { outbound_disabled: false, inbound_disabled: false, archive: false, - accounts_data_broadcast_rate_limit: demux::RateLimit { qps: 100., burst: 1000000 }, - routing_table_update_rate_limit: demux::RateLimit { qps: 100., burst: 1000000 }, + accounts_data_broadcast_rate_limit: rate::Limit { qps: 100., burst: 1000000 }, + routing_table_update_rate_limit: rate::Limit { qps: 100., burst: 1000000 }, tier1: Some(Tier1 { // Interval is very large, so that it doesn't happen spontaneously in tests. // It should rather be triggered manually in tests. @@ -364,6 +361,9 @@ impl NetworkConfig { self.accounts_data_broadcast_rate_limit .validate() .context("accounts_Data_broadcast_rate_limit")?; + self.routing_table_update_rate_limit + .validate() + .context("routing_table_update_rate_limit")?; Ok(VerifiedConfig { node_id: self.node_id(), inner: self }) } } diff --git a/chain/network/src/peer_manager/tests/accounts_data.rs b/chain/network/src/peer_manager/tests/accounts_data.rs index e19d179512b..862de1dea94 100644 --- a/chain/network/src/peer_manager/tests/accounts_data.rs +++ b/chain/network/src/peer_manager/tests/accounts_data.rs @@ -1,4 +1,4 @@ -use crate::concurrency::demux; +use crate::concurrency::rate; use crate::network_protocol::testonly as data; use crate::network_protocol::SyncAccountsData; use crate::peer; @@ -182,7 +182,7 @@ async fn rate_limiting() { let mut pms = vec![]; for _ in 0..n * m { let mut cfg = chain.make_config(rng); - cfg.accounts_data_broadcast_rate_limit = demux::RateLimit { qps: 0.5, burst: 1 }; + cfg.accounts_data_broadcast_rate_limit = rate::Limit { qps: 0.5, burst: 1 }; pms.push( peer_manager::testonly::start( clock.clock(), From 95d1d6c1f0471896d4822ea9c21f3bc840faf0e3 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Wed, 23 Nov 2022 15:47:21 +0100 Subject: [PATCH 025/188] made imports of "tracing" and "actix" across near-network uniform (#8108) --- chain/network/src/peer/peer_actor.rs | 54 ++++++++--------- .../src/peer_manager/network_state/mod.rs | 12 ++-- .../src/peer_manager/peer_manager_actor.rs | 58 ++++++++++--------- 3 files changed, 64 insertions(+), 60 deletions(-) diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index b007697e448..55a4cabc9b9 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -19,7 +19,7 @@ use crate::types::{ BlockInfo, Handshake, HandshakeFailureReason, PeerIdOrHash, PeerMessage, PeerType, ReasonForBan, }; use actix::fut::future::wrap_future; -use actix::{Actor, ActorContext, ActorFutureExt, AsyncContext, Context, Handler, Running}; +use actix::{Actor as _, ActorContext as _, ActorFutureExt as _, AsyncContext as _}; use lru::LruCache; use near_crypto::Signature; use near_o11y::{handler_debug_span, log_assert, pretty, OpenTelemetrySpanExt, WithSpanContext}; @@ -37,7 +37,7 @@ use std::io; use std::net::SocketAddr; use std::sync::atomic::Ordering; use std::sync::Arc; -use tracing::{debug, error, info, warn, Instrument}; +use tracing::Instrument as _; /// Maximum number of messages per minute from single peer. // TODO(#5453): current limit is way to high due to us sending lots of messages during sync. @@ -338,7 +338,7 @@ impl PeerActor { self.send_message_or_log(&msg); } - fn stop(&mut self, ctx: &mut Context, reason: ClosingReason) { + fn stop(&mut self, ctx: &mut actix::Context, reason: ClosingReason) { // Only the first call to stop sets the closing_reason. if self.closing_reason.is_none() { self.closing_reason = Some(reason); @@ -369,7 +369,7 @@ impl PeerActor { ctx: &mut ::Context, handshake: Handshake, ) { - debug!(target: "network", "{:?}: Received handshake {:?}", self.my_node_info.id, handshake); + tracing::debug!(target: "network", "{:?}: Received handshake {:?}", self.my_node_info.id, handshake); let cs = match &self.peer_status { PeerStatus::Connecting(it) => it, _ => panic!("process_handshake called in non-connecting state"), @@ -377,22 +377,22 @@ impl PeerActor { match cs { ConnectingStatus::Outbound { handshake_spec: spec, .. } => { if handshake.protocol_version != spec.protocol_version { - warn!(target: "network", "Protocol version mismatch. Disconnecting peer {}", handshake.sender_peer_id); + tracing::warn!(target: "network", "Protocol version mismatch. Disconnecting peer {}", handshake.sender_peer_id); self.stop(ctx, ClosingReason::HandshakeFailed); return; } if handshake.sender_chain_info.genesis_id != self.network_state.genesis_id { - warn!(target: "network", "Genesis mismatch. Disconnecting peer {}", handshake.sender_peer_id); + tracing::warn!(target: "network", "Genesis mismatch. Disconnecting peer {}", handshake.sender_peer_id); self.stop(ctx, ClosingReason::HandshakeFailed); return; } if handshake.sender_peer_id != spec.peer_id { - warn!(target: "network", "PeerId mismatch. Disconnecting peer {}", handshake.sender_peer_id); + tracing::warn!(target: "network", "PeerId mismatch. Disconnecting peer {}", handshake.sender_peer_id); self.stop(ctx, ClosingReason::HandshakeFailed); return; } if handshake.partial_edge_info.nonce != spec.partial_edge_info.nonce { - warn!(target: "network", "Nonce mismatch. Disconnecting peer {}", handshake.sender_peer_id); + tracing::warn!(target: "network", "Nonce mismatch. Disconnecting peer {}", handshake.sender_peer_id); self.stop(ctx, ClosingReason::HandshakeFailed); return; } @@ -401,7 +401,7 @@ impl PeerActor { if PEER_MIN_ALLOWED_PROTOCOL_VERSION > handshake.protocol_version || handshake.protocol_version > PROTOCOL_VERSION { - debug!( + tracing::debug!( target: "network", version = handshake.protocol_version, "Received connection from node with unsupported PROTOCOL_VERSION."); @@ -416,7 +416,7 @@ impl PeerActor { } let genesis_id = self.network_state.genesis_id.clone(); if handshake.sender_chain_info.genesis_id != genesis_id { - debug!(target: "network", "Received connection from node with different genesis."); + tracing::debug!(target: "network", "Received connection from node with different genesis."); self.send_message_or_log(&PeerMessage::HandshakeFailure( self.my_node_info.clone(), HandshakeFailureReason::GenesisMismatch(genesis_id), @@ -424,7 +424,7 @@ impl PeerActor { return; } if handshake.target_peer_id != self.my_node_info.id { - debug!(target: "network", "Received handshake from {:?} to {:?} but I am {:?}", handshake.sender_peer_id, handshake.target_peer_id, self.my_node_info.id); + tracing::debug!(target: "network", "Received handshake from {:?} to {:?} but I am {:?}", handshake.sender_peer_id, handshake.target_peer_id, self.my_node_info.id); self.send_message_or_log(&PeerMessage::HandshakeFailure( self.my_node_info.clone(), HandshakeFailureReason::InvalidTarget, @@ -433,7 +433,7 @@ impl PeerActor { } // Verify if nonce is sane. if let Err(err) = verify_nonce(&self.clock, handshake.partial_edge_info.nonce) { - debug!(target: "network", nonce=?handshake.partial_edge_info.nonce, my_node_id = ?self.my_node_id(), peer_id=?handshake.sender_peer_id, "bad nonce, disconnecting: {err}"); + tracing::debug!(target: "network", nonce=?handshake.partial_edge_info.nonce, my_node_id = ?self.my_node_id(), peer_id=?handshake.sender_peer_id, "bad nonce, disconnecting: {err}"); self.stop(ctx, ClosingReason::HandshakeFailed); return; } @@ -443,7 +443,7 @@ impl PeerActor { self.network_state.routing_table_view.get_local_edge(&handshake.sender_peer_id) { if last_edge.nonce() >= handshake.partial_edge_info.nonce { - debug!(target: "network", "{:?}: Received too low nonce from peer {:?} sending evidence.", self.my_node_id(), self.peer_addr); + tracing::debug!(target: "network", "{:?}: Received too low nonce from peer {:?} sending evidence.", self.my_node_id(), self.peer_addr); self.send_message_or_log(&PeerMessage::LastEdge(last_edge)); return; } @@ -458,7 +458,7 @@ impl PeerActor { &handshake.sender_peer_id, &handshake.partial_edge_info, ) { - warn!(target: "network", "partial edge with invalid signature, disconnecting"); + tracing::warn!(target: "network", "partial edge with invalid signature, disconnecting"); self.stop(ctx, ClosingReason::Ban(ReasonForBan::InvalidSignature)); return; } @@ -665,7 +665,7 @@ impl PeerActor { ) => { match reason { HandshakeFailureReason::GenesisMismatch(genesis) => { - warn!(target: "network", "Attempting to connect to a node ({}) with a different genesis block. Our genesis: {:?}, their genesis: {:?}", peer_info, self.network_state.genesis_id, genesis); + tracing::warn!(target: "network", "Attempting to connect to a node ({}) with a different genesis block. Our genesis: {:?}, their genesis: {:?}", peer_info, self.network_state.genesis_id, genesis); self.stop(ctx, ClosingReason::HandshakeFailed); } HandshakeFailureReason::ProtocolVersionMismatch { @@ -677,7 +677,7 @@ impl PeerActor { if common_version < oldest_supported_version || common_version < PEER_MIN_ALLOWED_PROTOCOL_VERSION { - warn!(target: "network", "Unable to connect to a node ({}) due to a network protocol version mismatch. Our version: {:?}, their: {:?}", peer_info, (PROTOCOL_VERSION, PEER_MIN_ALLOWED_PROTOCOL_VERSION), (version, oldest_supported_version)); + tracing::warn!(target: "network", "Unable to connect to a node ({}) due to a network protocol version mismatch. Our version: {:?}, their: {:?}", peer_info, (PROTOCOL_VERSION, PEER_MIN_ALLOWED_PROTOCOL_VERSION), (version, oldest_supported_version)); self.stop(ctx, ClosingReason::HandshakeFailed); return; } @@ -722,7 +722,7 @@ impl PeerActor { edge.verify(); // Disconnect if neighbor sent an invalid edge. if !ok { - info!(target: "network", "{:?}: Peer {:?} sent invalid edge. Disconnect.", self.my_node_id(), self.peer_addr); + tracing::info!(target: "network", "{:?}: Peer {:?} sent invalid edge. Disconnect.", self.my_node_id(), self.peer_addr); self.stop(ctx, ClosingReason::HandshakeFailed); return; } @@ -913,12 +913,12 @@ impl PeerActor { match peer_msg.clone() { PeerMessage::Disconnect => { - debug!(target: "network", "Disconnect signal. Me: {:?} Peer: {:?}", self.my_node_info.id, self.other_peer_id()); + tracing::debug!(target: "network", "Disconnect signal. Me: {:?} Peer: {:?}", self.my_node_info.id, self.other_peer_id()); self.stop(ctx, ClosingReason::DisconnectMessage); } PeerMessage::Handshake(_) => { // Received handshake after already have seen handshake from this peer. - debug!(target: "network", "Duplicate handshake from {}", self.peer_info); + tracing::debug!(target: "network", "Duplicate handshake from {}", self.peer_info); } PeerMessage::PeersRequest => { let peers = self @@ -1066,7 +1066,7 @@ impl PeerActor { self.network_state.send_message_to_peer(&self.clock, msg); } else { self.network_state.config.event_sink.push(Event::RoutedMessageDropped); - warn!(target: "network", ?msg, ?from, "Message dropped because TTL reached 0."); + tracing::warn!(target: "network", ?msg, ?from, "Message dropped because TTL reached 0."); metrics::ROUTED_MESSAGE_DROPPED .with_label_values(&[msg.body_variant()]) .inc(); @@ -1122,7 +1122,7 @@ impl actix::Actor for PeerActor { self.network_state.config.handshake_timeout.try_into().unwrap(), move |act, ctx| match act.peer_status { PeerStatus::Connecting { .. } => { - info!(target: "network", "Handshake timeout expired for {}", act.peer_info); + tracing::info!(target: "network", "Handshake timeout expired for {}", act.peer_info); act.stop(ctx, ClosingReason::HandshakeFailed); } _ => {} @@ -1141,15 +1141,15 @@ impl actix::Actor for PeerActor { .push(Event::HandshakeStarted(HandshakeStartedEvent { stream_id: self.stream_id })); } - fn stopping(&mut self, _: &mut Self::Context) -> Running { - Running::Stop + fn stopping(&mut self, _: &mut Self::Context) -> actix::Running { + actix::Running::Stop } fn stopped(&mut self, _ctx: &mut Self::Context) { // closing_reason may be None in case the whole actix system is stopped. // It happens a lot in tests. metrics::PEER_CONNECTIONS_TOTAL.dec(); - debug!(target: "network", "{:?}: [status = {:?}] Peer {} disconnected.", self.my_node_info.id, self.peer_status, self.peer_info); + tracing::debug!(target: "network", "{:?}: [status = {:?}] Peer {} disconnected.", self.my_node_info.id, self.peer_status, self.peer_info); if self.closing_reason.is_none() { // Due to Actix semantics, sometimes closing reason may be not set. // But it is only expected to happen in tests. @@ -1242,7 +1242,7 @@ impl actix::Handler for PeerActor { let mut peer_msg = match self.parse_message(&msg) { Ok(msg) => msg, Err(err) => { - debug!(target: "network", "Received invalid data {} from {}: {}", pretty::AbbrBytes(&msg), self.peer_info, err); + tracing::debug!(target: "network", "Received invalid data {} from {}: {}", pretty::AbbrBytes(&msg), self.peer_info, err); return; } }; @@ -1254,7 +1254,7 @@ impl actix::Handler for PeerActor { // Drop duplicated messages routed within DROP_DUPLICATED_MESSAGES_PERIOD ms if let Some(&t) = self.routed_message_cache.get(&key) { if now <= t + DROP_DUPLICATED_MESSAGES_PERIOD { - debug!(target: "network", "Dropping duplicated message from {} to {:?}", msg.author, msg.target); + tracing::debug!(target: "network", "Dropping duplicated message from {} to {:?}", msg.author, msg.target); return; } } @@ -1314,7 +1314,7 @@ impl actix::Handler for PeerActor { } } -impl Handler> for PeerActor { +impl actix::Handler> for PeerActor { type Result = (); #[perf] diff --git a/chain/network/src/peer_manager/network_state/mod.rs b/chain/network/src/peer_manager/network_state/mod.rs index 2e7a72a10d3..f56c34e4b78 100644 --- a/chain/network/src/peer_manager/network_state/mod.rs +++ b/chain/network/src/peer_manager/network_state/mod.rs @@ -29,7 +29,7 @@ use rayon::iter::ParallelBridge; use std::net::SocketAddr; use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; use std::sync::Arc; -use tracing::{debug, trace, Instrument}; +use tracing::Instrument as _; mod tier1; @@ -396,7 +396,7 @@ impl NetworkState { // Check if the message is for myself and don't try to send it in that case. if let PeerIdOrHash::PeerId(target) = &msg.target { if target == &my_peer_id { - debug!(target: "network", account_id = ?self.config.validator.as_ref().map(|v|v.account_id()), ?my_peer_id, ?msg, "Drop signed message to myself"); + tracing::debug!(target: "network", account_id = ?self.config.validator.as_ref().map(|v|v.account_id()), ?my_peer_id, ?msg, "Drop signed message to myself"); metrics::CONNECTED_TO_MYSELF.inc(); return false; } @@ -405,7 +405,7 @@ impl NetworkState { Ok(peer_id) => { // Remember if we expect a response for this message. if msg.author == my_peer_id && msg.expect_response() { - trace!(target: "network", ?msg, "initiate route back"); + tracing::trace!(target: "network", ?msg, "initiate route back"); self.routing_table_view.add_route_back(&clock, msg.hash(), my_peer_id); } return self.tier2.send_message(peer_id, Arc::new(PeerMessage::Routed(msg))); @@ -414,7 +414,7 @@ impl NetworkState { // TODO(MarX, #1369): Message is dropped here. Define policy for this case. metrics::MessageDropped::NoRouteFound.inc(&msg.body); - debug!(target: "network", + tracing::debug!(target: "network", account_id = ?self.config.validator.as_ref().map(|v|v.account_id()), to = ?msg.target, reason = ?find_route_error, @@ -440,12 +440,12 @@ impl NetworkState { None => { // TODO(MarX, #1369): Message is dropped here. Define policy for this case. metrics::MessageDropped::UnknownAccount.inc(&msg); - debug!(target: "network", + tracing::debug!(target: "network", account_id = ?self.config.validator.as_ref().map(|v|v.account_id()), to = ?account_id, ?msg,"Drop message: unknown account", ); - trace!(target: "network", known_peers = ?self.routing_table_view.get_accounts_keys(), "Known peers"); + tracing::trace!(target: "network", known_peers = ?self.routing_table_view.get_accounts_keys(), "Known peers"); return false; } }; diff --git a/chain/network/src/peer_manager/peer_manager_actor.rs b/chain/network/src/peer_manager/peer_manager_actor.rs index f23a404e81b..1f069a8f792 100644 --- a/chain/network/src/peer_manager/peer_manager_actor.rs +++ b/chain/network/src/peer_manager/peer_manager_actor.rs @@ -21,7 +21,7 @@ use crate::types::{ PeerManagerMessageResponse, PeerType, SetChainInfo, }; use actix::fut::future::wrap_future; -use actix::{Actor, AsyncContext, Context, Handler, Running}; +use actix::{Actor as _, AsyncContext as _}; use anyhow::Context as _; use near_o11y::{ handler_debug_span, handler_trace_span, OpenTelemetrySpanExt, WithSpanContext, @@ -40,7 +40,7 @@ use std::cmp::min; use std::collections::HashSet; use std::sync::atomic::Ordering; use std::sync::Arc; -use tracing::{debug, error, info, warn, Instrument}; +use tracing::Instrument as _; /// Ratio between consecutive attempts to establish connection with another peer. /// In the kth step node should wait `10 * EXPONENTIAL_BACKOFF_RATIO**k` milliseconds @@ -132,13 +132,13 @@ pub enum Event { ConnectionClosed(crate::peer::peer_actor::ConnectionClosedEvent), } -impl Actor for PeerManagerActor { - type Context = Context; +impl actix::Actor for PeerManagerActor { + type Context = actix::Context; fn started(&mut self, ctx: &mut Self::Context) { // Start server if address provided. if let Some(server_addr) = self.state.config.node_addr { - debug!(target: "network", at = ?server_addr, "starting public server"); + tracing::debug!(target: "network", at = ?server_addr, "starting public server"); let clock = self.clock.clone(); let state = self.state.clone(); ctx.spawn(wrap_future(async move { @@ -156,7 +156,7 @@ impl Actor for PeerManagerActor { // It is expected to be reasonably cheap: eventually, for TIER2 network // we would like to exchange set of connected peers even without establishing // a proper connection. - debug!(target: "network", from = ?stream.peer_addr, "got new connection"); + tracing::debug!(target: "network", from = ?stream.peer_addr, "got new connection"); if let Err(err) = PeerActor::spawn(clock.clone(), stream, None, state.clone()) { @@ -171,7 +171,7 @@ impl Actor for PeerManagerActor { self.push_network_info_trigger(ctx, self.state.config.push_info_period); // Periodically starts peer monitoring. - debug!(target: "network", + tracing::debug!(target: "network", max_period=?self.state.config.monitor_peers_max_period, "monitor_peers_trigger"); self.monitor_peers_trigger( @@ -213,11 +213,11 @@ impl Actor for PeerManagerActor { } /// Try to gracefully disconnect from connected peers. - fn stopping(&mut self, _ctx: &mut Self::Context) -> Running { - warn!("PeerManager: stopping"); + fn stopping(&mut self, _ctx: &mut Self::Context) -> actix::Running { + tracing::warn!("PeerManager: stopping"); self.state.tier2.broadcast_message(Arc::new(PeerMessage::Disconnect)); self.state.routing_table_addr.do_send(StopMsg {}.with_span_context()); - Running::Stop + actix::Running::Stop } fn stopped(&mut self, _ctx: &mut Self::Context) { @@ -286,7 +286,11 @@ impl PeerManagerActor { } /// Periodically prints bandwidth stats for each peer. - fn report_bandwidth_stats_trigger(&mut self, ctx: &mut Context, every: time::Duration) { + fn report_bandwidth_stats_trigger( + &mut self, + ctx: &mut actix::Context, + every: time::Duration, + ) { let _timer = metrics::PEER_MANAGER_TRIGGER_TIME .with_label_values(&["report_bandwidth_stats"]) .start_timer(); @@ -300,7 +304,7 @@ impl PeerManagerActor { if bandwidth_used > REPORT_BANDWIDTH_THRESHOLD_BYTES || msg_received_count > REPORT_BANDWIDTH_THRESHOLD_COUNT { - debug!(target: "bandwidth", + tracing::debug!(target: "bandwidth", ?peer_id, bandwidth_used, msg_received_count, "Peer bandwidth exceeded threshold", ); @@ -309,7 +313,7 @@ impl PeerManagerActor { total_msg_received_count += msg_received_count; } - info!( + tracing::info!( target: "bandwidth", total_bandwidth_used_by_all_peers, total_msg_received_count, "Bandwidth stats" @@ -477,7 +481,7 @@ impl PeerManagerActor { // Build valid candidate list to choose the peer to be removed. All peers outside the safe set. let candidates = tier2.ready.values().filter(|p| !safe_set.contains(&p.peer_info.id)); if let Some(p) = candidates.choose(&mut rand::thread_rng()) { - debug!(target: "network", id = ?p.peer_info.id, + tracing::debug!(target: "network", id = ?p.peer_info.id, tier2_len = tier2.ready.len(), ideal_connections_hi = self.state.config.ideal_connections_hi, "Stop active connection" @@ -500,7 +504,7 @@ impl PeerManagerActor { /// reach value of `max_internal` eventually. fn monitor_peers_trigger( &mut self, - ctx: &mut Context, + ctx: &mut actix::Context, mut interval: time::Duration, (default_interval, max_interval): (time::Duration, time::Duration), ) { @@ -510,7 +514,7 @@ impl PeerManagerActor { self.state.peer_store.unban(&self.clock); if let Err(err) = self.state.peer_store.update_connected_peers_last_seen(&self.clock) { - error!(target: "network", ?err, "Failed to update peers last seen time."); + tracing::error!(target: "network", ?err, "Failed to update peers last seen time."); } if self.is_outbound_bootstrap_needed() { @@ -547,7 +551,7 @@ impl PeerManagerActor { tracing::info!(target:"network", ?result, "failed to connect to {peer_info}"); } if state.peer_store.peer_connection_attempt(&clock, &peer_info.id, result).is_err() { - error!(target: "network", ?peer_info, "Failed to mark peer as failed."); + tracing::error!(target: "network", ?peer_info, "Failed to mark peer as failed."); } }.instrument(tracing::trace_span!(target: "network", "monitor_peers_trigger_connect")) })); @@ -558,7 +562,7 @@ impl PeerManagerActor { self.maybe_stop_active_connection(); if let Err(err) = self.state.peer_store.remove_expired(&self.clock) { - error!(target: "network", ?err, "Failed to remove expired peers"); + tracing::error!(target: "network", ?err, "Failed to remove expired peers"); }; // Find peers that are not reliable (too much behind) - and make sure that we're not routing messages through them. @@ -646,7 +650,7 @@ impl PeerManagerActor { } } - fn push_network_info_trigger(&self, ctx: &mut Context, interval: time::Duration) { + fn push_network_info_trigger(&self, ctx: &mut actix::Context, interval: time::Duration) { let _span = tracing::trace_span!(target: "network", "push_network_info_trigger").entered(); let network_info = self.get_network_info(); let _timer = metrics::PEER_MANAGER_TRIGGER_TIME @@ -673,7 +677,7 @@ impl PeerManagerActor { fn handle_msg_network_requests( &mut self, msg: NetworkRequests, - ctx: &mut Context, + ctx: &mut actix::Context, ) -> NetworkResponses { let msg_type: &str = msg.as_ref(); let _span = @@ -815,7 +819,7 @@ impl PeerManagerActor { break; } } else { - debug!(target: "network", chunk_hash=?request.chunk_hash, "Failed to find any matching peer for chunk"); + tracing::debug!(target: "network", chunk_hash=?request.chunk_hash, "Failed to find any matching peer for chunk"); } } } @@ -823,7 +827,7 @@ impl PeerManagerActor { if success { NetworkResponses::NoResponse } else { - debug!(target: "network", chunk_hash=?request.chunk_hash, "Failed to find a route for chunk"); + tracing::debug!(target: "network", chunk_hash=?request.chunk_hash, "Failed to find a route for chunk"); NetworkResponses::RouteNotFound } } @@ -905,7 +909,7 @@ impl PeerManagerActor { fn handle_peer_manager_message( &mut self, msg: PeerManagerMessageRequest, - ctx: &mut Context, + ctx: &mut actix::Context, ) -> PeerManagerMessageResponse { match msg { PeerManagerMessageRequest::NetworkRequests(msg) => { @@ -945,7 +949,7 @@ impl PeerManagerActor { /// TODO(gprusak): In prod, NetworkInfo is pushed periodically from PeerManagerActor to ClientActor. /// It would be cleaner to replace the push loop in PeerManagerActor with a pull loop /// in the ClientActor. -impl Handler> for PeerManagerActor { +impl actix::Handler> for PeerManagerActor { type Result = NetworkInfo; fn handle( &mut self, @@ -1008,7 +1012,7 @@ impl actix::Handler> for PeerManagerActor { } } -impl Handler> for PeerManagerActor { +impl actix::Handler> for PeerManagerActor { type Result = PeerManagerMessageResponse; fn handle( &mut self, @@ -1023,9 +1027,9 @@ impl Handler> for PeerManagerActor { } } -impl Handler for PeerManagerActor { +impl actix::Handler for PeerManagerActor { type Result = DebugStatus; - fn handle(&mut self, msg: GetDebugStatus, _ctx: &mut Context) -> Self::Result { + fn handle(&mut self, msg: GetDebugStatus, _ctx: &mut actix::Context) -> Self::Result { match msg { GetDebugStatus::PeerStore => { let mut peer_states_view = self From 6253ddf26d2154fe57db44ea8abdcfcdcfefe970 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Wed, 23 Nov 2022 16:09:45 +0100 Subject: [PATCH 026/188] Remove routing actor. (#8076) Replaced actix routing Actor with synchronous demultiplexed routing table computation. This makes code less asynchronous, so more predictable, so less prone to make tests flaky. Routing table computation still has a dedicated thread, because moving CPU heavy stuff to rayon would require even more invasive refactor. I had to move some code around between the files and that's where the PR size comes from. I've also removed the graph-related benchmark, because nobody is using them anyway (so they are a deadcode in a way) and they weren't compatible with this refactor. I can write new benchmarks if needed. --- chain/network/Cargo.toml | 8 - chain/network/benches/graph.rs | 79 ----- chain/network/benches/routing_table_actor.rs | 171 ---------- chain/network/src/broadcast/mod.rs | 30 +- chain/network/src/concurrency/mod.rs | 1 + chain/network/src/concurrency/runtime.rs | 34 ++ chain/network/src/config.rs | 6 +- chain/network/src/network_protocol/edge.rs | 2 +- chain/network/src/peer/peer_actor.rs | 18 +- chain/network/src/peer/testonly.rs | 72 ++-- chain/network/src/peer/tests/communication.rs | 12 +- .../src/peer_manager/connection/mod.rs | 40 +-- .../src/peer_manager/network_state/mod.rs | 284 ++++------------ .../src/peer_manager/network_state/routing.rs | 115 +++++++ .../src/peer_manager/peer_manager_actor.rs | 76 +---- chain/network/src/peer_manager/testonly.rs | 80 ++--- .../src/peer_manager/tests/accounts_data.rs | 30 +- chain/network/src/peer_manager/tests/nonce.rs | 81 +++-- .../network/src/peer_manager/tests/routing.rs | 68 ++-- chain/network/src/routing/actor.rs | 271 --------------- .../network/src/routing/{graph.rs => bfs.rs} | 52 ++- chain/network/src/routing/graph/mod.rs | 315 ++++++++++++++++++ chain/network/src/routing/graph/tests.rs | 262 +++++++++++++++ chain/network/src/routing/graph_with_cache.rs | 157 --------- chain/network/src/routing/mod.rs | 16 +- .../mod.rs} | 50 +-- .../cache.rs => routing_table_view/tests.rs} | 44 ++- .../network/src/routing/tests/cache_edges.rs | 287 ---------------- chain/network/src/routing/tests/mod.rs | 3 - .../src/routing/tests/routing_table_view.rs | 33 -- chain/network/src/stats/metrics.rs | 9 - chain/network/src/testonly/stream.rs | 8 +- 32 files changed, 1069 insertions(+), 1645 deletions(-) delete mode 100644 chain/network/benches/graph.rs delete mode 100644 chain/network/benches/routing_table_actor.rs create mode 100644 chain/network/src/concurrency/runtime.rs create mode 100644 chain/network/src/peer_manager/network_state/routing.rs delete mode 100644 chain/network/src/routing/actor.rs rename chain/network/src/routing/{graph.rs => bfs.rs} (93%) create mode 100644 chain/network/src/routing/graph/mod.rs create mode 100644 chain/network/src/routing/graph/tests.rs delete mode 100644 chain/network/src/routing/graph_with_cache.rs rename chain/network/src/routing/{routing_table_view.rs => routing_table_view/mod.rs} (82%) rename chain/network/src/routing/{tests/cache.rs => routing_table_view/tests.rs} (71%) delete mode 100644 chain/network/src/routing/tests/cache_edges.rs delete mode 100644 chain/network/src/routing/tests/mod.rs delete mode 100644 chain/network/src/routing/tests/routing_table_view.rs diff --git a/chain/network/Cargo.toml b/chain/network/Cargo.toml index 3eb9d395f51..4632dfa1703 100644 --- a/chain/network/Cargo.toml +++ b/chain/network/Cargo.toml @@ -67,11 +67,3 @@ performance_stats = [ test_features = [] shardnet = [] - -[[bench]] -name = "graph" -harness = false - -[[bench]] -name = "routing_table_actor" -harness = false diff --git a/chain/network/benches/graph.rs b/chain/network/benches/graph.rs deleted file mode 100644 index e1e026487ee..00000000000 --- a/chain/network/benches/graph.rs +++ /dev/null @@ -1,79 +0,0 @@ -#[macro_use] -extern crate criterion; - -use criterion::{black_box, Criterion}; -use near_network::routing::Graph; -use near_network::test_utils::random_peer_id; - -fn build_graph(depth: usize, size: usize) -> Graph { - let source = random_peer_id(); - let nodes: Vec<_> = (0..depth * size).map(|_| random_peer_id()).collect(); - - let mut graph = Graph::new(source.clone()); - - for node in &nodes[..size] { - graph.add_edge(&source, node); - } - - for layer in 0..depth - 1 { - for u in 0..size { - for v in 0..size { - graph.add_edge(&nodes[layer * size + u], &nodes[(layer + 1) * size + v]); - } - } - } - - graph -} - -fn calculate_distance_3_3(c: &mut Criterion) { - let graph = build_graph(3, 3); - c.bench_function("calculate_distance_3_3", |bench| { - bench.iter(|| { - black_box(graph.calculate_distance()); - }) - }); -} - -fn calculate_distance_10_10(c: &mut Criterion) { - let graph = build_graph(10, 10); - c.bench_function("calculate_distance_10_10", |bench| { - bench.iter(|| { - black_box(graph.calculate_distance()); - }) - }); -} - -fn calculate_distance_10_100(c: &mut Criterion) { - c.bench_function("calculate_distance_10_100", |bench| { - let graph = build_graph(10, 100); - bench.iter(|| { - black_box(graph.calculate_distance()); - }) - }); -} - -#[allow(dead_code)] -fn calculate_distance_100_100(c: &mut Criterion) { - let graph = build_graph(100, 100); - c.bench_function("calculate_distance_100_100", |bench| { - bench.iter(|| { - black_box(graph.calculate_distance()); - }) - }); -} - -criterion_group!( - benches, - calculate_distance_3_3, - calculate_distance_10_10, - // calculate_distance_100_100, - calculate_distance_10_100 -); - -criterion_main!(benches); - -// running 3 tests -// calculate_distance_3_3 time: [566.42 ns 571.50 ns 578.62 ns] -// calculate_distance_10_10 time: [10.631 us 10.651 us 10.679 us] -// calculate_distance_10_100 time: [607.36 us 610.44 us 613.75 us] diff --git a/chain/network/benches/routing_table_actor.rs b/chain/network/benches/routing_table_actor.rs deleted file mode 100644 index 29265df4167..00000000000 --- a/chain/network/benches/routing_table_actor.rs +++ /dev/null @@ -1,171 +0,0 @@ -#[macro_use] -extern crate criterion; - -use criterion::{black_box, Criterion}; -use near_crypto::{KeyType, SecretKey, Signature}; -use near_network::routing; -use near_network::test_utils::random_peer_id; -use near_network::types::Edge; -use near_primitives::network::PeerId; -use std::collections::HashMap; -use std::sync::Arc; - -fn build_graph(depth: usize, size: usize) -> routing::GraphWithCache { - let source = random_peer_id(); - let nodes: Vec<_> = (0..depth * size).map(|_| random_peer_id()).collect(); - - let mut graph = routing::GraphWithCache::new(source.clone()); - - let mut edges: Vec = Vec::new(); - for node in &nodes[..size] { - edges.push(Edge::make_fake_edge(source.clone(), node.clone(), 1)); - } - - for layer in 0..depth - 1 { - for u in 0..size { - for v in 0..size { - let peer0 = nodes[layer * size + u].clone(); - let peer1 = nodes[(layer + 1) * size + v].clone(); - edges.push(Edge::make_fake_edge(peer0, peer1, (layer + u + v) as u64)); - } - } - } - graph.update_edges(edges); - graph -} - -#[allow(dead_code)] -fn get_all_edges_bench_old(c: &mut Criterion) { - // 1000 nodes, 10m edges - let graph = build_graph(10, 100); - c.bench_function("get_all_edges_bench_old", |bench| { - bench.iter(|| { - let result = graph.edges(); - black_box(result); - }) - }); -} - -#[allow(dead_code)] -fn get_all_edges_bench_new2(c: &mut Criterion) { - // this is how we efficient we could make get_all_edges by using Arc - - // 1000 nodes, 10m edges - let graph = build_graph(10, 100); - let all_edges = graph.edges(); - let mut new_edges_info = HashMap::new(); - for edge in all_edges.values() { - let edge = EdgeNew { - key: Arc::new(edge.key().clone()), - nonce: edge.nonce(), - signature0: edge.signature0().clone(), - signature1: edge.signature1().clone(), - removal_info: edge.removal_info().cloned(), - }; - - new_edges_info.insert(edge.key.clone(), Arc::new(edge)); - } - - c.bench_function("get_all_edges_bench_new2", |bench| { - bench.iter(|| { - let result: Vec> = new_edges_info.iter().map(|x| x.1.clone()).collect(); - black_box(result); - }) - }); -} - -#[allow(dead_code)] -fn get_all_edges_bench_new3(c: &mut Criterion) { - // this is how we efficient we could make get_all_edges by using Arc - - // 1000 nodes, 10m edges - let rt = build_graph(10, 100); - let all_edges = rt.edges(); - let mut new_edges_info = HashMap::new(); - for edge in all_edges.values() { - let edge = EdgeNew2 { - key: (Arc::new(edge.key().0.clone()), Arc::new(edge.key().1.clone())), - nonce: edge.nonce(), - signature0: edge.signature0().clone(), - signature1: edge.signature1().clone(), - removal_info: edge.removal_info().cloned(), - }; - - new_edges_info.insert(edge.key.clone(), Arc::new(edge)); - } - - c.bench_function("get_all_edges_bench3", |bench| { - bench.iter(|| { - let result: Vec> = new_edges_info.iter().map(|x| x.1.clone()).collect(); - black_box(result); - }) - }); -} - -#[allow(dead_code)] -fn benchmark_sign_edge(c: &mut Criterion) { - let sk = SecretKey::from_seed(KeyType::ED25519, "1234"); - - let p0 = PeerId::new(sk.public_key()); - let p1 = PeerId::random(); - - c.bench_function("benchmark_sign_edge", |bench| { - bench.iter(|| { - let ei = Edge::build_hash(&p0, &p1, 123); - black_box(ei); - }) - }); -} - -criterion_group!( - benches, - get_all_edges_bench_old, - get_all_edges_bench_new2, - get_all_edges_bench_new3, - benchmark_sign_edge -); - -criterion_main!(benches); - -// running 3 tests -// test get_all_edges_bench_old ... bench: 1,296,045 ns/iter (+/- 601,626) -// replace key with Arc -// test get_all_edges_bench_new2 ... bench: 1,001,090 ns/iter (+/- 25,434) -// replace PeerId with Arc (Preferred) -// test get_all_edges_bench_new3 ... bench: 1,017,563 ns/iter (+/- 37,675) - -pub struct EdgeNew { - /// Since edges are not directed `peer0 < peer1` should hold. - pub key: Arc<(PeerId, PeerId)>, - /// Nonce to keep tracking of the last update on this edge. - /// It must be even - pub nonce: u64, - /// Signature from parties validating the edge. These are signature of the added edge. - #[allow(unused)] - signature0: Signature, - #[allow(unused)] - signature1: Signature, - /// Info necessary to declare an edge as removed. - /// The bool says which party is removing the edge: false for Peer0, true for Peer1 - /// The signature from the party removing the edge. - #[allow(unused)] - removal_info: Option<(bool, Signature)>, -} - -pub struct EdgeNew2 { - /// Since edges are not directed `peer0 < peer1` should hold. - pub key: (Arc, Arc), - /// Nonce to keep tracking of the last update on this edge. - /// It must be even - pub nonce: u64, - /// Signature from parties validating the edge. These are signature of the added edge. - #[allow(unused)] - signature0: Signature, - #[allow(unused)] - signature1: Signature, - /// Info necessary to declare an edge as removed. - /// The bool says which party is removing the edge: false for Peer0, true for Peer1 - /// The signature from the party removing the edge. - #[allow(unused)] - removal_info: Option<(bool, Signature)>, -} diff --git a/chain/network/src/broadcast/mod.rs b/chain/network/src/broadcast/mod.rs index 16dab6ab164..04d063a1c60 100644 --- a/chain/network/src/broadcast/mod.rs +++ b/chain/network/src/broadcast/mod.rs @@ -56,20 +56,32 @@ impl Receiver { Self { channel: self.channel.clone(), next: self.channel.stream.read().unwrap().len() } } + /// recv() extracts a value from the channel. + /// If channel is empty, awaits until a value is pushed to the channel. pub async fn recv(&mut self) -> T { self.next += 1; - let l = self.channel.stream.read().unwrap(); - let n = self.channel.notify.notified(); - let v = if l.len() > self.next - 1 { Some(l[self.next - 1].clone()) } else { None }; - if let Some(v) = v { - return v; - } - drop(l); - n.await; + let new_value_pushed = { + // The lock has to be inside a block without await, + // because otherwise recv() is not Send. + let l = self.channel.stream.read().unwrap(); + let new_value_pushed = self.channel.notify.notified(); + // Synchronically check if the channel is non-empty. + // If so, pop a value and return immediately. + let v = if l.len() > self.next - 1 { Some(l[self.next - 1].clone()) } else { None }; + if let Some(v) = v { + return v; + } + new_value_pushed + }; + // Channel was empty, so we wait for the new value. + new_value_pushed.await; let v = self.channel.stream.read().unwrap()[self.next - 1].clone(); v } + /// Calls recv() in a loop until the returned value satisfies the predicate `pred` + /// (predicate is satisfied iff it returns `Some(u)`). Returns `u`. + /// All the values popped from the channel in the process are dropped silently. pub async fn recv_until(&mut self, mut pred: impl FnMut(T) -> Option) -> U { loop { if let Some(u) = pred(self.recv().await) { @@ -78,6 +90,8 @@ impl Receiver { } } + /// Non-blocking version of recv(): pops a value from the channel, + /// or returns None if channel is empty. pub fn try_recv(&mut self) -> Option { let l = self.channel.stream.read().unwrap(); if l.len() <= self.next { diff --git a/chain/network/src/concurrency/mod.rs b/chain/network/src/concurrency/mod.rs index 909e82b32f2..08d6d92b1db 100644 --- a/chain/network/src/concurrency/mod.rs +++ b/chain/network/src/concurrency/mod.rs @@ -3,6 +3,7 @@ pub mod atomic_cell; pub mod demux; pub mod rate; pub mod rayon; +pub mod runtime; #[cfg(test)] mod tests; diff --git a/chain/network/src/concurrency/runtime.rs b/chain/network/src/concurrency/runtime.rs new file mode 100644 index 00000000000..538b5ae8f76 --- /dev/null +++ b/chain/network/src/concurrency/runtime.rs @@ -0,0 +1,34 @@ +use std::sync::Arc; + +/// Single-threaded runtime which cancels all the tasks as soon as it is dropped. +/// A potential in-place replacement for actix::Actor. +pub(crate) struct Runtime { + pub handle: tokio::runtime::Handle, + stop: Arc, + thread: Option>, +} + +impl Runtime { + pub fn new() -> Self { + let stop = Arc::new(tokio::sync::Notify::new()); + let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); + let handle = runtime.handle().clone(); + let thread = std::thread::spawn({ + let stop = stop.clone(); + move || runtime.block_on(stop.notified()) + }); + Self { handle, stop, thread: Some(thread) } + } +} + +impl Drop for Runtime { + fn drop(&mut self) { + self.stop.notify_one(); + let thread = self.thread.take().unwrap(); + // Await for the thread to stop, unless it is the current thread + // (i.e. nobody waits for it). + if std::thread::current().id() != thread.thread().id() { + thread.join().unwrap(); + } + } +} diff --git a/chain/network/src/config.rs b/chain/network/src/config.rs index 9202b43d28f..4924ed3d20d 100644 --- a/chain/network/src/config.rs +++ b/chain/network/src/config.rs @@ -130,7 +130,7 @@ pub struct NetworkConfig { pub archive: bool, /// Maximal rate at which SyncAccountsData can be broadcasted. pub accounts_data_broadcast_rate_limit: rate::Limit, - /// Maximal rate at which RoutingTableUpdate can be sent out. + /// Maximal rate at which RoutingTable can be recomputed. pub routing_table_update_rate_limit: rate::Limit, /// Config of the TIER1 network. pub tier1: Option, @@ -252,7 +252,7 @@ impl NetworkConfig { outbound_disabled: false, archive, accounts_data_broadcast_rate_limit: rate::Limit { qps: 0.1, burst: 1 }, - routing_table_update_rate_limit: rate::Limit { qps: 0.5, burst: 1 }, + routing_table_update_rate_limit: rate::Limit { qps: 1., burst: 1 }, tier1: Some(Tier1 { advertise_proxies_interval: time::Duration::minutes(15) }), inbound_disabled: cfg.experimental.inbound_disabled, skip_tombstones: if cfg.experimental.skip_sending_tombstones_seconds > 0 { @@ -317,7 +317,7 @@ impl NetworkConfig { inbound_disabled: false, archive: false, accounts_data_broadcast_rate_limit: rate::Limit { qps: 100., burst: 1000000 }, - routing_table_update_rate_limit: rate::Limit { qps: 100., burst: 1000000 }, + routing_table_update_rate_limit: rate::Limit { qps: 10., burst: 1 }, tier1: Some(Tier1 { // Interval is very large, so that it doesn't happen spontaneously in tests. // It should rather be triggered manually in tests. diff --git a/chain/network/src/network_protocol/edge.rs b/chain/network/src/network_protocol/edge.rs index dde30828f59..720bf283557 100644 --- a/chain/network/src/network_protocol/edge.rs +++ b/chain/network/src/network_protocol/edge.rs @@ -147,7 +147,7 @@ impl Edge { Edge(Arc::new(edge)) } - fn hash(&self) -> CryptoHash { + pub(crate) fn hash(&self) -> CryptoHash { Edge::build_hash(&self.key().0, &self.key().1, self.nonce()) } diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index 55a4cabc9b9..53085b38a81 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -440,11 +440,11 @@ impl PeerActor { // Check that the received nonce is greater than the current nonce of this connection. // If not (and this is an inbound connection) propose a new nonce. if let Some(last_edge) = - self.network_state.routing_table_view.get_local_edge(&handshake.sender_peer_id) + self.network_state.graph.load().local_edges.get(&handshake.sender_peer_id) { if last_edge.nonce() >= handshake.partial_edge_info.nonce { tracing::debug!(target: "network", "{:?}: Received too low nonce from peer {:?} sending evidence.", self.my_node_id(), self.peer_addr); - self.send_message_or_log(&PeerMessage::LastEdge(last_edge)); + self.send_message_or_log(&PeerMessage::LastEdge(last_edge.clone())); return; } } @@ -516,9 +516,6 @@ impl PeerActor { send_accounts_data_demux: demux::Demux::new( self.network_state.config.accounts_data_broadcast_rate_limit, ), - send_routing_table_update_demux: demux::Demux::new( - self.network_state.config.routing_table_update_rate_limit, - ), }); let tracker = self.tracker.clone(); @@ -645,12 +642,12 @@ impl PeerActor { // Send full RoutingTable. fn sync_routing_table(&self) { let mut known_edges: Vec = - self.network_state.graph.read().edges().values().cloned().collect(); + self.network_state.graph.load().edges.values().cloned().collect(); if self.network_state.config.skip_tombstones.is_some() { known_edges.retain(|edge| edge.removal_info().is_none()); metrics::EDGE_TOMBSTONE_SENDING_SKIPPED.inc(); } - let known_accounts = self.network_state.routing_table_view.get_announce_accounts(); + let known_accounts = self.network_state.graph.routing_table.get_announce_accounts(); self.send_message_or_log(&PeerMessage::SyncRoutingTable(RoutingTableUpdate::new( known_edges, known_accounts, @@ -947,7 +944,7 @@ impl PeerActor { let network_state = self.network_state.clone(); ctx.spawn(wrap_future(async move { let peer_id = &conn.peer_info.id; - let edge = match network_state.routing_table_view.get_local_edge(peer_id) { + let edge = match network_state.graph.load().local_edges.get(peer_id) { Some(cur_edge) if cur_edge.edge_type() == EdgeState::Active && cur_edge.nonce() >= edge_info.nonce => @@ -1031,7 +1028,7 @@ impl PeerActor { let from = &conn.peer_info.id; if msg.expect_response() { tracing::trace!(target: "network", route_back = ?msg.clone(), "Received peer message that requires response"); - self.network_state.routing_table_view.add_route_back( + self.network_state.graph.routing_table.add_route_back( &self.clock, msg.hash(), from.clone(), @@ -1092,7 +1089,8 @@ impl PeerActor { // as well as filter out those which are older than the fetched ones (to avoid overriding // a newer announce with an older one). let old = network_state - .routing_table_view + .graph + .routing_table .get_broadcasted_announces(rtu.accounts.iter().map(|a| &a.account_id)); let accounts: Vec<(AnnounceAccount, Option)> = rtu .accounts diff --git a/chain/network/src/peer/testonly.rs b/chain/network/src/peer/testonly.rs index 45d4c9dfc00..19c4c8012db 100644 --- a/chain/network/src/peer/testonly.rs +++ b/chain/network/src/peer/testonly.rs @@ -4,7 +4,7 @@ use crate::network_protocol::testonly as data; use crate::network_protocol::{ Edge, PartialEdgeInfo, PeerMessage, RawRoutedMessage, RoutedMessageBody, RoutedMessageV2, }; -use crate::peer::peer_actor::{ClosingReason, PeerActor}; +use crate::peer::peer_actor::PeerActor; use crate::peer_manager::network_state::NetworkState; use crate::peer_manager::peer_manager_actor; use crate::peer_manager::peer_store; @@ -15,7 +15,6 @@ use crate::testonly::actix::ActixSystem; use crate::testonly::fake_client; use crate::time; use crate::types::PeerIdOrHash; -use near_crypto::Signature; use near_o11y::WithSpanContextExt; use near_primitives::network::PeerId; use std::sync::Arc; @@ -24,14 +23,6 @@ pub struct PeerConfig { pub chain: Arc, pub network: NetworkConfig, pub force_encoding: Option, - /// If both start_handshake_with and nonce are set, PeerActor - /// will use this nonce in the handshake. - /// WARNING: it has to be >0. - /// WARNING: currently nonce is decided by a lookup in the RoutingTableView, - /// so to enforce the nonce below, we add an artificial edge to RoutingTableView. - /// Once we switch to generating nonce from timestamp, this field should be deprecated - /// in favor of passing a fake clock. - pub nonce: Option, } impl PeerConfig { @@ -81,17 +72,6 @@ impl PeerHandle { .await, ); } - pub async fn fail_handshake(&mut self) -> ClosingReason { - self.events - .recv_until(|ev| match ev { - Event::Network(peer_manager_actor::Event::ConnectionClosed(ev)) => Some(ev.reason), - // HandshakeDone means that handshake succeeded locally, - // but in case this is an inbound connection, it can still - // fail on the other side. Therefore we cannot panic on HandshakeDone. - _ => None, - }) - .await - } pub fn routed_message( &self, @@ -113,38 +93,28 @@ impl PeerHandle { stream: tcp::Stream, ) -> PeerHandle { let cfg = Arc::new(cfg); - let cfg_ = cfg.clone(); let (send, recv) = broadcast::unbounded_channel(); - let actix = ActixSystem::spawn(move || { - let fc = Arc::new(fake_client::Fake { event_sink: send.sink().compose(Event::Client) }); - let store = store::Store::from(near_store::db::TestDB::new()); - let mut network_cfg = cfg.network.clone(); - network_cfg.event_sink = send.sink().compose(Event::Network); - let network_state = Arc::new(NetworkState::new( - &clock, - store.clone(), - peer_store::PeerStore::new(&clock, network_cfg.peer_store.clone(), store.clone()) - .unwrap(), - network_cfg.verify().unwrap(), - cfg.chain.genesis_id.clone(), - fc, - vec![], - )); - // WARNING: this is a hack to make PeerActor use a specific nonce - if let (Some(nonce), tcp::StreamType::Outbound { peer_id }) = - (&cfg.nonce, &stream.type_) - { - network_state.routing_table_view.add_local_edges(&[Edge::new( - cfg.id(), - peer_id.clone(), - nonce - 1, - Signature::default(), - Signature::default(), - )]); - } - PeerActor::spawn(clock, stream, cfg.force_encoding, network_state).unwrap() + + let fc = Arc::new(fake_client::Fake { event_sink: send.sink().compose(Event::Client) }); + let store = store::Store::from(near_store::db::TestDB::new()); + let mut network_cfg = cfg.network.clone(); + network_cfg.event_sink = send.sink().compose(Event::Network); + let network_state = Arc::new(NetworkState::new( + &clock, + store.clone(), + peer_store::PeerStore::new(&clock, network_cfg.peer_store.clone(), store.clone()) + .unwrap(), + network_cfg.verify().unwrap(), + cfg.chain.genesis_id.clone(), + fc, + vec![], + )); + let actix = ActixSystem::spawn({ + let clock = clock.clone(); + let cfg = cfg.clone(); + move || PeerActor::spawn(clock, stream, cfg.force_encoding, network_state).unwrap() }) .await; - Self { actix, cfg: cfg_, events: recv, edge: None } + Self { actix, cfg, events: recv, edge: None } } } diff --git a/chain/network/src/peer/tests/communication.rs b/chain/network/src/peer/tests/communication.rs index 0b7afc567e5..0e9e26080db 100644 --- a/chain/network/src/peer/tests/communication.rs +++ b/chain/network/src/peer/tests/communication.rs @@ -29,13 +29,11 @@ async fn test_peer_communication( chain: chain.clone(), network: chain.make_config(&mut rng), force_encoding: inbound_encoding, - nonce: None, }; let outbound_cfg = PeerConfig { chain: chain.clone(), network: chain.make_config(&mut rng), force_encoding: outbound_encoding, - nonce: None, }; let (outbound_stream, inbound_stream) = tcp::Stream::loopback(inbound_cfg.id()).await; let mut inbound = PeerHandle::start_endpoint(clock.clock(), inbound_cfg, inbound_stream).await; @@ -186,13 +184,11 @@ async fn test_handshake(outbound_encoding: Option, inbound_encoding: O network: chain.make_config(&mut rng), chain: chain.clone(), force_encoding: inbound_encoding, - nonce: None, }; let outbound_cfg = PeerConfig { network: chain.make_config(&mut rng), chain: chain.clone(), force_encoding: outbound_encoding, - nonce: None, }; let (outbound_stream, inbound_stream) = tcp::Stream::loopback(inbound_cfg.id()).await; let inbound = PeerHandle::start_endpoint(clock.clock(), inbound_cfg, inbound_stream).await; @@ -212,7 +208,7 @@ async fn test_handshake(outbound_encoding: Option, inbound_encoding: O // We will also introduce chain_id mismatch, but ProtocolVersionMismatch is expected to take priority. handshake.sender_chain_info.genesis_id.chain_id = "unknown_chain".to_string(); outbound.write(&PeerMessage::Handshake(handshake.clone())).await; - let resp = outbound.read().await; + let resp = outbound.read().await.unwrap(); assert_matches!( resp, PeerMessage::HandshakeFailure(_, HandshakeFailureReason::ProtocolVersionMismatch { .. }) @@ -222,7 +218,7 @@ async fn test_handshake(outbound_encoding: Option, inbound_encoding: O handshake.protocol_version = PROTOCOL_VERSION + 1; handshake.oldest_supported_version = PROTOCOL_VERSION + 1; outbound.write(&PeerMessage::Handshake(handshake.clone())).await; - let resp = outbound.read().await; + let resp = outbound.read().await.unwrap(); assert_matches!( resp, PeerMessage::HandshakeFailure(_, HandshakeFailureReason::ProtocolVersionMismatch { .. }) @@ -233,7 +229,7 @@ async fn test_handshake(outbound_encoding: Option, inbound_encoding: O handshake.protocol_version = PROTOCOL_VERSION; handshake.oldest_supported_version = PROTOCOL_VERSION; outbound.write(&PeerMessage::Handshake(handshake.clone())).await; - let resp = outbound.read().await; + let resp = outbound.read().await.unwrap(); assert_matches!( resp, PeerMessage::HandshakeFailure(_, HandshakeFailureReason::GenesisMismatch(_)) @@ -242,7 +238,7 @@ async fn test_handshake(outbound_encoding: Option, inbound_encoding: O // Send a correct Handshake, expect a matching Handshake response. handshake.sender_chain_info = chain.get_peer_chain_info(); outbound.write(&PeerMessage::Handshake(handshake.clone())).await; - let resp = outbound.read().await; + let resp = outbound.read().await.unwrap(); assert_matches!(resp, PeerMessage::Handshake(_)); } diff --git a/chain/network/src/peer_manager/connection/mod.rs b/chain/network/src/peer_manager/connection/mod.rs index ff57a948529..188cd842f92 100644 --- a/chain/network/src/peer_manager/connection/mod.rs +++ b/chain/network/src/peer_manager/connection/mod.rs @@ -1,9 +1,7 @@ use crate::concurrency::arc_mutex::ArcMutex; use crate::concurrency::atomic_cell::AtomicCell; use crate::concurrency::demux; -use crate::network_protocol::{ - Edge, PeerInfo, PeerMessage, RoutingTableUpdate, SignedAccountData, SyncAccountsData, -}; +use crate::network_protocol::{Edge, PeerInfo, PeerMessage, SignedAccountData, SyncAccountsData}; use crate::peer::peer_actor; use crate::peer::peer_actor::PeerActor; use crate::private_actix::SendMessage; @@ -73,7 +71,6 @@ pub(crate) struct Connection { /// A helper data structure for limiting reading, reporting stats. pub send_accounts_data_demux: demux::Demux>, ()>, - pub send_routing_table_update_demux: demux::Demux, ()>, } impl fmt::Debug for Connection { @@ -110,41 +107,6 @@ impl Connection { self.addr.do_send(SendMessage { message: msg }.with_span_context()); } - async fn send_routing_table_update_inner( - self: Arc, - rtus: Vec>, - ) -> Vec<()> { - self.send_message(Arc::new(PeerMessage::SyncRoutingTable(RoutingTableUpdate { - edges: Edge::deduplicate( - rtus.iter().map(|rtu| rtu.edges.iter()).flatten().cloned().collect(), - ), - accounts: rtus.iter().flat_map(|rtu| rtu.accounts.iter()).cloned().collect(), - }))); - rtus.iter().map(|_| ()).collect() - } - - pub fn send_routing_table_update( - self: &Arc, - rtu: Arc, - ) -> impl Future { - let this = self.clone(); - async move { - let res = this - .send_routing_table_update_demux - .call(rtu, { - let this = this.clone(); - move |rtus| this.send_routing_table_update_inner(rtus) - }) - .await; - if res.is_err() { - tracing::info!( - "peer {} disconnected, while sending SyncRoutingTable", - this.peer_info.id - ); - } - } - } - pub fn send_accounts_data( self: &Arc, data: Vec>, diff --git a/chain/network/src/peer_manager/network_state/mod.rs b/chain/network/src/peer_manager/network_state/mod.rs index f56c34e4b78..b21c7e28e61 100644 --- a/chain/network/src/peer_manager/network_state/mod.rs +++ b/chain/network/src/peer_manager/network_state/mod.rs @@ -1,36 +1,34 @@ use crate::accounts_data; use crate::client; -use crate::concurrency; +use crate::concurrency::demux; +use crate::concurrency::runtime::Runtime; use crate::config; use crate::network_protocol::{ Edge, EdgeState, PartialEdgeInfo, PeerIdOrHash, PeerInfo, PeerMessage, Ping, Pong, - RawRoutedMessage, RoutedMessageBody, RoutedMessageV2, RoutingTableUpdate, SignedAccountData, + RawRoutedMessage, RoutedMessageBody, RoutedMessageV2, SignedAccountData, }; use crate::peer::peer_actor::{ClosingReason, ConnectionClosedEvent}; use crate::peer_manager::connection; use crate::peer_manager::peer_manager_actor::Event; use crate::peer_manager::peer_store; use crate::private_actix::RegisterPeerError; -use crate::routing; -use crate::routing::routing_table_view::RoutingTableView; use crate::stats::metrics; use crate::store; use crate::tcp; use crate::time; use crate::types::{ChainInfo, PeerType, ReasonForBan}; use arc_swap::ArcSwap; -use near_o11y::WithSpanContextExt; use near_primitives::block::GenesisId; use near_primitives::hash::CryptoHash; -use near_primitives::network::{AnnounceAccount, PeerId}; +use near_primitives::network::PeerId; use near_primitives::types::AccountId; -use parking_lot::RwLock; -use rayon::iter::ParallelBridge; +use parking_lot::Mutex; use std::net::SocketAddr; use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; use std::sync::Arc; use tracing::Instrument as _; +mod routing; mod tier1; /// Limit number of pending Peer actors to avoid OOM. @@ -40,36 +38,11 @@ pub(crate) const LIMIT_PENDING_PEERS: usize = 60; /// We send these messages multiple times to reduce the chance that they are lost const IMPORTANT_MESSAGE_RESENT_COUNT: usize = 3; -struct Runtime { - handle: tokio::runtime::Handle, - stop: Arc, - thread: Option>, -} - -impl Runtime { - fn new() -> Self { - let stop = Arc::new(tokio::sync::Notify::new()); - let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); - let handle = runtime.handle().clone(); - let thread = std::thread::spawn({ - let stop = stop.clone(); - move || runtime.block_on(stop.notified()) - }); - Self { handle, stop, thread: Some(thread) } - } -} +/// How long a peer has to be unreachable, until we prune it from the in-memory graph. +const PRUNE_UNREACHABLE_PEERS_AFTER: time::Duration = time::Duration::hours(1); -impl Drop for Runtime { - fn drop(&mut self) { - self.stop.notify_one(); - let thread = self.thread.take().unwrap(); - // Await for the thread to stop, unless it is the current thread - // (i.e. nobody waits for it). - if std::thread::current().id() != thread.thread().id() { - thread.join().unwrap(); - } - } -} +/// Remove the edges that were created more that this duration ago. +const PRUNE_EDGES_AFTER: time::Duration = time::Duration::minutes(30); impl WhitelistNode { pub fn from_peer_info(pi: &PeerInfo) -> anyhow::Result { @@ -109,8 +82,6 @@ pub(crate) struct NetworkState { /// GenesisId of the chain. pub genesis_id: GenesisId, pub client: Arc, - /// RoutingTableActor, responsible for computing routing table, routing table exchange, etc. - pub routing_table_addr: actix::Addr, /// Network-related info about the chain. pub chain_info: ArcSwap>, @@ -123,14 +94,7 @@ pub(crate) struct NetworkState { /// Peer store that provides read/write access to peers. pub peer_store: peer_store::PeerStore, /// A graph of the whole NEAR network. - pub graph: Arc>, - - /// View of the Routing table. It keeps: - /// - routing information - how to route messages - /// - edges adjacent to my_peer_id - /// - account id - /// Full routing table (that currently includes information about all edges in the graph) is now inside Routing Table. - pub routing_table_view: RoutingTableView, + pub graph: Arc, /// Shared counter across all PeerActors, which counts number of `RoutedMessageBody::ForwardTx` /// messages sincce last block. @@ -145,6 +109,13 @@ pub(crate) struct NetworkState { /// TODO(gprusak): determine why tests need to change that dynamically /// in the first place. pub max_num_peers: AtomicU32, + + /// Demultiplexer aggregating calls to add_edges(). + add_edges_demux: demux::Demux, ()>, + + /// Mutex serializing calls to set_chain_info(), which mutates a bunch of stuff non-atomically. + /// TODO(gprusak): make it use synchronization primitives in some more canonical way. + set_chain_info_mutex: Mutex<()>, } impl NetworkState { @@ -157,11 +128,16 @@ impl NetworkState { client: Arc, whitelist_nodes: Vec, ) -> Self { - let graph = Arc::new(RwLock::new(routing::GraphWithCache::new(config.node_id()))); Self { runtime: Runtime::new(), - routing_table_addr: routing::Actor::spawn(clock.clone(), store.clone(), graph.clone()), - graph, + graph: Arc::new(crate::routing::Graph::new( + crate::routing::GraphConfig { + node_id: config.node_id(), + prune_unreachable_peers_after: PRUNE_UNREACHABLE_PEERS_AFTER, + prune_edges_after: Some(PRUNE_EDGES_AFTER), + }, + store.clone(), + )), genesis_id, client, chain_info: Default::default(), @@ -169,10 +145,11 @@ impl NetworkState { inbound_handshake_permits: Arc::new(tokio::sync::Semaphore::new(LIMIT_PENDING_PEERS)), peer_store, accounts_data: Arc::new(accounts_data::Cache::new()), - routing_table_view: RoutingTableView::new(store, config.node_id()), txns_since_last_block: AtomicUsize::new(0), whitelist_nodes, max_num_peers: AtomicU32::new(config.max_num_peers), + add_edges_demux: demux::Demux::new(config.routing_table_update_rate_limit), + set_chain_info_mutex: Mutex::new(()), config, created_at: clock.now(), } @@ -193,15 +170,6 @@ impl NetworkState { self.runtime.handle.spawn(fut.in_current_span()) } - pub fn propose_edge(&self, peer1: &PeerId, with_nonce: Option) -> PartialEdgeInfo { - // When we create a new edge we increase the latest nonce by 2 in case we miss a removal - // proposal from our partner. - let nonce = with_nonce.unwrap_or_else(|| { - self.routing_table_view.get_local_edge(peer1).map_or(1, |edge| edge.next()) - }); - PartialEdgeInfo::new(&self.config.node_id(), peer1, nonce, &self.config.node_key) - } - /// Stops peer instance if it is still connected, /// and then mark peer as banned in the peer store. pub fn disconnect_and_ban( @@ -322,7 +290,7 @@ impl NetworkState { // If the last edge we have with this peer represent a connection addition, create the edge // update that represents the connection removal. - if let Some(edge) = this.routing_table_view.get_local_edge(&peer_id) { + if let Some(edge) = this.graph.load().local_edges.get(&peer_id) { if edge.edge_type() == EdgeState::Active { let edge_update = edge.remove_edge(this.config.node_id(), &this.config.node_key); @@ -346,24 +314,13 @@ impl NetworkState { }); } - // TODO(gprusak): eventually, this should be blocking, as it should be up to the caller - // whether to wait for the broadcast to finish, or run it in parallel with sth else. - fn broadcast_routing_table_update(&self, rtu: Arc) { - if rtu.as_ref() == &RoutingTableUpdate::default() { - return; - } - for conn in self.tier2.load().ready.values() { - self.spawn(conn.send_routing_table_update(rtu.clone())); - } - } - /// Determine if the given target is referring to us. pub fn message_for_me(&self, target: &PeerIdOrHash) -> bool { let my_peer_id = self.config.node_id(); match target { PeerIdOrHash::PeerId(peer_id) => &my_peer_id == peer_id, PeerIdOrHash::Hash(hash) => { - self.routing_table_view.compare_route_back(*hash, &my_peer_id) + self.graph.routing_table.compare_route_back(*hash, &my_peer_id) } } } @@ -401,12 +358,12 @@ impl NetworkState { return false; } } - match self.routing_table_view.find_route(&clock, &msg.target) { + match self.graph.routing_table.find_route(&clock, &msg.target) { Ok(peer_id) => { // Remember if we expect a response for this message. if msg.author == my_peer_id && msg.expect_response() { tracing::trace!(target: "network", ?msg, "initiate route back"); - self.routing_table_view.add_route_back(&clock, msg.hash(), my_peer_id); + self.graph.routing_table.add_route_back(&clock, msg.hash(), my_peer_id); } return self.tier2.send_message(peer_id, Arc::new(PeerMessage::Routed(msg))); } @@ -418,7 +375,7 @@ impl NetworkState { account_id = ?self.config.validator.as_ref().map(|v|v.account_id()), to = ?msg.target, reason = ?find_route_error, - known_peers = ?self.routing_table_view.reachable_peers(), + known_peers = ?self.graph.routing_table.reachable_peers(), msg = ?msg.body, "Drop signed message" ); @@ -435,7 +392,7 @@ impl NetworkState { account_id: &AccountId, msg: RoutedMessageBody, ) -> bool { - let target = match self.routing_table_view.account_owner(account_id) { + let target = match self.graph.routing_table.account_owner(account_id) { Some(peer_id) => peer_id, None => { // TODO(MarX, #1369): Message is dropped here. Define policy for this case. @@ -445,7 +402,7 @@ impl NetworkState { to = ?account_id, ?msg,"Drop message: unknown account", ); - tracing::trace!(target: "network", known_peers = ?self.routing_table_view.get_accounts_keys(), "Known peers"); + tracing::trace!(target: "network", known_peers = ?self.graph.routing_table.get_accounts_keys(), "Known peers"); return false; } }; @@ -463,150 +420,6 @@ impl NetworkState { } } - pub async fn add_accounts(self: &Arc, accounts: Vec) { - let this = self.clone(); - self.spawn(async move { - let new_accounts = this.routing_table_view.add_accounts(accounts); - tracing::debug!(target: "network", account_id = ?this.config.validator.as_ref().map(|v|v.account_id()), ?new_accounts, "Received new accounts"); - this.broadcast_routing_table_update(Arc::new(RoutingTableUpdate::from_accounts( - new_accounts.clone(), - ))); - this.config.event_sink.push(Event::AccountsAdded(new_accounts)); - }).await.unwrap() - } - - /// Sends list of edges, from peer `peer_id` to check their signatures to `EdgeValidatorActor`. - /// Bans peer `peer_id` if an invalid edge is found. - /// `PeerManagerActor` periodically runs `broadcast_validated_edges_trigger`, which gets edges - /// from `EdgeValidatorActor` concurrent queue and sends edges to be added to `RoutingTableActor`. - pub async fn add_edges( - self: &Arc, - clock: &time::Clock, - edges: Vec, - ) -> Result<(), ReasonForBan> { - let this = self.clone(); - let clock = clock.clone(); - self.spawn(async move { - // Deduplicate edges. - // TODO(gprusak): sending duplicate edges should be considered a malicious behavior - // instead, however that would be backward incompatible, so it can be introduced in - // PROTOCOL_VERSION 60 earliest. - let mut edges = Edge::deduplicate(edges); - { - let graph = this.graph.read(); - edges.retain(|x| !graph.has(x)); - } - if edges.is_empty() { - return Ok(()); - } - // Verify the edges in parallel on rayon. - let (edges, ok) = concurrency::rayon::run(move || { - concurrency::rayon::try_map(edges.into_iter().par_bridge(), |e| { - if e.verify() { - Some(e) - } else { - None - } - }) - }) - .await; - this.routing_table_view.add_local_edges(&edges); - - let mut new_edges = match this - .routing_table_addr - .send( - routing::actor::Message::AddVerifiedEdges { edges: edges.clone() } - .with_span_context(), - ) - .await - { - Ok(routing::actor::Response::AddVerifiedEdges(new_edges)) => new_edges, - Ok(_) => panic!("expected AddVerifiedEdges"), - Err(err) => { - tracing::error!(target:"network","routing::actor::Actor closed: {err}"); - return Ok(()); - } - }; - this.config.event_sink.push(Event::EdgesVerified(edges.clone())); - // Don't send tombstones during the initial time. - // Most of the network is created during this time, which results - // in us sending a lot of tombstones to peers. - // Later, the amount of new edges is a lot smaller. - if let Some(skip_tombstones_duration) = this.config.skip_tombstones { - if clock.now() < this.created_at + skip_tombstones_duration { - new_edges.retain(|edge| edge.edge_type() == EdgeState::Active); - metrics::EDGE_TOMBSTONE_SENDING_SKIPPED.inc(); - } - } - // Broadcast new edges to all other peers. - this.broadcast_routing_table_update(Arc::new(RoutingTableUpdate::from_edges( - new_edges, - ))); - if !ok { - return Err(ReasonForBan::InvalidEdge); - } - Ok(()) - }) - .await - .unwrap() - } - - pub async fn update_routing_table( - self: &Arc, - prune_unreachable_since: Option, - prune_edges_older_than: Option, - ) { - let this = self.clone(); - self.spawn(async move { - let resp = match this - .routing_table_addr - .send( - routing::actor::Message::RoutingTableUpdate { - prune_unreachable_since, - prune_edges_older_than, - } - .with_span_context(), - ) - .await - { - Ok(resp) => resp, - Err(err) => { - // This can happen only when the PeerManagerActor is being shut down. - tracing::error!(target:"network","routing::actor::Actor: {err}"); - return; - } - }; - match resp { - routing::actor::Response::RoutingTableUpdate { pruned_edges, next_hops } => { - this.routing_table_view.update(&pruned_edges, next_hops.clone()); - this.config - .event_sink - .push(Event::RoutingTableUpdate { next_hops, pruned_edges }); - } - _ => panic!("expected RoutingTableUpdateResponse"), - } - }) - .await - .unwrap() - } - - pub async fn finalize_edge( - self: &Arc, - clock: &time::Clock, - peer_id: PeerId, - edge_info: PartialEdgeInfo, - ) -> Result { - let edge = Edge::build_with_secret_key( - self.config.node_id(), - peer_id.clone(), - edge_info.nonce, - &self.config.node_key, - edge_info.signature, - ); - self.add_edges(&clock, vec![edge.clone()]).await?; - Ok(edge) - } - pub async fn add_accounts_data( self: &Arc, accounts_data: Vec>, @@ -643,10 +456,11 @@ impl NetworkState { let this = self.clone(); let clock = clock.clone(); self.spawn(async move { - let local_edges = this.routing_table_view.get_local_edges(); + let graph = this.graph.load(); let tier2 = this.tier2.load(); let mut tasks = vec![]; - for edge in local_edges { + for edge in graph.local_edges.values() { + let edge = edge.clone(); let node_id = this.config.node_id(); let other_peer = edge.other(&node_id).unwrap(); match (tier2.ready.get(other_peer), edge.edge_type()) { @@ -668,7 +482,7 @@ impl NetworkState { // response (with timeout). Until network round trips are implemented, we just // blindly wait for a while, then check again. clock.sleep(timeout).await; - match this.routing_table_view.get_local_edge(&conn.peer_info.id) { + match this.graph.load().local_edges.get(&conn.peer_info.id) { Some(edge) if edge.edge_type() == EdgeState::Active => return, _ => conn.stop(None), } @@ -706,4 +520,24 @@ impl NetworkState { .await .unwrap() } + + /// Sets the chain info, and updates the set of TIER1 keys. + /// Returns true iff the set of TIER1 keys has changed. + pub fn set_chain_info(self: &Arc, info: ChainInfo) -> bool { + let _mutex = self.set_chain_info_mutex.lock(); + + // We set state.chain_info and call accounts_data.set_keys + // synchronously, therefore, assuming actix in-order delivery, + // there will be no race condition between subsequent SetChainInfo + // calls. + self.chain_info.store(Arc::new(Some(info.clone()))); + + // If tier1 is not enabled, we skip set_keys() call. + // This way self.state.accounts_data is always empty, hence no data + // will be collected or broadcasted. + if self.config.tier1.is_none() { + return false; + } + self.accounts_data.set_keys(info.tier1_accounts.clone()) + } } diff --git a/chain/network/src/peer_manager/network_state/routing.rs b/chain/network/src/peer_manager/network_state/routing.rs new file mode 100644 index 00000000000..8acd41fa860 --- /dev/null +++ b/chain/network/src/peer_manager/network_state/routing.rs @@ -0,0 +1,115 @@ +use super::NetworkState; +use crate::network_protocol::{Edge, EdgeState, PartialEdgeInfo, PeerMessage, RoutingTableUpdate}; +use crate::peer_manager::peer_manager_actor::Event; +use crate::stats::metrics; +use crate::time; +use crate::types::ReasonForBan; +use near_primitives::network::{AnnounceAccount, PeerId}; +use std::sync::Arc; + +impl NetworkState { + // TODO(gprusak): eventually, this should be blocking, as it should be up to the caller + // whether to wait for the broadcast to finish, or run it in parallel with sth else. + fn broadcast_routing_table_update(&self, mut rtu: RoutingTableUpdate) { + if rtu == RoutingTableUpdate::default() { + return; + } + rtu.edges = Edge::deduplicate(rtu.edges); + let msg = Arc::new(PeerMessage::SyncRoutingTable(rtu)); + for conn in self.tier2.load().ready.values() { + conn.send_message(msg.clone()); + } + } + + /// Adds AnnounceAccounts (without validating them) to the routing table. + /// Then it broadcasts all the AnnounceAccounts that haven't been seen before. + pub async fn add_accounts(self: &Arc, accounts: Vec) { + let this = self.clone(); + self.spawn(async move { + let new_accounts = this.graph.routing_table.add_accounts(accounts); + tracing::debug!(target: "network", account_id = ?this.config.validator.as_ref().map(|v|v.account_id()), ?new_accounts, "Received new accounts"); + this.broadcast_routing_table_update(RoutingTableUpdate::from_accounts( + new_accounts.clone(), + )); + this.config.event_sink.push(Event::AccountsAdded(new_accounts)); + }).await.unwrap() + } + + /// Constructs a partial edge to the given peer with the nonce specified. + /// If nonce is None, nonce is selected automatically. + pub fn propose_edge(&self, peer1: &PeerId, with_nonce: Option) -> PartialEdgeInfo { + // When we create a new edge we increase the latest nonce by 2 in case we miss a removal + // proposal from our partner. + let nonce = with_nonce.unwrap_or_else(|| { + self.graph.load().local_edges.get(peer1).map_or(1, |edge| edge.next()) + }); + PartialEdgeInfo::new(&self.config.node_id(), peer1, nonce, &self.config.node_key) + } + + /// Constructs an edge from the partial edge constructed by the peer, + /// adds it to the graph and then broadcasts it. + pub async fn finalize_edge( + self: &Arc, + clock: &time::Clock, + peer_id: PeerId, + edge_info: PartialEdgeInfo, + ) -> Result { + let edge = Edge::build_with_secret_key( + self.config.node_id(), + peer_id.clone(), + edge_info.nonce, + &self.config.node_key, + edge_info.signature, + ); + self.add_edges(&clock, vec![edge.clone()]).await?; + Ok(edge) + } + + /// Validates edges, then adds them to the graph and then broadcasts all the edges that + /// hasn't been observed before. Returns an error iff any edge was invalid. Even if an + /// error was returned some of the valid input edges might have been added to the graph. + pub async fn add_edges( + self: &Arc, + clock: &time::Clock, + edges: Vec, + ) -> Result<(), ReasonForBan> { + // TODO(gprusak): sending duplicate edges should be considered a malicious behavior + // instead, however that would be backward incompatible, so it can be introduced in + // PROTOCOL_VERSION 60 earliest. + let (edges, ok) = self.graph.verify(edges).await; + let this = self.clone(); + let clock = clock.clone(); + let _ = self + .add_edges_demux + .call(edges, |edges: Vec>| async move { + let results: Vec<_> = edges.iter().map(|_| ()).collect(); + let edges: Vec<_> = edges.into_iter().flatten().collect(); + let mut edges = this.graph.update(&clock, edges).await; + // Don't send tombstones during the initial time. + // Most of the network is created during this time, which results + // in us sending a lot of tombstones to peers. + // Later, the amount of new edges is a lot smaller. + if let Some(skip_tombstones_duration) = this.config.skip_tombstones { + if clock.now() < this.created_at + skip_tombstones_duration { + edges.retain(|edge| edge.edge_type() == EdgeState::Active); + metrics::EDGE_TOMBSTONE_SENDING_SKIPPED.inc(); + } + } + // Broadcast new edges to all other peers. + this.broadcast_routing_table_update(RoutingTableUpdate::from_edges(edges)); + results + }) + .await; + // self.graph.verify() returns a partial result if it encountered an invalid edge: + // Edge verification is expensive, and it would be an attack vector if we dropped on the + // floor valid edges verified so far: an attacker could prepare a SyncRoutingTable + // containing a lot of valid edges, except for the last one, and send it repeatedly to a + // node. The node would then validate all the edges every time, then reject the whole set + // because just the last edge was invalid. Instead, we accept all the edges verified so + // far and return an error only afterwards. + match ok { + true => Ok(()), + false => Err(ReasonForBan::InvalidEdge), + } + } +} diff --git a/chain/network/src/peer_manager/peer_manager_actor.rs b/chain/network/src/peer_manager/peer_manager_actor.rs index 1f069a8f792..aff0aa0b5d3 100644 --- a/chain/network/src/peer_manager/peer_manager_actor.rs +++ b/chain/network/src/peer_manager/peer_manager_actor.rs @@ -9,7 +9,6 @@ use crate::peer::peer_actor::PeerActor; use crate::peer_manager::connection; use crate::peer_manager::network_state::{NetworkState, WhitelistNode}; use crate::peer_manager::peer_store; -use crate::private_actix::StopMsg; use crate::routing; use crate::stats::metrics; use crate::store; @@ -23,10 +22,7 @@ use crate::types::{ use actix::fut::future::wrap_future; use actix::{Actor as _, AsyncContext as _}; use anyhow::Context as _; -use near_o11y::{ - handler_debug_span, handler_trace_span, OpenTelemetrySpanExt, WithSpanContext, - WithSpanContextExt, -}; +use near_o11y::{handler_debug_span, handler_trace_span, OpenTelemetrySpanExt, WithSpanContext}; use near_performance_metrics_macros::perf; use near_primitives::block::GenesisId; use near_primitives::network::{AnnounceAccount, PeerId}; @@ -47,9 +43,6 @@ use tracing::Instrument as _; const EXPONENTIAL_BACKOFF_RATIO: f64 = 1.1; /// The initial waiting time between consecutive attempts to establish connection const MONITOR_PEERS_INITIAL_DURATION: time::Duration = time::Duration::milliseconds(10); -/// How often should we update the routing table -pub(crate) const UPDATE_ROUTING_TABLE_INTERVAL: time::Duration = - time::Duration::milliseconds(1_000); /// How often should we check wheter local edges match the connection pool. const FIX_LOCAL_EDGES_INTERVAL: time::Duration = time::Duration::seconds(60); /// How much time we give fix_local_edges() to resolve the discrepancies, before forcing disconnect. @@ -63,11 +56,6 @@ const REPORT_BANDWIDTH_STATS_TRIGGER_INTERVAL: time::Duration = const REPORT_BANDWIDTH_THRESHOLD_BYTES: usize = 10_000_000; /// If we received more than REPORT_BANDWIDTH_THRESHOLD_COUNT` of messages from given peer it's bandwidth stats will be reported. const REPORT_BANDWIDTH_THRESHOLD_COUNT: usize = 10_000; -/// How long a peer has to be unreachable, until we prune it from the in-memory graph. -const PRUNE_UNREACHABLE_PEERS_AFTER: time::Duration = time::Duration::hours(1); - -/// Remove the edges that were created more that this duration ago. -const PRUNE_EDGES_AFTER: time::Duration = time::Duration::minutes(30); /// If a peer is more than these blocks behind (comparing to our current head) - don't route any messages through it. /// We are updating the list of unreliable peers every MONITOR_PEER_MAX_DURATION (60 seconds) - so the current @@ -77,7 +65,7 @@ const PRUNE_EDGES_AFTER: time::Duration = time::Duration::minutes(30); const UNRELIABLE_PEER_HORIZON: u64 = 60; /// Due to implementation limits of `Graph` in `near-network`, we support up to 128 client. -pub const MAX_NUM_PEERS: usize = 128; +pub const MAX_TIER2_PEERS: usize = 128; /// When picking a peer to connect to, we'll pick from the 'safer peers' /// (a.k.a. ones that we've been connected to in the past) with these odds. @@ -109,7 +97,6 @@ pub enum Event { EdgesVerified(Vec), Ping(Ping), Pong(Pong), - SetChainInfo, // Reported once a message has been processed. // In contrast to typical RPC protocols, many P2P messages do not trigger // sending a response at the end of processing. @@ -180,24 +167,6 @@ impl actix::Actor for PeerManagerActor { (MONITOR_PEERS_INITIAL_DURATION, self.state.config.monitor_peers_max_period), ); - let state = self.state.clone(); - let clock = self.clock.clone(); - ctx.spawn(wrap_future(async move { - let mut interval = time::Interval::new(clock.now(), UPDATE_ROUTING_TABLE_INTERVAL); - loop { - interval.tick(&clock).await; - let _timer = metrics::PEER_MANAGER_TRIGGER_TIME - .with_label_values(&["update_routing_table"]) - .start_timer(); - state - .update_routing_table( - clock.now().checked_sub(PRUNE_UNREACHABLE_PEERS_AFTER), - clock.now_utc().checked_sub(PRUNE_EDGES_AFTER), - ) - .await; - } - })); - let clock = self.clock.clone(); let state = self.state.clone(); ctx.spawn(wrap_future(async move { @@ -216,7 +185,6 @@ impl actix::Actor for PeerManagerActor { fn stopping(&mut self, _ctx: &mut Self::Context) -> actix::Running { tracing::warn!("PeerManager: stopping"); self.state.tier2.broadcast_message(Arc::new(PeerMessage::Disconnect)); - self.state.routing_table_addr.do_send(StopMsg {}.with_span_context()); actix::Running::Stop } @@ -568,7 +536,7 @@ impl PeerManagerActor { // Find peers that are not reliable (too much behind) - and make sure that we're not routing messages through them. let unreliable_peers = self.unreliable_peers(); metrics::PEER_UNRELIABLE.set(unreliable_peers.len() as i64); - self.state.graph.write().set_unreliable_peers(unreliable_peers); + self.state.graph.set_unreliable_peers(unreliable_peers); let new_interval = min(max_interval, interval * EXPONENTIAL_BACKOFF_RATIO); @@ -635,7 +603,8 @@ impl PeerManagerActor { .sum(), known_producers: self .state - .routing_table_view + .graph + .routing_table .get_announce_accounts() .into_iter() .map(|announce_account| KnownProducer { @@ -643,7 +612,7 @@ impl PeerManagerActor { peer_id: announce_account.peer_id.clone(), // TODO: fill in the address. addr: None, - next_hops: self.state.routing_table_view.view_route(&announce_account.peer_id), + next_hops: self.state.graph.routing_table.view_route(&announce_account.peer_id), }) .collect(), tier1_accounts: self.state.accounts_data.load().data.values().cloned().collect(), @@ -933,7 +902,7 @@ impl PeerManagerActor { } // TEST-ONLY PeerManagerMessageRequest::FetchRoutingTable => { - PeerManagerMessageResponse::FetchRoutingTable(self.state.routing_table_view.info()) + PeerManagerMessageResponse::FetchRoutingTable(self.state.graph.routing_table.info()) } // TEST-ONLY PeerManagerMessageRequest::PingTo { nonce, target } => { @@ -967,29 +936,19 @@ impl actix::Handler> for PeerManagerActor { impl actix::Handler> for PeerManagerActor { type Result = (); fn handle(&mut self, msg: WithSpanContext, ctx: &mut Self::Context) { - let (_span, info) = handler_trace_span!(target: "network", msg); + let (_span, SetChainInfo(info)) = handler_trace_span!(target: "network", msg); let _timer = metrics::PEER_MANAGER_MESSAGES_TIME.with_label_values(&["SetChainInfo"]).start_timer(); - let SetChainInfo(info) = info; - let state = self.state.clone(); - // We set state.chain_info and call accounts_data.set_keys + // We call self.state.set_chain_info() // synchronously, therefore, assuming actix in-order delivery, // there will be no race condition between subsequent SetChainInfo // calls. - state.chain_info.store(Arc::new(Some(info.clone()))); - - // If tier1 is not enabled, we skip set_keys() call. - // This way self.state.accounts_data is always empty, hence no data - // will be collected or broadcasted. - if state.config.tier1.is_none() { - state.config.event_sink.push(Event::SetChainInfo); - return; - } - // If the key set didn't change, early exit. - if !state.accounts_data.set_keys(info.tier1_accounts.clone()) { - state.config.event_sink.push(Event::SetChainInfo); + if !self.state.set_chain_info(info) { + // We early exit in case the set of TIER1 account keys hasn't changed. return; } + + let state = self.state.clone(); let clock = self.clock.clone(); ctx.spawn(wrap_future( async move { @@ -1005,7 +964,6 @@ impl actix::Handler> for PeerManagerActor { // that our peers know about. tier1_advertise_proxies() has a side effect // of asking peers for a full sync of the accounts_data with the TIER2 peers. state.tier1_advertise_proxies(&clock).await; - state.config.event_sink.push(Event::SetChainInfo); } .in_current_span(), )); @@ -1067,10 +1025,10 @@ impl actix::Handler for PeerManagerActor { edges: self .state .graph - .read() - .edges() - .iter() - .map(|(_, edge)| { + .load() + .edges + .values() + .map(|edge| { let key = edge.key(); EdgeView { peer0: key.0.clone(), peer1: key.1.clone(), nonce: edge.nonce() } }) diff --git a/chain/network/src/peer_manager/testonly.rs b/chain/network/src/peer_manager/testonly.rs index 95af4eb7257..4ed08b551a1 100644 --- a/chain/network/src/peer_manager/testonly.rs +++ b/chain/network/src/peer_manager/testonly.rs @@ -7,7 +7,6 @@ use crate::network_protocol::{ use crate::peer; use crate::peer::peer_actor::ClosingReason; use crate::peer_manager::network_state::NetworkState; -use crate::peer_manager::peer_manager_actor; use crate::peer_manager::peer_manager_actor::Event as PME; use crate::tcp; use crate::test_utils; @@ -136,24 +135,29 @@ impl ActorHandler { } } - pub async fn connect_to(&self, peer_info: &PeerInfo) { - let stream = tcp::Stream::connect(peer_info).await.unwrap(); - let mut events = self.events.from_now(); - let stream_id = stream.id(); - self.actix - .addr - .do_send(PeerManagerMessageRequest::OutboundTcpConnect(stream).with_span_context()); - events - .recv_until(|ev| match &ev { - Event::PeerManager(PME::HandshakeCompleted(ev)) if ev.stream_id == stream_id => { - Some(()) - } - Event::PeerManager(PME::ConnectionClosed(ev)) if ev.stream_id == stream_id => { - panic!("PeerManager rejected the handshake") - } - _ => None, - }) - .await; + pub fn connect_to(&self, peer_info: &PeerInfo) -> impl 'static + Send + Future { + let addr = self.actix.addr.clone(); + let events = self.events.clone(); + let peer_info = peer_info.clone(); + async move { + let stream = tcp::Stream::connect(&peer_info).await.unwrap(); + let mut events = events.from_now(); + let stream_id = stream.id(); + addr.do_send(PeerManagerMessageRequest::OutboundTcpConnect(stream).with_span_context()); + events + .recv_until(|ev| match &ev { + Event::PeerManager(PME::HandshakeCompleted(ev)) + if ev.stream_id == stream_id => + { + Some(()) + } + Event::PeerManager(PME::ConnectionClosed(ev)) if ev.stream_id == stream_id => { + panic!("PeerManager rejected the handshake") + } + _ => None, + }) + .await + } } pub async fn with_state>( @@ -191,7 +195,6 @@ impl ActorHandler { network: network_cfg, chain, force_encoding: Some(Encoding::Proto), - nonce: None, }, }; // Wait until the TCP connection is accepted or rejected. @@ -229,7 +232,6 @@ impl ActorHandler { network: network_cfg, chain, force_encoding: Some(Encoding::Proto), - nonce: None, }, }; // Wait until the handshake started or connection is closed. @@ -273,9 +275,10 @@ impl ActorHandler { // Check that the local_edges of the graph match the TIER2 connection pool. let node_id = s.config.node_id(); let local_edges: HashSet<_> = s - .routing_table_view - .get_local_edges() - .iter() + .graph + .load() + .local_edges + .values() .filter_map(|e| match e.edge_type() { EdgeState::Active => Some(e.other(&node_id).unwrap().clone()), EdgeState::Removed => None, @@ -291,15 +294,8 @@ impl ActorHandler { self.with_state(move |s| async move { s.fix_local_edges(&clock, timeout).await }).await } - pub async fn set_chain_info(&self, chain_info: ChainInfo) { - let mut events = self.events.from_now(); - self.actix.addr.send(SetChainInfo(chain_info).with_span_context()).await.unwrap(); - events - .recv_until(|ev| match ev { - Event::PeerManager(PME::SetChainInfo) => Some(()), - _ => None, - }) - .await; + pub async fn set_chain_info(&self, chain_info: ChainInfo) -> bool { + self.with_state(move |s| async move { s.set_chain_info(chain_info) }).await } pub async fn tier1_advertise_proxies( @@ -341,27 +337,19 @@ impl ActorHandler { } // Awaits until the routing_table matches `want`. - pub async fn wait_for_routing_table( - &self, - clock: &mut time::FakeClock, - want: &[(PeerId, Vec)], - ) { + pub async fn wait_for_routing_table(&self, want: &[(PeerId, Vec)]) { let mut events = self.events.from_now(); loop { let got = - self.with_state(|s| async move { s.routing_table_view.info().next_hops }).await; + self.with_state(|s| async move { s.graph.routing_table.info().next_hops }).await; if test_utils::expected_routing_tables(&got, want) { return; } events .recv_until(|ev| match ev { - Event::PeerManager(PME::RoutingTableUpdate { .. }) => Some(()), Event::PeerManager(PME::MessageProcessed(PeerMessage::SyncRoutingTable { .. - })) => { - clock.advance(peer_manager_actor::UPDATE_ROUTING_TABLE_INTERVAL); - None - } + })) => Some(()), _ => None, }) .await; @@ -373,7 +361,9 @@ impl ActorHandler { loop { let account = account.clone(); let got = self - .with_state(|s| async move { s.routing_table_view.account_owner(&account).clone() }) + .with_state( + |s| async move { s.graph.routing_table.account_owner(&account).clone() }, + ) .await; if let Some(got) = got { return got; diff --git a/chain/network/src/peer_manager/tests/accounts_data.rs b/chain/network/src/peer_manager/tests/accounts_data.rs index 862de1dea94..babf946666c 100644 --- a/chain/network/src/peer_manager/tests/accounts_data.rs +++ b/chain/network/src/peer_manager/tests/accounts_data.rs @@ -15,6 +15,14 @@ use rand::seq::SliceRandom as _; use std::collections::HashSet; use std::sync::Arc; +/// Each actix arbiter (in fact, the underlying tokio runtime) creates 4 file descriptors: +/// 1. eventfd2() +/// 2. epoll_create1() +/// 3. fcntl() duplicating one end of some globally shared socketpair() +/// 4. fcntl() duplicating epoll socket created in (2) +/// This gives 5 file descriptors per PeerActor (4 + 1 TCP socket). +const FDS_PER_PEER: usize = 5; + #[tokio::test] async fn broadcast() { init_test_logger(); @@ -158,17 +166,12 @@ async fn gradual_epoch_change() { #[tokio::test(flavor = "multi_thread")] async fn rate_limiting() { init_test_logger(); - // Each actix arbiter (in fact, the underlying tokio runtime) creates 4 file descriptors: - // 1. eventfd2() - // 2. epoll_create1() - // 3. fcntl() duplicating one end of some globally shared socketpair() - // 4. fcntl() duplicating epoll socket created in (2) - // This gives 5 file descriptors per PeerActor (4 + 1 TCP socket). - // PeerManager (together with the whole ActixSystem) creates 13 file descriptors. - // The usual default soft limit on the number of file descriptors on linux is 1024. - // Here we adjust it appropriately to account for test requirements. + // Adjust the file descriptors limit, so that we can create many connection in the test. + const MAX_CONNECTIONS: usize = 300; let limit = rlimit::Resource::NOFILE.get().unwrap(); - rlimit::Resource::NOFILE.set(std::cmp::min(limit.1, 3000), limit.1).unwrap(); + rlimit::Resource::NOFILE + .set(std::cmp::min(limit.1, (1000 + 2 * FDS_PER_PEER * MAX_CONNECTIONS) as u64), limit.1) + .unwrap(); let mut rng = make_rng(921853233); let rng = &mut rng; @@ -194,16 +197,21 @@ async fn rate_limiting() { ); } tracing::info!(target:"test", "Construct a 4-layer bipartite graph."); + let mut connections = 0; + let mut tasks = vec![]; for i in 0..n - 1 { for j in 0..m { for k in 0..m { let pi = pms[(i + 1) * m + k].peer_info(); - pms[i * m + j].connect_to(&pi).await; + tasks.push(tokio::spawn(pms[i * m + j].connect_to(&pi))); connections += 1; } } } + for t in tasks { + t.await.unwrap(); + } // Construct ChainInfo with tier1_accounts containing all validators. let chain_info = testonly::make_chain_info(&chain, &pms.iter().collect::>()[..]); diff --git a/chain/network/src/peer_manager/tests/nonce.rs b/chain/network/src/peer_manager/tests/nonce.rs index 1e6b18a4a32..e69e60acf68 100644 --- a/chain/network/src/peer_manager/tests/nonce.rs +++ b/chain/network/src/peer_manager/tests/nonce.rs @@ -1,11 +1,15 @@ use crate::network_protocol::testonly as data; -use crate::network_protocol::{Encoding, EDGE_MIN_TIMESTAMP_NONCE}; -use crate::peer; +use crate::network_protocol::{ + Encoding, Handshake, PartialEdgeInfo, PeerMessage, EDGE_MIN_TIMESTAMP_NONCE, +}; use crate::peer_manager; use crate::tcp; use crate::testonly::make_rng; +use crate::testonly::stream; use crate::time; use near_o11y::testonly::init_test_logger; +use near_primitives::network::PeerId; +use near_primitives::version; use std::sync::Arc; // Nonces must be odd (as even ones are reserved for tombstones). @@ -27,50 +31,59 @@ async fn test_nonces() { let mut clock = time::FakeClock::new(*EDGE_MIN_TIMESTAMP_NONCE + time::Duration::days(2)); let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); - // Start a PeerManager and connect a peer to it. - let pm = peer_manager::testonly::start( - clock.clock(), - near_store::db::TestDB::new(), - chain.make_config(rng), - chain.clone(), - ) - .await; - let test_cases = [ // Try to connect with peer with a valid nonce (current timestamp). - (Some(to_active_nonce(clock.now_utc())), true, "current timestamp"), + (to_active_nonce(clock.now_utc()), true, "current timestamp"), // Now try the peer with invalid timestamp (in the past) - (Some(to_active_nonce(clock.now_utc() - time::Duration::days(1))), false, "past timestamp"), + (to_active_nonce(clock.now_utc() - time::Duration::days(1)), false, "past timestamp"), // Now try the peer with invalid timestamp (in the future) - ( - Some(to_active_nonce(clock.now_utc() + time::Duration::days(1))), - false, - "future timestamp", - ), - (Some(u64::MAX), false, "u64 max"), - (Some(i64::MAX as u64), false, "i64 max"), - (Some((i64::MAX - 1) as u64), false, "i64 max - 1"), - (Some(253402300799), false, "Max time"), - (Some(253402300799 + 2), false, "Over max time"), + (to_active_nonce(clock.now_utc() + time::Duration::days(1)), false, "future timestamp"), + (u64::MAX, false, "u64 max"), + (i64::MAX as u64, false, "i64 max"), + ((i64::MAX - 1) as u64, false, "i64 max - 1"), + (253402300799, false, "Max time"), + (253402300799 + 2, false, "Over max time"), //(Some(0), false, "Nonce 0"), - (None, true, "Nonce 1"), + (1, true, "Nonce 1"), ]; for test in test_cases { tracing::info!(target: "test", "Running test {:?}", test.2); - let cfg = peer::testonly::PeerConfig { - network: chain.make_config(rng), - chain: chain.clone(), - force_encoding: Some(Encoding::Proto), - // Connect with nonce equal to unix timestamp - nonce: test.0, - }; + // Start a PeerManager and connect a peer to it. + let pm = peer_manager::testonly::start( + clock.clock(), + near_store::db::TestDB::new(), + chain.make_config(rng), + chain.clone(), + ) + .await; + let stream = tcp::Stream::connect(&pm.peer_info()).await.unwrap(); - let mut peer = peer::testonly::PeerHandle::start_endpoint(clock.clock(), cfg, stream).await; + let mut stream = stream::Stream::new(Some(Encoding::Proto), stream); + let peer_key = data::make_secret_key(rng); + let peer_id = PeerId::new(peer_key.public_key()); + let handshake = PeerMessage::Handshake(Handshake { + protocol_version: version::PROTOCOL_VERSION, + oldest_supported_version: version::PEER_MIN_ALLOWED_PROTOCOL_VERSION, + sender_peer_id: peer_id.clone(), + target_peer_id: pm.cfg.node_id(), + // we have to set this even if we have no intention of listening since otherwise + // the peer will drop our connection + sender_listen_port: Some(24567), + sender_chain_info: chain.get_peer_chain_info(), + partial_edge_info: PartialEdgeInfo::new(&peer_id, &pm.cfg.node_id(), test.0, &peer_key), + }); + stream.write(&handshake).await; if test.1 { - peer.complete_handshake().await; + match stream.read().await { + Ok(PeerMessage::Handshake { .. }) => {} + got => panic!("got = {got:?}, want Handshake"), + } } else { - peer.fail_handshake().await; + match stream.read().await { + Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {} + got => panic!("got = {got:?}, want UnexpectedEof"), + } } } } diff --git a/chain/network/src/peer_manager/tests/routing.rs b/chain/network/src/peer_manager/tests/routing.rs index 61f3379d4e4..0dac8a239a8 100644 --- a/chain/network/src/peer_manager/tests/routing.rs +++ b/chain/network/src/peer_manager/tests/routing.rs @@ -37,12 +37,11 @@ async fn ttl() { network: chain.make_config(rng), chain, force_encoding: Some(Encoding::Proto), - nonce: None, }; let stream = tcp::Stream::connect(&pm.peer_info()).await.unwrap(); let mut peer = peer::testonly::PeerHandle::start_endpoint(clock.clock(), cfg, stream).await; peer.complete_handshake().await; - pm.wait_for_routing_table(&mut clock, &[(peer.cfg.id(), vec![peer.cfg.id()])]).await; + pm.wait_for_routing_table(&[(peer.cfg.id(), vec![peer.cfg.id()])]).await; for ttl in 0..5 { let msg = RoutedMessageBody::Ping(Ping { nonce: rng.gen(), source: peer.cfg.id() }); @@ -92,7 +91,6 @@ async fn repeated_data_in_sync_routing_table() { network: chain.make_config(rng), chain, force_encoding: Some(Encoding::Proto), - nonce: None, }; let stream = tcp::Stream::connect(&pm.peer_info()).await.unwrap(); let mut peer = peer::testonly::PeerHandle::start_endpoint(clock.clock(), cfg, stream).await; @@ -158,11 +156,13 @@ async fn wait_for_edges( want: &HashSet, ) { let mut got = HashSet::new(); + tracing::info!(target: "test", "want edges: {:?}",want.iter().map(|e|e.hash()).collect::>()); while &got != want { match events.recv().await { peer::testonly::Event::Network(PME::MessageProcessed( PeerMessage::SyncRoutingTable(msg), )) => { + tracing::info!(target: "test", "got edges: {:?}",msg.edges.iter().map(|e|e.hash()).collect::>()); got.extend(msg.edges); assert!(want.is_superset(&got), "want: {:#?}, got: {:#?}", want, got); } @@ -260,34 +260,31 @@ async fn square() { let id2 = pm2.cfg.node_id(); let id3 = pm3.cfg.node_id(); - pm0.wait_for_routing_table( - &mut clock, - &[ - (id1.clone(), vec![id1.clone()]), - (id3.clone(), vec![id3.clone()]), - (id2.clone(), vec![id1.clone(), id3.clone()]), - ], - ) + pm0.wait_for_routing_table(&[ + (id1.clone(), vec![id1.clone()]), + (id3.clone(), vec![id3.clone()]), + (id2.clone(), vec![id1.clone(), id3.clone()]), + ]) .await; tracing::info!(target:"test","stop {id1}"); drop(pm1); tracing::info!(target:"test","wait for {id0} routing table"); - pm0.wait_for_routing_table( - &mut clock, - &[(id3.clone(), vec![id3.clone()]), (id2.clone(), vec![id3.clone()])], - ) + pm0.wait_for_routing_table(&[ + (id3.clone(), vec![id3.clone()]), + (id2.clone(), vec![id3.clone()]), + ]) .await; tracing::info!(target:"test","wait for {id2} routing table"); - pm2.wait_for_routing_table( - &mut clock, - &[(id3.clone(), vec![id3.clone()]), (id0.clone(), vec![id3.clone()])], - ) + pm2.wait_for_routing_table(&[ + (id3.clone(), vec![id3.clone()]), + (id0.clone(), vec![id3.clone()]), + ]) .await; tracing::info!(target:"test","wait for {id3} routing table"); - pm3.wait_for_routing_table( - &mut clock, - &[(id2.clone(), vec![id2.clone()]), (id0.clone(), vec![id0.clone()])], - ) + pm3.wait_for_routing_table(&[ + (id2.clone(), vec![id2.clone()]), + (id0.clone(), vec![id0.clone()]), + ]) .await; drop(pm0); drop(pm2); @@ -302,8 +299,7 @@ async fn fix_local_edges() { let mut clock = time::FakeClock::default(); let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); - let mut pm = - start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; let conn = pm .start_inbound(chain.clone(), chain.make_config(rng)) .await @@ -323,23 +319,21 @@ async fn fix_local_edges() { ])); tracing::info!(target:"test", "waiting for fake edges to be processed"); - conn.send(msg).await; - let mut got = HashSet::new(); - let want = [&edge1, &edge2].into_iter().cloned().collect(); - while !got.is_superset(&want) { - pm.events - .recv_until(|ev| match ev { - Event::PeerManager(PME::EdgesVerified(edges)) => Some(got.extend(edges)), - _ => None, - }) - .await; - } + let mut events = pm.events.from_now(); + conn.send(msg.clone()).await; + events + .recv_until(|ev| match ev { + Event::PeerManager(PME::MessageProcessed(got)) if got == msg => Some(()), + _ => None, + }) + .await; tracing::info!(target:"test","waiting for fake edges to be fixed"); + let mut events = pm.events.from_now(); pm.fix_local_edges(&clock.clock(), time::Duration::ZERO).await; // TODO(gprusak): make fix_local_edges await closing of the connections, so // that we don't have to wait for it explicitly here. - pm.events + events .recv_until(|ev| match ev { Event::PeerManager(PME::ConnectionClosed { .. }) => Some(()), _ => None, diff --git a/chain/network/src/routing/actor.rs b/chain/network/src/routing/actor.rs deleted file mode 100644 index 26dc4f0f4b9..00000000000 --- a/chain/network/src/routing/actor.rs +++ /dev/null @@ -1,271 +0,0 @@ -use crate::network_protocol::Edge; -use crate::private_actix::StopMsg; -use crate::routing; -use crate::stats::metrics; -use crate::store; -use crate::time; -use actix::{Actor as _, ActorContext as _, Context, Running}; -use near_o11y::{handler_debug_span, handler_trace_span, OpenTelemetrySpanExt, WithSpanContext}; -use near_performance_metrics_macros::perf; -use near_primitives::network::PeerId; -use parking_lot::RwLock; -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; -use tracing::warn; - -/// Actor that maintains routing table information. -/// -/// We store the following information -/// - routing information (where a message should be send to reach given peer) -/// -/// We use store for following reasons: -/// - store removed edges to disk -/// - we currently don't store active edges to disk -pub(crate) struct Actor { - clock: time::Clock, - my_peer_id: PeerId, - // routing::Actor is the only actor which is allowed to update the Graph. - // Other actors (namely PeerManagerActor) are supposed to have read-only access to it. - // Graph is locked for writes only to update a bunch of edges. - // You must not put expensive computations under the lock and/or DB access. - // TODO: Reimplement GraphWithCache to GraphWithCache(Arc>), - // to enforce the constraint above. - graph: Arc>, - store: store::Store, - /// Last time a peer was reachable. - peer_reachable_at: HashMap, -} - -impl Actor { - pub(super) fn new( - clock: time::Clock, - store: store::Store, - graph: Arc>, - ) -> Self { - let my_peer_id = graph.read().my_peer_id(); - Self { clock, my_peer_id, graph, store, peer_reachable_at: Default::default() } - } - - pub fn spawn( - clock: time::Clock, - store: store::Store, - graph: Arc>, - ) -> actix::Addr { - let arbiter = actix::Arbiter::new(); - Actor::start_in_arbiter(&arbiter.handle(), |_| Self::new(clock, store, graph)) - } - - /// Add several edges to the current view of the network. - /// These edges are assumed to have been verified at this point. - /// Each edge actually represents a "version" of an edge, identified by Edge.nonce. - /// Returns a list of edges (versions) which were not previously observed. Requires DB access. - /// - /// Everytime we remove an edge we store all edges removed at given time to disk. - /// If new edge comes comes that is adjacent to a peer that has been previously removed, - /// we will try to re-add edges previously removed from disk. - pub fn add_verified_edges(&mut self, edges: Vec) -> Vec { - let total = edges.len(); - // load the components BEFORE graph.update_edges - // so that result doesn't contain edges we already have in storage. - // It is especially important for initial full sync with peers, because - // we broadcast all the returned edges to all connected peers. - for edge in &edges { - let key = edge.key(); - self.load_component(&key.0); - self.load_component(&key.1); - } - let edges = self.graph.write().update_edges(edges); - // Update metrics after edge update - metrics::EDGE_UPDATES.inc_by(total as u64); - metrics::EDGE_ACTIVE.set(self.graph.read().total_active_edges() as i64); - metrics::EDGE_TOTAL.set(self.graph.read().edges().len() as i64); - edges - } - - /// If peer_id is not in memory check if it is on disk in bring it back on memory. - /// - /// Note: here an advanced example, which shows what's happening. - /// Let's say we have a full graph fully connected with nodes `A, B, C, D`. - /// Step 1 ) `A`, `B` get removed. - /// We store edges belonging to `A` and `B`: `, , , , ` - /// into component 1 let's call it `C_1`. - /// And mapping from `A` to `C_1`, and from `B` to `C_1` - /// - /// Note that `C`, `D` is still active. - /// - /// Step 2) 'C' gets removed. - /// We stored edges into component 2 `C_2`. - /// And a mapping from `C` to `C_2`. - /// - /// Note that `D` is still active. - /// - /// Step 3) An active edge gets added from `D` to `A`. - /// We will load `C_1` and try to re-add all edges belonging to `C_1`. - /// We will add `, , , , ` - /// - /// Important note: `C_1` also contains an edge from `A` to `C`, though `C` was removed in `C_2`. - /// - 1) We will not load edges belonging to `C_2`, even though we are adding an edges from `A` to deleted `C`. - /// - 2) We will not delete mapping from `C` to `C_2`, because `C` doesn't belong to `C_1`. - /// - 3) Later, `C` will be deleted, because we will figure out it's not reachable. - /// New component `C_3` will be created. - /// And mapping from `C` to `C_2` will be overridden by mapping from `C` to `C_3`. - /// And therefore `C_2` component will become unreachable. - /// TODO(gprusak): this whole algorithm seems to be leaking stuff to storage and never cleaning up. - /// What is the point of it? What does it actually gives us? - fn load_component(&mut self, peer_id: &PeerId) { - if *peer_id == self.my_peer_id || self.peer_reachable_at.contains_key(peer_id) { - return; - } - let edges = match self.store.pop_component(peer_id) { - Ok(edges) => edges, - Err(e) => { - warn!("self.store.pop_component({}): {}", peer_id, e); - return; - } - }; - self.graph.write().update_edges(edges); - } - - /// Prunes peers unreachable since (and their adjacent edges) - /// from the in-mem graph and stores them in DB. - /// # Returns - /// List of edges removed. - pub(crate) fn prune_unreachable_peers( - &mut self, - unreachable_since: time::Instant, - ) -> Vec { - let _d = delay_detector::DelayDetector::new(|| "pruning unreachable peers".into()); - - // Select peers to prune. - let mut peers = HashSet::new(); - for (k, _) in self.graph.read().edges() { - for peer_id in [&k.0, &k.1] { - if self - .peer_reachable_at - .get(peer_id) - .map(|t| t < &unreachable_since) - .unwrap_or(true) - { - peers.insert(peer_id.clone()); - } - } - } - if peers.is_empty() { - return vec![]; - } - - // Prune peers from peer_reachable_at. - for peer_id in &peers { - self.peer_reachable_at.remove(&peer_id); - } - - // Prune edges from graph. - let edges = self.graph.write().remove_adjacent_edges(&peers); - - // Store the pruned data in DB. - if let Err(e) = self.store.push_component(&peers, &edges) { - warn!("self.store.push_component(): {}", e); - } - edges - } - - /// update_routing_table - /// 1. recomputes the routing table (if needed) - /// 2. bumps peer_reachable_at to now() for peers which are still reachable. - /// 3. prunes peers which are unreachable `prune_unreachable_since`. - /// Returns the new routing table and the pruned edges - adjacent to the pruned peers. - /// Should be called periodically. - pub fn update_routing_table( - &mut self, - prune_unreachable_since: Option, - prune_edges_older_than: Option, - ) -> (Arc, Vec) { - if let Some(prune_edges_older_than) = prune_edges_older_than { - self.graph.write().prune_old_edges(prune_edges_older_than) - } - let next_hops = self.graph.read().next_hops(); - // Update peer_reachable_at. - let now = self.clock.now(); - self.peer_reachable_at.insert(self.my_peer_id.clone(), now); - for peer in next_hops.keys() { - self.peer_reachable_at.insert(peer.clone(), now); - } - let pruned_edges = match prune_unreachable_since { - None => vec![], - Some(t) => self.prune_unreachable_peers(t), - }; - (next_hops, pruned_edges) - } -} - -impl actix::Actor for Actor { - type Context = Context; - fn started(&mut self, _ctx: &mut Self::Context) {} - fn stopping(&mut self, _ctx: &mut Self::Context) -> Running { - Running::Stop - } - fn stopped(&mut self, _ctx: &mut Self::Context) { - actix::Arbiter::current().stop(); - } -} - -impl actix::Handler> for Actor { - type Result = (); - fn handle(&mut self, msg: WithSpanContext, ctx: &mut Self::Context) -> Self::Result { - let (_span, _msg) = handler_debug_span!(target: "network", msg); - ctx.stop(); - } -} - -/// Messages for `RoutingTableActor` -#[derive(actix::Message, Debug, strum::IntoStaticStr)] -#[rtype(result = "Response")] -pub(crate) enum Message { - /// Add verified edges to routing table actor and update stats. - /// Each edge contains signature of both peers. - /// We say that the edge is "verified" if and only if we checked that the `signature0` and - /// `signature1` is valid. - AddVerifiedEdges { edges: Vec }, - /// Request routing table update and maybe prune edges. - RoutingTableUpdate { - prune_unreachable_since: Option, - prune_edges_older_than: Option, - }, -} - -#[derive(actix::MessageResponse, Debug)] -pub enum Response { - Empty, - AddVerifiedEdges(Vec), - RoutingTableUpdate { - /// PeerManager maintains list of local edges. We will notify `PeerManager` - /// to remove those edges. - pruned_edges: Vec, - /// Active PeerId that are part of the shortest path to each PeerId. - next_hops: Arc, - }, -} - -impl actix::Handler> for Actor { - type Result = Response; - - #[perf] - fn handle(&mut self, msg: WithSpanContext, _ctx: &mut Self::Context) -> Self::Result { - let msg_type: &str = (&msg.msg).into(); - let (_span, msg) = handler_trace_span!(target: "network", msg, msg_type); - let _timer = - metrics::ROUTING_TABLE_MESSAGES_TIME.with_label_values(&[msg_type]).start_timer(); - match msg { - // Adds verified edges to the graph. Accesses DB. - Message::AddVerifiedEdges { edges } => { - Response::AddVerifiedEdges(self.add_verified_edges(edges)) - } - // Recalculates the routing table. - Message::RoutingTableUpdate { prune_unreachable_since, prune_edges_older_than } => { - let (next_hops, pruned_edges) = - self.update_routing_table(prune_unreachable_since, prune_edges_older_than); - Response::RoutingTableUpdate { pruned_edges, next_hops } - } - } - } -} diff --git a/chain/network/src/routing/graph.rs b/chain/network/src/routing/bfs.rs similarity index 93% rename from chain/network/src/routing/graph.rs rename to chain/network/src/routing/bfs.rs index 61bb815a870..06ca725c018 100644 --- a/chain/network/src/routing/graph.rs +++ b/chain/network/src/routing/bfs.rs @@ -1,4 +1,4 @@ -use crate::peer_manager::peer_manager_actor::MAX_NUM_PEERS; +use crate::peer_manager::peer_manager_actor::MAX_TIER2_PEERS; use near_primitives::network::PeerId; use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet, VecDeque}; @@ -9,8 +9,6 @@ use tracing::warn; /// to that are on the shortest path between us as destination `peer`. #[derive(Clone)] pub struct Graph { - /// peer_id of current peer - my_peer_id: PeerId, /// `id` as integer corresponding to `my_peer_id`. /// We use u32 to reduce both improve performance, and reduce memory usage. source_id: u32, @@ -28,15 +26,11 @@ pub struct Graph { /// Total number of edges used for stats. total_active_edges: u64, - - // Set of peers that are 'unreliable', and we should avoid routing through them. - unreliable_peers: HashSet, } impl Graph { pub fn new(source: PeerId) -> Self { let mut res = Self { - my_peer_id: source.clone(), source_id: 0, p2id: HashMap::default(), id2p: Vec::default(), @@ -44,7 +38,6 @@ impl Graph { unused: Vec::default(), adjacency: Vec::default(), total_active_edges: 0, - unreliable_peers: HashSet::default(), }; res.id2p.push(source.clone()); res.adjacency.push(Vec::default()); @@ -54,19 +47,10 @@ impl Graph { res } - pub fn my_peer_id(&self) -> &PeerId { - &self.my_peer_id - } - pub fn total_active_edges(&self) -> u64 { self.total_active_edges } - pub fn set_unreliable_peers(&mut self, unreliable_peers: HashSet) { - self.unreliable_peers = - unreliable_peers.iter().filter_map(|peer_id| self.p2id.get(peer_id).cloned()).collect(); - } - // Compute number of active edges. We divide by 2 to remove duplicates. #[cfg(test)] pub fn compute_total_active_edges(&self) -> u64 { @@ -150,9 +134,15 @@ impl Graph { /// Compute for every node `u` on the graph (other than `source`) which are the neighbors of /// `sources` which belong to the shortest path from `source` to `u`. Nodes that are /// not connected to `source` will not appear in the result. - pub fn calculate_distance(&self) -> HashMap> { + pub fn calculate_distance( + &self, + unreliable_peers: &HashSet, + ) -> HashMap> { // TODO add removal of unreachable nodes + let unreliable_peers: HashSet<_> = + unreliable_peers.iter().filter_map(|peer_id| self.p2id.get(peer_id).cloned()).collect(); + let mut queue = VecDeque::new(); let nodes = self.id2p.len(); @@ -163,8 +153,8 @@ impl Graph { { let neighbors = &self.adjacency[self.source_id as usize]; - for (id, &neighbor) in neighbors.iter().enumerate().take(MAX_NUM_PEERS) { - if !self.unreliable_peers.contains(&neighbor) { + for (id, &neighbor) in neighbors.iter().enumerate().take(MAX_TIER2_PEERS) { + if !unreliable_peers.contains(&neighbor) { queue.push_back(neighbor); } @@ -222,7 +212,7 @@ impl Graph { let peer_set = neighbors .iter() .enumerate() - .take(MAX_NUM_PEERS) + .take(MAX_TIER2_PEERS) .filter(|(id, _)| (cur_route & (1u128 << id)) != 0) .map(|(_, &neighbor)| self.id2p[neighbor as usize].clone()) .collect(); @@ -237,7 +227,7 @@ impl Graph { #[cfg(test)] mod test { - use crate::routing::graph::Graph; + use super::Graph; use crate::test_utils::{expected_routing_tables, random_peer_id}; use std::collections::HashSet; use std::ops::Not; @@ -283,7 +273,7 @@ mod test { graph.add_edge(&source, &node0); assert!(expected_routing_tables( - &graph.calculate_distance(), + &graph.calculate_distance(&HashSet::new()), &[(node0.clone(), vec![node0.clone()])], )); @@ -302,7 +292,7 @@ mod test { graph.add_edge(&nodes[2], &nodes[1]); graph.add_edge(&nodes[1], &nodes[2]); - assert!(expected_routing_tables(&graph.calculate_distance(), &[])); + assert!(expected_routing_tables(&graph.calculate_distance(&HashSet::new()), &[])); assert_eq!(2, graph.total_active_edges() as usize); assert_eq!(2, graph.compute_total_active_edges() as usize); @@ -321,7 +311,7 @@ mod test { graph.add_edge(&source, &nodes[0]); assert!(expected_routing_tables( - &graph.calculate_distance(), + &graph.calculate_distance(&HashSet::new()), &[ (nodes[0].clone(), vec![nodes[0].clone()]), (nodes[1].clone(), vec![nodes[0].clone()]), @@ -346,7 +336,7 @@ mod test { graph.add_edge(&source, &nodes[1]); assert!(expected_routing_tables( - &graph.calculate_distance(), + &graph.calculate_distance(&HashSet::new()), &[ (nodes[0].clone(), vec![nodes[0].clone()]), (nodes[1].clone(), vec![nodes[1].clone()]), @@ -398,7 +388,7 @@ mod test { next_hops.push((node.clone(), target.clone())); } - assert!(expected_routing_tables(&graph.calculate_distance(), &next_hops)); + assert!(expected_routing_tables(&graph.calculate_distance(&HashSet::new()), &next_hops)); assert_eq!(22, graph.total_active_edges() as usize); assert_eq!(22, graph.compute_total_active_edges() as usize); @@ -427,7 +417,7 @@ mod test { // Dummy edge. graph.add_edge(&nodes[9], &nodes[10]); - graph.set_unreliable_peers(HashSet::from([nodes[0].clone()])); + let unreliable_peers = HashSet::from([nodes[0].clone()]); let mut next_hops: Vec<_> = (0..3).map(|i| (nodes[i].clone(), vec![nodes[i].clone()])).collect(); @@ -437,7 +427,7 @@ mod test { next_hops.push((node.clone(), target.clone())); } - assert!(expected_routing_tables(&graph.calculate_distance(), &next_hops)); + assert!(expected_routing_tables(&graph.calculate_distance(&unreliable_peers), &next_hops)); assert_eq!(22, graph.total_active_edges() as usize); assert_eq!(22, graph.compute_total_active_edges() as usize); @@ -459,7 +449,7 @@ mod test { graph.add_edge(&nodes[3], &nodes[1]); graph.add_edge(&nodes[0], &nodes[1]); - graph.set_unreliable_peers(HashSet::from([nodes[0].clone()])); + let unreliable_peers = HashSet::from([nodes[0].clone()]); let next_hops = vec![ (nodes[0].clone(), vec![nodes[0].clone()]), @@ -467,6 +457,6 @@ mod test { (nodes[2].clone(), vec![nodes[2].clone()]), (nodes[3].clone(), vec![nodes[2].clone()]), ]; - assert!(expected_routing_tables(&graph.calculate_distance(), &next_hops)); + assert!(expected_routing_tables(&graph.calculate_distance(&unreliable_peers), &next_hops)); } } diff --git a/chain/network/src/routing/graph/mod.rs b/chain/network/src/routing/graph/mod.rs new file mode 100644 index 00000000000..57eeb6be9f6 --- /dev/null +++ b/chain/network/src/routing/graph/mod.rs @@ -0,0 +1,315 @@ +use crate::concurrency; +use crate::concurrency::runtime::Runtime; +use crate::network_protocol::{Edge, EdgeState}; +use crate::routing::bfs; +use crate::routing::routing_table_view::RoutingTableView; +use crate::stats::metrics; +use crate::store; +use crate::time; +use arc_swap::ArcSwap; +use near_primitives::network::PeerId; +use parking_lot::Mutex; +use rayon::iter::ParallelBridge; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +#[cfg(test)] +mod tests; + +// TODO: make it opaque, so that the key.0 < key.1 invariant is protected. +type EdgeKey = (PeerId, PeerId); +pub type NextHopTable = HashMap>; + +#[derive(Clone)] +pub struct GraphConfig { + pub node_id: PeerId, + pub prune_unreachable_peers_after: time::Duration, + pub prune_edges_after: Option, +} + +#[derive(Default)] +pub struct GraphSnapshot { + pub edges: im::HashMap, + pub local_edges: HashMap, + pub next_hops: Arc, +} + +struct Inner { + config: GraphConfig, + + /// Current view of the network represented by an undirected graph. + /// Contains only validated edges. + /// Nodes are Peers and edges are active connections. + graph: bfs::Graph, + + edges: im::HashMap, + /// Last time a peer was reachable. + peer_reachable_at: HashMap, + store: store::Store, +} + +fn has(set: &im::HashMap, edge: &Edge) -> bool { + set.get(&edge.key()).map_or(false, |x| x.nonce() >= edge.nonce()) +} + +impl Inner { + /// Adds an edge without validating the signatures. O(1). + /// Returns true, iff was newer than an already known version of this edge. + fn update_edge(&mut self, now: time::Utc, edge: Edge) -> bool { + if has(&self.edges, &edge) { + return false; + } + if let Some(prune_edges_after) = self.config.prune_edges_after { + // Don't add edges that are older than the limit. + if edge.is_edge_older_than(now - prune_edges_after) { + return false; + } + } + let key = edge.key(); + // Add the edge. + match edge.edge_type() { + EdgeState::Active => self.graph.add_edge(&key.0, &key.1), + EdgeState::Removed => self.graph.remove_edge(&key.0, &key.1), + } + self.edges.insert(key.clone(), edge); + true + } + + /// Removes an edge by key. O(1). + fn remove_edge(&mut self, key: &EdgeKey) { + if self.edges.remove(key).is_some() { + self.graph.remove_edge(&key.0, &key.1); + } + } + + /// Removes all edges adjacent to the peers from the set. + /// It is used to prune unreachable connected components from the inmem graph. + fn remove_adjacent_edges(&mut self, peers: &HashSet) -> Vec { + let mut edges = vec![]; + for e in self.edges.clone().values() { + if peers.contains(&e.key().0) || peers.contains(&e.key().1) { + self.remove_edge(e.key()); + edges.push(e.clone()); + } + } + edges + } + + fn prune_old_edges(&mut self, prune_edges_older_than: time::Utc) { + for e in self.edges.clone().values() { + if e.is_edge_older_than(prune_edges_older_than) { + self.remove_edge(e.key()); + } + } + } + + /// If peer_id is not in memory check if it is on disk in bring it back on memory. + /// + /// Note: here an advanced example, which shows what's happening. + /// Let's say we have a full graph fully connected with nodes `A, B, C, D`. + /// Step 1 ) `A`, `B` get removed. + /// We store edges belonging to `A` and `B`: `, , , , ` + /// into component 1 let's call it `C_1`. + /// And mapping from `A` to `C_1`, and from `B` to `C_1` + /// + /// Note that `C`, `D` is still active. + /// + /// Step 2) 'C' gets removed. + /// We stored edges into component 2 `C_2`. + /// And a mapping from `C` to `C_2`. + /// + /// Note that `D` is still active. + /// + /// Step 3) An active edge gets added from `D` to `A`. + /// We will load `C_1` and try to re-add all edges belonging to `C_1`. + /// We will add `, , , , ` + /// + /// Important note: `C_1` also contains an edge from `A` to `C`, though `C` was removed in `C_2`. + /// - 1) We will not load edges belonging to `C_2`, even though we are adding an edges from `A` to deleted `C`. + /// - 2) We will not delete mapping from `C` to `C_2`, because `C` doesn't belong to `C_1`. + /// - 3) Later, `C` will be deleted, because we will figure out it's not reachable. + /// New component `C_3` will be created. + /// And mapping from `C` to `C_2` will be overridden by mapping from `C` to `C_3`. + /// And therefore `C_2` component will become unreachable. + /// TODO(gprusak): this whole algorithm seems to be leaking stuff to storage and never cleaning up. + /// What is the point of it? What does it actually gives us? + fn load_component(&mut self, now: time::Utc, peer_id: PeerId) { + if peer_id == self.config.node_id || self.peer_reachable_at.contains_key(&peer_id) { + return; + } + let edges = match self.store.pop_component(&peer_id) { + Ok(edges) => edges, + Err(e) => { + tracing::warn!("self.store.pop_component({}): {}", peer_id, e); + return; + } + }; + for e in edges { + self.update_edge(now, e); + } + } + + /// Prunes peers unreachable since (and their adjacent edges) + /// from the in-mem graph and stores them in DB. + fn prune_unreachable_peers(&mut self, unreachable_since: time::Instant) { + // Select peers to prune. + let mut peers = HashSet::new(); + for k in self.edges.keys() { + for peer_id in [&k.0, &k.1] { + if self + .peer_reachable_at + .get(peer_id) + .map(|t| t < &unreachable_since) + .unwrap_or(true) + { + peers.insert(peer_id.clone()); + } + } + } + if peers.is_empty() { + return; + } + + // Prune peers from peer_reachable_at. + for peer_id in &peers { + self.peer_reachable_at.remove(&peer_id); + } + + // Prune edges from graph. + let edges = self.remove_adjacent_edges(&peers); + + // Store the pruned data in DB. + if let Err(e) = self.store.push_component(&peers, &edges) { + tracing::warn!("self.store.push_component(): {}", e); + } + } + + /// 1. Adds edges to the graph (edges are expected to be already validated). + /// 2. Prunes expired edges. + /// 3. Prunes unreachable graph components. + /// 4. Recomputes GraphSnapshot. + /// Returns a subset of `edges`, consisting of edges which were not in the graph before. + pub fn update( + &mut self, + clock: &time::Clock, + mut edges: Vec, + unreliable_peers: &HashSet, + ) -> (Vec, GraphSnapshot) { + let _update_time = metrics::ROUTING_TABLE_RECALCULATION_HISTOGRAM.start_timer(); + let total = edges.len(); + // load the components BEFORE updating the edges. + // so that result doesn't contain edges we already have in storage. + // It is especially important for initial full sync with peers, because + // we broadcast all the returned edges to all connected peers. + let now = clock.now_utc(); + for edge in &edges { + let key = edge.key(); + self.load_component(now, key.0.clone()); + self.load_component(now, key.1.clone()); + } + edges.retain(|e| self.update_edge(now, e.clone())); + // Update metrics after edge update + if let Some(prune_edges_after) = self.config.prune_edges_after { + self.prune_old_edges(now - prune_edges_after); + } + let next_hops = Arc::new(self.graph.calculate_distance(unreliable_peers)); + + // Update peer_reachable_at. + let now = clock.now(); + self.peer_reachable_at.insert(self.config.node_id.clone(), now); + for peer in next_hops.keys() { + self.peer_reachable_at.insert(peer.clone(), now); + } + self.prune_unreachable_peers(now - self.config.prune_unreachable_peers_after); + let mut local_edges = HashMap::new(); + for e in self.edges.clone().values() { + if let Some(other) = e.other(&self.config.node_id) { + local_edges.insert(other.clone(), e.clone()); + } + } + metrics::ROUTING_TABLE_RECALCULATIONS.inc(); + metrics::PEER_REACHABLE.set(next_hops.len() as i64); + metrics::EDGE_UPDATES.inc_by(total as u64); + metrics::EDGE_ACTIVE.set(self.graph.total_active_edges() as i64); + metrics::EDGE_TOTAL.set(self.edges.len() as i64); + (edges, GraphSnapshot { edges: self.edges.clone(), local_edges, next_hops }) + } +} + +pub(crate) struct Graph { + inner: Arc>, + snapshot: ArcSwap, + unreliable_peers: ArcSwap>, + // TODO(gprusak): RoutingTableView consists of a bunch of unrelated stateful features. + // It requires a refactor. + pub routing_table: RoutingTableView, + + runtime: Runtime, +} + +impl Graph { + pub fn new(config: GraphConfig, store: store::Store) -> Self { + Self { + routing_table: RoutingTableView::new(store.clone()), + inner: Arc::new(Mutex::new(Inner { + graph: bfs::Graph::new(config.node_id.clone()), + config, + edges: Default::default(), + peer_reachable_at: HashMap::new(), + store, + })), + unreliable_peers: ArcSwap::default(), + snapshot: ArcSwap::default(), + runtime: Runtime::new(), + } + } + + pub fn load(&self) -> Arc { + self.snapshot.load_full() + } + + pub fn set_unreliable_peers(&self, unreliable_peers: HashSet) { + self.unreliable_peers.store(Arc::new(unreliable_peers)); + } + + pub async fn verify(&self, edges: Vec) -> (Vec, bool) { + let old = self.load(); + let mut edges = Edge::deduplicate(edges); + edges.retain(|x| !has(&old.edges, x)); + // Verify the edges in parallel on rayon. + concurrency::rayon::run(move || { + concurrency::rayon::try_map(edges.into_iter().par_bridge(), |e| { + if e.verify() { + Some(e) + } else { + None + } + }) + }) + .await + } + + /// Adds edges to the graph and recomputes the routing table. + /// Returns the edges which were actually new and should be broadcasted. + pub async fn update(self: &Arc, clock: &time::Clock, edges: Vec) -> Vec { + // Computation is CPU heavy and accesses DB so we execute it on a dedicated thread. + // TODO(gprusak): It would be better to move CPU heavy stuff to rayon and make DB calls async, + // but that will require further refactor. Or even better: get rid of the Graph all + // together. + let this = self.clone(); + let clock = clock.clone(); + self.runtime + .handle + .spawn(async move { + let mut inner = this.inner.lock(); + let (new_edges, snapshot) = + inner.update(&clock, edges, &this.unreliable_peers.load()); + let snapshot = Arc::new(snapshot); + this.routing_table.update(snapshot.next_hops.clone()); + this.snapshot.store(snapshot); + new_edges + }) + .await + .unwrap() + } +} diff --git a/chain/network/src/routing/graph/tests.rs b/chain/network/src/routing/graph/tests.rs new file mode 100644 index 00000000000..8073ba57d0c --- /dev/null +++ b/chain/network/src/routing/graph/tests.rs @@ -0,0 +1,262 @@ +use super::{Graph, GraphConfig}; +use crate::network_protocol::testonly as data; +use crate::network_protocol::Edge; +use crate::network_protocol::EDGE_MIN_TIMESTAMP_NONCE; +use crate::store; +use crate::store::testonly::Component; +use crate::testonly::make_rng; +use crate::time; +use near_crypto::Signature; +use near_primitives::network::PeerId; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +impl Graph { + async fn check(&self, want_mem: &[Edge], want_db: &[Component]) { + let got_mem = self.load(); + let got_mem: HashMap<_, _> = got_mem.edges.iter().collect(); + let mut want_mem_map = HashMap::new(); + for e in want_mem { + if want_mem_map.insert(e.key(), e).is_some() { + panic!("want_mem: multiple entries for {:?}", e.key()); + } + } + assert_eq!(got_mem, want_mem_map); + + let got_db: HashSet<_> = + self.inner.lock().store.list_components().into_iter().map(|c| c.normal()).collect(); + let want_db: HashSet<_> = want_db.iter().map(|c| c.clone().normal()).collect(); + assert_eq!(got_db, want_db); + } +} + +fn edge(p0: &PeerId, p1: &PeerId, nonce: u64) -> Edge { + Edge::new(p0.clone(), p1.clone(), nonce, Signature::default(), Signature::default()) +} + +fn store() -> store::Store { + store::Store::from(near_store::db::TestDB::new()) +} + +#[tokio::test] +async fn empty() { + let mut rng = make_rng(87927345); + let rng = &mut rng; + let cfg = GraphConfig { + node_id: data::make_peer_id(rng), + prune_unreachable_peers_after: time::Duration::seconds(3), + prune_edges_after: None, + }; + let g = Graph::new(cfg, store()); + g.check(&[], &[]).await; +} + +const SEC: time::Duration = time::Duration::seconds(1); + +#[tokio::test] +async fn one_edge() { + let clock = time::FakeClock::default(); + let mut rng = make_rng(87927345); + let rng = &mut rng; + let cfg = GraphConfig { + node_id: data::make_peer_id(rng), + prune_unreachable_peers_after: time::Duration::seconds(3), + prune_edges_after: None, + }; + let g = Arc::new(Graph::new(cfg.clone(), store())); + + let p1 = data::make_peer_id(rng); + let e1 = edge(&cfg.node_id, &p1, 1); + let e1v2 = edge(&cfg.node_id, &p1, 2); + + // Add an active edge. + // Update RT with pruning. NOOP, since p1 is reachable. + g.update(&clock.clock(), vec![e1.clone()]).await; + g.check(&[e1.clone()], &[]).await; + + // Override with an inactive edge. + g.update(&clock.clock(), vec![e1v2.clone()]).await; + g.check(&[e1v2.clone()], &[]).await; + + // After 2s, update RT with pruning unreachable for 3s. + // NOOP, since p1 is unreachable for 2s. + clock.advance(2 * SEC); + g.update(&clock.clock(), vec![]).await; + g.check(&[e1v2.clone()], &[]).await; + + // Update RT with pruning unreachable for 1s. p1 should be moved to DB. + clock.advance(2 * SEC); + g.update(&clock.clock(), vec![]).await; + g.check(&[], &[Component { edges: vec![e1v2.clone()], peers: vec![p1.clone()] }]).await; +} + +#[tokio::test] +async fn load_component() { + let clock = time::FakeClock::default(); + let mut rng = make_rng(87927345); + let rng = &mut rng; + let cfg = GraphConfig { + node_id: data::make_peer_id(rng), + prune_unreachable_peers_after: time::Duration::seconds(3), + prune_edges_after: None, + }; + let g = Arc::new(Graph::new(cfg.clone(), store())); + + let p1 = data::make_peer_id(rng); + let p2 = data::make_peer_id(rng); + let e1 = edge(&cfg.node_id, &p1, 2); + let e2 = edge(&cfg.node_id, &p2, 2); + let e3 = edge(&p1, &p2, 1); + let e1v2 = edge(&cfg.node_id, &p1, 3); + + // There is an active edge between p1,p2, but neither is reachable from me(). + // They should be pruned. + g.update(&clock.clock(), vec![e1.clone(), e2.clone(), e3.clone()]).await; + g.check( + &[], + &[Component { + edges: vec![e1.clone(), e2.clone(), e3.clone()], + peers: vec![p1.clone(), p2.clone()], + }], + ) + .await; + + // Add an active edge from me() to p1. This should trigger loading the whole component from DB. + g.update(&clock.clock(), vec![e1v2.clone()]).await; + g.check(&[e1v2, e2, e3], &[]).await; +} + +#[tokio::test] +async fn components_nonces_are_tracked_in_storage() { + let clock = time::FakeClock::default(); + let mut rng = make_rng(87927345); + let rng = &mut rng; + let cfg = GraphConfig { + node_id: data::make_peer_id(rng), + prune_unreachable_peers_after: time::Duration::seconds(3), + prune_edges_after: None, + }; + let store = store(); + let g = Arc::new(Graph::new(cfg.clone(), store.clone())); + + // Add an inactive edge and prune it. + let p1 = data::make_peer_id(rng); + let e1 = edge(&cfg.node_id, &p1, 2); + g.update(&clock.clock(), vec![e1.clone()]).await; + g.check(&[], &[Component { edges: vec![e1.clone()], peers: vec![p1.clone()] }]).await; + + // Add an active unreachable edge, which also should get pruned. + let p2 = data::make_peer_id(rng); + let p3 = data::make_peer_id(rng); + let e23 = edge(&p2, &p3, 2); + g.update(&clock.clock(), vec![e23.clone()]).await; + g.check( + &[], + &[ + Component { edges: vec![e1.clone()], peers: vec![p1.clone()] }, + Component { edges: vec![e23.clone()], peers: vec![p2.clone(), p3.clone()] }, + ], + ) + .await; + + // Spawn a new graph with the same storage. + // Add another inactive edge and prune it. The previously created component shouldn't get + // overwritten, but rather a new one should be created. + // This verifies that the last_component_nonce (which indicates which component IDs have been + // already utilized) is persistently stored in DB. + let g = Arc::new(Graph::new(cfg.clone(), store)); + let p4 = data::make_peer_id(rng); + let e4 = edge(&cfg.node_id, &p4, 2); + g.update(&clock.clock(), vec![e4.clone()]).await; + g.check( + &[], + &[ + Component { edges: vec![e1.clone()], peers: vec![p1.clone()] }, + Component { edges: vec![e23.clone()], peers: vec![p2.clone(), p3.clone()] }, + Component { edges: vec![e4.clone()], peers: vec![p4.clone()] }, + ], + ) + .await; + + // Add an active edge between unreachable nodes, which will merge 2 components in DB. + let e34 = edge(&p3, &p4, 1); + g.update(&clock.clock(), vec![e34.clone()]).await; + g.check( + &[], + &[ + Component { edges: vec![e1.clone()], peers: vec![p1.clone()] }, + Component { + edges: vec![e4.clone(), e23.clone(), e34.clone()], + peers: vec![p2.clone(), p3.clone(), p4.clone()], + }, + ], + ) + .await; +} + +fn to_active_nonce(t: time::Utc) -> u64 { + let value = t.unix_timestamp() as u64; + if value % 2 == 1 { + return value; + } + return value + 1; +} + +#[tokio::test] +async fn expired_edges() { + let clock = time::FakeClock::default(); + clock.set_utc(*EDGE_MIN_TIMESTAMP_NONCE + time::Duration::days(2)); + let mut rng = make_rng(87927345); + let rng = &mut rng; + let cfg = GraphConfig { + node_id: data::make_peer_id(rng), + prune_unreachable_peers_after: time::Duration::hours(100), + prune_edges_after: Some(110 * SEC), + }; + let g = Arc::new(Graph::new(cfg.clone(), store())); + + let p1 = data::make_peer_id(rng); + let p2 = data::make_peer_id(rng); + + let now = clock.now_utc(); + let e1 = edge(&cfg.node_id, &p1, to_active_nonce(now)); + let old_e2 = edge(&cfg.node_id, &p2, to_active_nonce(now - 100 * SEC)); + let still_old_e2 = edge(&cfg.node_id, &p2, to_active_nonce(now - 90 * SEC)); + let fresh_e2 = edge(&cfg.node_id, &p2, to_active_nonce(now)); + + // Add an active edge. + g.update(&clock.clock(), vec![e1.clone(), old_e2.clone()]).await; + g.check(&[e1.clone(), old_e2.clone()], &[]).await; + // Update RT with pruning. e1 should stay - as it is fresh, but old_e2 should be removed. + clock.advance(40 * SEC); + g.update(&clock.clock(), vec![]).await; + g.check(&[e1.clone()], &[]).await; + + // Adding 'still old' edge to e2 should fail (as it is older than the last prune_edges_older_than) + g.update(&clock.clock(), vec![still_old_e2.clone()]).await; + g.check(&[e1.clone()], &[]).await; + + // But adding the fresh edge should work. + g.update(&clock.clock(), vec![fresh_e2.clone()]).await; + g.check(&[e1.clone(), fresh_e2.clone()], &[]).await; + + // Advance so that the edge is 'too old' and should be removed. + clock.advance(100 * SEC); + g.update(&clock.clock(), vec![]).await; + g.check(&[], &[]).await; + + // Let's create a removal edge. + let e1v2 = edge(&cfg.node_id, &p1, to_active_nonce(clock.now_utc()) + 1); + g.update(&clock.clock(), vec![e1v2.clone()]).await; + g.check(&[e1v2.clone()], &[]).await; + + // Advance time a bit. The edge should stay. + clock.advance(20 * SEC); + g.update(&clock.clock(), vec![]).await; + g.check(&[e1v2.clone()], &[]).await; + + // Advance time a lot. The edge should be pruned. + clock.advance(100 * SEC); + g.update(&clock.clock(), vec![]).await; + g.check(&[], &[]).await; +} diff --git a/chain/network/src/routing/graph_with_cache.rs b/chain/network/src/routing/graph_with_cache.rs deleted file mode 100644 index fca34cbc1ed..00000000000 --- a/chain/network/src/routing/graph_with_cache.rs +++ /dev/null @@ -1,157 +0,0 @@ -use crate::network_protocol::{Edge, EdgeState}; -use crate::routing; -use crate::stats::metrics; -use crate::time; -use near_primitives::network::PeerId; -use once_cell::sync::OnceCell; -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; -use tracing::trace; - -// TODO: make it opaque, so that the key.0 < key.1 invariant is protected. -type EdgeKey = (PeerId, PeerId); -pub type NextHopTable = HashMap>; - -pub struct GraphWithCache { - /// Current view of the network represented by an undirected graph. - /// Contains only validated edges. - /// Nodes are Peers and edges are active connections. - graph: routing::Graph, - /// Edges of the raw_graph, indexed by Edge::key(). - /// Contains also the edge tombstones. - edges: HashMap, - /// Peers of this node, which are on any shortest path to the given node. - /// Derived from graph. - cached_next_hops: OnceCell>, - // Don't allow edges that are before this time (if set) - prune_edges_before: Option, -} - -impl GraphWithCache { - pub fn new(my_peer_id: PeerId) -> Self { - Self { - graph: routing::Graph::new(my_peer_id), - edges: Default::default(), - cached_next_hops: OnceCell::new(), - prune_edges_before: None, - } - } - - pub fn my_peer_id(&self) -> PeerId { - self.graph.my_peer_id().clone() - } - pub fn total_active_edges(&self) -> u64 { - self.graph.total_active_edges() - } - pub fn edges(&self) -> &HashMap { - &self.edges - } - - pub fn has(&self, edge: &Edge) -> bool { - let prev = self.edges.get(&edge.key()); - prev.map_or(false, |x| x.nonce() >= edge.nonce()) - } - - pub fn set_unreliable_peers(&mut self, unreliable_peers: HashSet) { - self.graph.set_unreliable_peers(unreliable_peers); - // Invalidate cache. - self.cached_next_hops = OnceCell::new(); - } - - /// Adds an edge without validating the signatures. O(1). - /// Returns true, iff was newer than an already known version of this edge. - pub fn update_edge(&mut self, edge: Edge) -> bool { - if self.has(&edge) { - return false; - } - if let Some(edge_limit) = self.prune_edges_before { - // Don't add edges that are older than the limit. - if edge.is_edge_older_than(edge_limit) { - return false; - } - } - - let key = edge.key(); - // Add the edge. - match edge.edge_type() { - EdgeState::Active => self.graph.add_edge(&key.0, &key.1), - EdgeState::Removed => self.graph.remove_edge(&key.0, &key.1), - } - self.edges.insert(key.clone(), edge); - // Invalidate cache. - self.cached_next_hops = OnceCell::new(); - true - } - - pub fn update_edges(&mut self, mut edges: Vec) -> Vec { - edges.retain(|e| self.update_edge(e.clone())); - edges - } - - /// Removes an edge by key. O(1). - pub fn remove_edge(&mut self, key: &EdgeKey) { - self.remove_edges(&[key]) - } - - // Removes mutiple edges. - // The main benefit is that we take the cached_next_hops lock only once. - fn remove_edges(&mut self, edge_keys: &[&EdgeKey]) { - let mut removed = false; - for key in edge_keys { - if self.edges.remove(key).is_some() { - self.graph.remove_edge(&key.0, &key.1); - removed = true; - } - } - if removed { - self.cached_next_hops = OnceCell::new(); - } - } - - /// Computes the next hops table, based on the graph. O(|Graph|). - pub fn next_hops(&self) -> Arc { - self.cached_next_hops - .get_or_init(|| { - let _d = delay_detector::DelayDetector::new(|| "routing table update".into()); - let _next_hops_recalculation = - metrics::ROUTING_TABLE_RECALCULATION_HISTOGRAM.start_timer(); - trace!(target: "network", "Update routing table."); - let rt = Arc::new(self.graph.calculate_distance()); - metrics::ROUTING_TABLE_RECALCULATIONS.inc(); - metrics::PEER_REACHABLE.set(rt.len() as i64); - rt - }) - .clone() - } - - pub fn remove_adjacent_edges(&mut self, peers: &HashSet) -> Vec { - let edges: Vec<_> = self - .edges() - .values() - .filter(|edge| { - let key = edge.key(); - peers.contains(&key.0) || peers.contains(&key.1) - }) - .cloned() - .collect(); - self.remove_edges(&edges.iter().map(|edge| edge.key()).collect::>()); - edges - } - - pub fn prune_old_edges(&mut self, prune_edges_older_than: time::Utc) { - self.prune_edges_before = Some(prune_edges_older_than); - let old_edges = self - .edges() - .iter() - .filter_map(|(edge_key, edge)| { - if edge.is_edge_older_than(prune_edges_older_than) { - Some(edge_key) - } else { - None - } - }) - .cloned() - .collect::>(); - self.remove_edges(&old_edges.iter().collect::>()); - } -} diff --git a/chain/network/src/routing/mod.rs b/chain/network/src/routing/mod.rs index 2cdcda05e79..7112ce2b92d 100644 --- a/chain/network/src/routing/mod.rs +++ b/chain/network/src/routing/mod.rs @@ -1,15 +1,7 @@ -pub(crate) mod route_back_cache; -pub mod routing_table_view; - -pub mod actor; +mod bfs; pub(crate) mod edge; mod graph; -mod graph_with_cache; -pub(crate) use actor::Actor; -pub(crate) use graph_with_cache::NextHopTable; -// for benchmark only -pub use graph::Graph; -pub use graph_with_cache::GraphWithCache; +pub(crate) mod route_back_cache; +pub mod routing_table_view; -#[cfg(test)] -mod tests; +pub(crate) use graph::{Graph, GraphConfig, NextHopTable}; diff --git a/chain/network/src/routing/routing_table_view.rs b/chain/network/src/routing/routing_table_view/mod.rs similarity index 82% rename from chain/network/src/routing/routing_table_view.rs rename to chain/network/src/routing/routing_table_view/mod.rs index e74875e5195..0e1b4c43598 100644 --- a/chain/network/src/routing/routing_table_view.rs +++ b/chain/network/src/routing/routing_table_view/mod.rs @@ -1,4 +1,4 @@ -use crate::network_protocol::{Edge, PeerIdOrHash}; +use crate::network_protocol::PeerIdOrHash; use crate::routing; use crate::routing::route_back_cache::RouteBackCache; use crate::store; @@ -11,16 +11,15 @@ use parking_lot::Mutex; use std::collections::HashMap; use std::sync::Arc; +#[cfg(test)] +mod tests; + const ANNOUNCE_ACCOUNT_CACHE_SIZE: usize = 10_000; const LAST_ROUTED_CACHE_SIZE: usize = 10_000; pub(crate) struct RoutingTableView(Mutex); struct Inner { - my_peer_id: PeerId, - /// Store last update for known edges. This is limited to list of adjacent edges to `my_peer_id`. - local_edges: HashMap, - /// Maps an account_id to a peer owning it. account_peers: LruCache, /// Subset of account_peers, which we have broadcasted to the peers. @@ -89,13 +88,11 @@ pub(crate) enum FindRouteError { } impl RoutingTableView { - pub fn new(store: store::Store, my_peer_id: PeerId) -> Self { + pub fn new(store: store::Store) -> Self { Self(Mutex::new(Inner { - my_peer_id, account_peers: LruCache::new(ANNOUNCE_ACCOUNT_CACHE_SIZE), account_peers_broadcasted: LruCache::new(ANNOUNCE_ACCOUNT_CACHE_SIZE), next_hops: Default::default(), - local_edges: Default::default(), route_back: RouteBackCache::default(), store, find_route_calls: 0, @@ -103,14 +100,8 @@ impl RoutingTableView { })) } - pub(crate) fn update(&self, pruned_edges: &[Edge], next_hops: Arc) { - let mut inner = self.0.lock(); - for e in pruned_edges { - if let Some(peer_id) = e.other(&inner.my_peer_id) { - inner.local_edges.remove(peer_id); - } - } - inner.next_hops = next_hops; + pub(crate) fn update(&self, next_hops: Arc) { + self.0.lock().next_hops = next_hops; } pub(crate) fn reachable_peers(&self) -> usize { @@ -205,33 +196,6 @@ impl RoutingTableView { }) .collect() } - - pub(crate) fn get_local_edge(&self, other_peer: &PeerId) -> Option { - self.0.lock().local_edges.get(other_peer).cloned() - } - - pub(crate) fn get_local_edges(&self) -> Vec { - self.0.lock().local_edges.values().cloned().collect() - } - - /// Returns the diff: new local edges added. - pub(crate) fn add_local_edges(&self, edges: &[Edge]) -> Vec { - let mut inner = self.0.lock(); - let mut res = vec![]; - for edge in edges { - let other = match edge.other(&inner.my_peer_id) { - Some(other) => other, - None => continue, - }; - let old_nonce = inner.local_edges.get(other).map_or(0, |e| e.nonce()); - if old_nonce >= edge.nonce() { - continue; - } - inner.local_edges.insert(other.clone(), edge.clone()); - res.push(edge.clone()); - } - res - } } #[derive(Debug)] diff --git a/chain/network/src/routing/tests/cache.rs b/chain/network/src/routing/routing_table_view/tests.rs similarity index 71% rename from chain/network/src/routing/tests/cache.rs rename to chain/network/src/routing/routing_table_view/tests.rs index fe69dcd4af7..f2db55bd5c8 100644 --- a/chain/network/src/routing/tests/cache.rs +++ b/chain/network/src/routing/routing_table_view/tests.rs @@ -1,7 +1,39 @@ -use crate::routing::routing_table_view::RoutingTableView; +use crate::network_protocol::testonly as data; +use crate::routing; +use crate::routing::routing_table_view::*; use crate::test_utils::{random_epoch_id, random_peer_id}; +use crate::testonly::make_rng; +use crate::time; +use crate::types::PeerIdOrHash; use near_crypto::Signature; use near_primitives::network::AnnounceAccount; +use rand::seq::SliceRandom; +use std::sync::Arc; + +#[test] +fn find_route() { + let mut rng = make_rng(385305732); + let clock = time::FakeClock::default(); + let rng = &mut rng; + let store = crate::store::Store::from(near_store::db::TestDB::new()); + + // Create a sample NextHopTable. + let peers: Vec<_> = (0..10).map(|_| data::make_peer_id(rng)).collect(); + let mut next_hops = routing::NextHopTable::new(); + for p in &peers { + next_hops.insert(p.clone(), (0..3).map(|_| peers.choose(rng).cloned().unwrap()).collect()); + } + let next_hops = Arc::new(next_hops); + + // Check that RoutingTableView always selects a valid next hop. + let rtv = RoutingTableView::new(store); + rtv.update(next_hops.clone()); + for _ in 0..1000 { + let p = peers.choose(rng).unwrap(); + let got = rtv.find_route(&clock.clock(), &PeerIdOrHash::PeerId(p.clone())).unwrap(); + assert!(next_hops.get(p).unwrap().contains(&got)); + } +} #[test] fn announcement_same_epoch() { @@ -11,7 +43,7 @@ fn announcement_same_epoch() { let peer_id1 = random_peer_id(); let epoch_id0 = random_epoch_id(); - let routing_table = RoutingTableView::new(store, random_peer_id()); + let routing_table = RoutingTableView::new(store); let announce0 = AnnounceAccount { account_id: "near0".parse().unwrap(), @@ -52,7 +84,7 @@ fn dont_load_on_build() { let epoch_id0 = random_epoch_id(); let epoch_id1 = random_epoch_id(); - let routing_table = RoutingTableView::new(store.clone(), random_peer_id()); + let routing_table = RoutingTableView::new(store.clone()); let announce0 = AnnounceAccount { account_id: "near0".parse().unwrap(), @@ -75,7 +107,7 @@ fn dont_load_on_build() { assert!(vec![announce0, announce1].iter().all(|announce| { accounts.contains(&announce) })); assert_eq!(accounts.len(), 2); - let routing_table1 = RoutingTableView::new(store, random_peer_id()); + let routing_table1 = RoutingTableView::new(store); assert_eq!(routing_table1.get_announce_accounts().len(), 0); } @@ -86,8 +118,8 @@ fn load_from_disk() { let peer_id0 = random_peer_id(); let epoch_id0 = random_epoch_id(); - let routing_table = RoutingTableView::new(store.clone(), random_peer_id()); - let routing_table1 = RoutingTableView::new(store, random_peer_id()); + let routing_table = RoutingTableView::new(store.clone()); + let routing_table1 = RoutingTableView::new(store); let announce0 = AnnounceAccount { account_id: "near0".parse().unwrap(), diff --git a/chain/network/src/routing/tests/cache_edges.rs b/chain/network/src/routing/tests/cache_edges.rs deleted file mode 100644 index 93d543ab5d5..00000000000 --- a/chain/network/src/routing/tests/cache_edges.rs +++ /dev/null @@ -1,287 +0,0 @@ -use crate::network_protocol::testonly as data; -use crate::network_protocol::Edge; -use crate::network_protocol::EDGE_MIN_TIMESTAMP_NONCE; -use crate::routing; -use crate::store; -use crate::store::testonly::Component; -use crate::testonly::make_rng; -use crate::time; -use near_crypto::Signature; -use near_primitives::network::PeerId; -use parking_lot::RwLock; -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; - -fn edge(p0: &PeerId, p1: &PeerId, nonce: u64) -> Edge { - Edge::new(p0.clone(), p1.clone(), nonce, Signature::default(), Signature::default()) -} - -struct RoutingTableTest { - clock: time::FakeClock, - rng: crate::testonly::Rng, - db: Arc, - graph: Arc>, - // This is the system runner attached to the given test's system thread. - // Allows to create actors within the test. - _system: actix::SystemRunner, -} - -impl Drop for RoutingTableTest { - fn drop(&mut self) { - actix::System::current().stop(); - } -} - -impl RoutingTableTest { - // Gets a new random PeerId. - pub fn make_peer(&mut self) -> PeerId { - data::make_peer_id(&mut self.rng) - } - - fn me(&self) -> PeerId { - self.graph.read().my_peer_id() - } - - fn new() -> Self { - let mut rng = make_rng(87927345); - let clock = time::FakeClock::default(); - let me = data::make_peer_id(&mut rng); - let db = near_store::db::TestDB::new(); - - let graph = Arc::new(RwLock::new(routing::GraphWithCache::new(me.clone()))); - Self { rng, clock, graph, db, _system: actix::System::new() } - } - - fn new_actor(&self) -> routing::actor::Actor { - routing::actor::Actor::new( - self.clock.clock(), - store::Store::from(self.db.clone()), - self.graph.clone(), - ) - } - - fn check(&mut self, want_mem: &[Edge], want_db: &[Component]) { - let store = store::Store::from(self.db.clone()); - let got_mem = self.graph.read().edges().clone(); - let got_mem: HashMap<_, _> = got_mem.iter().collect(); - let mut want_mem_map = HashMap::new(); - for e in want_mem { - if want_mem_map.insert(e.key(), e).is_some() { - panic!("want_mem: multiple entries for {:?}", e.key()); - } - } - assert_eq!(got_mem, want_mem_map); - - let got_db: HashSet<_> = store.list_components().into_iter().map(|c| c.normal()).collect(); - let want_db: HashSet<_> = want_db.iter().map(|c| c.clone().normal()).collect(); - assert_eq!(got_db, want_db); - } -} - -#[test] -fn empty() { - let mut test = RoutingTableTest::new(); - test.check(&[], &[]); -} - -const SEC: time::Duration = time::Duration::seconds(1); - -#[test] -fn one_edge() { - let mut test = RoutingTableTest::new(); - let mut actor = test.new_actor(); - let p1 = test.make_peer(); - let e1 = edge(&test.me(), &p1, 1); - let e1v2 = edge(&test.me(), &p1, 2); - - // Add an active edge. - actor.add_verified_edges(vec![e1.clone()]); - test.check(&[e1.clone()], &[]); - - // Update RT with pruning. NOOP, since p1 is reachable. - actor.update_routing_table(Some(test.clock.now()), None); - test.check(&[e1.clone()], &[]); - - // Override with an inactive edge. - actor.add_verified_edges(vec![e1v2.clone()]); - test.check(&[e1v2.clone()], &[]); - - // After 2s, update RT without pruning. - test.clock.advance(2 * SEC); - actor.update_routing_table(None, None); - test.check(&[e1v2.clone()], &[]); - - // Update RT with pruning unreachable for 3s. NOOP, since p1 is unreachable for 2s. - actor.update_routing_table(Some(test.clock.now() - 3 * SEC), None); - test.check(&[e1v2.clone()], &[]); - - // Update RT with pruning unreachable for 1s. p1 should be moved to DB. - actor.update_routing_table(Some(test.clock.now() - SEC), None); - test.check(&[], &[Component { edges: vec![e1v2.clone()], peers: vec![p1.clone()] }]); -} - -#[test] -fn load_component() { - let mut test = RoutingTableTest::new(); - let mut actor = test.new_actor(); - let p1 = test.make_peer(); - let p2 = test.make_peer(); - let e1 = edge(&test.me(), &p1, 2); - let e2 = edge(&test.me(), &p2, 2); - let e3 = edge(&p1, &p2, 1); - let e1v2 = edge(&test.me(), &p1, 3); - - // There is an active edge between p1,p2, but neither is reachable from me(). - // They should be pruned. - actor.add_verified_edges(vec![e1.clone(), e2.clone(), e3.clone()]); - actor.update_routing_table(Some(test.clock.now()), None); - test.check( - &[], - &[Component { - edges: vec![e1.clone(), e2.clone(), e3.clone()], - peers: vec![p1.clone(), p2.clone()], - }], - ); - - // Add an active edge from me() to p1. This should trigger loading the whole component from DB. - actor.add_verified_edges(vec![e1v2.clone()]); - actor.update_routing_table(Some(test.clock.now()), None); - test.check(&[e1v2, e2, e3], &[]); -} - -#[test] -fn components_nonces_are_tracked_in_storage() { - let mut test = RoutingTableTest::new(); - - // Start the actor, add an inactive edge and prune it. - let mut actor = test.new_actor(); - let p1 = test.make_peer(); - let e1 = edge(&test.me(), &p1, 2); - actor.add_verified_edges(vec![e1.clone()]); - actor.update_routing_table(Some(test.clock.now()), None); - test.check(&[], &[Component { edges: vec![e1.clone()], peers: vec![p1.clone()] }]); - - // Add an active unreachable edge, which also should get pruned. - let p2 = test.make_peer(); - let p3 = test.make_peer(); - let e23 = edge(&p2, &p3, 2); - actor.add_verified_edges(vec![e23.clone()]); - actor.update_routing_table(Some(test.clock.now()), None); - test.check( - &[], - &[ - Component { edges: vec![e1.clone()], peers: vec![p1.clone()] }, - Component { edges: vec![e23.clone()], peers: vec![p2.clone(), p3.clone()] }, - ], - ); - - // Restart the actor. - // Add another inactive edge and prune it. The previously created component shouldn't get - // overwritten, but rather a new one should be created. - // This verifies that the last_component_nonce (which indicates which component IDs have been - // already utilized) is persistently stored in DB. - let mut actor = test.new_actor(); - let p4 = test.make_peer(); - let e4 = edge(&test.me(), &p4, 2); - actor.add_verified_edges(vec![e4.clone()]); - actor.update_routing_table(Some(test.clock.now()), None); - test.check( - &[], - &[ - Component { edges: vec![e1.clone()], peers: vec![p1.clone()] }, - Component { edges: vec![e23.clone()], peers: vec![p2.clone(), p3.clone()] }, - Component { edges: vec![e4.clone()], peers: vec![p4.clone()] }, - ], - ); - - // Add an active edge between unreachable nodes, which will merge 2 components in DB. - let e34 = edge(&p3, &p4, 1); - actor.add_verified_edges(vec![e34.clone()]); - actor.update_routing_table(Some(test.clock.now()), None); - test.check( - &[], - &[ - Component { edges: vec![e1.clone()], peers: vec![p1.clone()] }, - Component { - edges: vec![e4.clone(), e23.clone(), e34.clone()], - peers: vec![p2.clone(), p3.clone(), p4.clone()], - }, - ], - ); -} - -fn to_active_nonce(value: u64) -> u64 { - if value % 2 == 1 { - return value; - } - return value + 1; -} - -#[test] -fn expired_edges() { - let mut test = RoutingTableTest::new(); - test.clock.set_utc(*EDGE_MIN_TIMESTAMP_NONCE + time::Duration::days(2)); - let mut actor = test.new_actor(); - let p1 = test.make_peer(); - let p2 = test.make_peer(); - let current_odd_nonce = to_active_nonce(test.clock.now_utc().unix_timestamp() as u64); - - let e1 = edge(&test.me(), &p1, current_odd_nonce); - - let old_e2 = edge(&test.me(), &p2, current_odd_nonce - 100); - let still_old_e2 = edge(&test.me(), &p2, current_odd_nonce - 90); - let fresh_e2 = edge(&test.me(), &p2, current_odd_nonce); - - // Add an active edge. - actor.add_verified_edges(vec![e1.clone(), old_e2.clone()]); - test.check(&[e1.clone(), old_e2.clone()], &[]); - - // Update RT with pruning. e1 should stay - as it is fresh, but old_e2 should be removed. - actor.update_routing_table( - Some(test.clock.now()), - Some(test.clock.now_utc().checked_sub(time::Duration::seconds(10)).unwrap()), - ); - test.check(&[e1.clone()], &[]); - - // Adding 'still old' edge to e2 should fail (as it is older than the last prune_edges_older_than) - actor.add_verified_edges(vec![still_old_e2.clone()]); - test.check(&[e1.clone()], &[]); - - // But adding the fresh edge should work. - actor.add_verified_edges(vec![fresh_e2.clone()]); - test.check(&[e1.clone(), fresh_e2.clone()], &[]); - - // Advance 20 seconds: - test.clock.advance(20 * SEC); - - // Now the edge is 'too old' and should be removed. - actor.update_routing_table( - Some(test.clock.now()), - Some(test.clock.now_utc().checked_sub(time::Duration::seconds(10)).unwrap()), - ); - test.check(&[], &[]); - - // let's create a removal edge - let e1v2 = - edge(&test.me(), &p1, to_active_nonce(test.clock.now_utc().unix_timestamp() as u64) + 1); - actor.add_verified_edges(vec![e1v2.clone()]); - test.check(&[e1v2.clone()], &[]); - - actor.update_routing_table( - None, - Some(test.clock.now_utc().checked_sub(time::Duration::seconds(10)).unwrap()), - ); - test.check(&[e1v2.clone()], &[]); - - // And now it should disappear - - // Advance 20 seconds: - test.clock.advance(20 * SEC); - - // Now the edge is 'too old' and should be removed. - actor.update_routing_table( - Some(test.clock.now()), - Some(test.clock.now_utc().checked_sub(time::Duration::seconds(10)).unwrap()), - ); - test.check(&[], &[]); -} diff --git a/chain/network/src/routing/tests/mod.rs b/chain/network/src/routing/tests/mod.rs deleted file mode 100644 index bb1a34a94db..00000000000 --- a/chain/network/src/routing/tests/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod cache; -mod cache_edges; -mod routing_table_view; diff --git a/chain/network/src/routing/tests/routing_table_view.rs b/chain/network/src/routing/tests/routing_table_view.rs deleted file mode 100644 index eef92ccd12f..00000000000 --- a/chain/network/src/routing/tests/routing_table_view.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::network_protocol::testonly as data; -use crate::routing; -use crate::routing::routing_table_view::*; -use crate::testonly::make_rng; -use crate::time; -use crate::types::PeerIdOrHash; -use rand::seq::SliceRandom; -use std::sync::Arc; - -#[test] -fn find_route() { - let mut rng = make_rng(385305732); - let clock = time::FakeClock::default(); - let rng = &mut rng; - let store = crate::store::Store::from(near_store::db::TestDB::new()); - - // Create a sample NextHopTable. - let peers: Vec<_> = (0..10).map(|_| data::make_peer_id(rng)).collect(); - let mut next_hops = routing::NextHopTable::new(); - for p in &peers { - next_hops.insert(p.clone(), (0..3).map(|_| peers.choose(rng).cloned().unwrap()).collect()); - } - let next_hops = Arc::new(next_hops); - - // Check that RoutingTableView always selects a valid next hop. - let rtv = RoutingTableView::new(store, data::make_peer_id(rng)); - rtv.update(&[], next_hops.clone()); - for _ in 0..1000 { - let p = peers.choose(rng).unwrap(); - let got = rtv.find_route(&clock.clock(), &PeerIdOrHash::PeerId(p.clone())).unwrap(); - assert!(next_hops.get(p).unwrap().contains(&got)); - } -} diff --git a/chain/network/src/stats/metrics.rs b/chain/network/src/stats/metrics.rs index d6e9c1790f9..e6c1244b3bf 100644 --- a/chain/network/src/stats/metrics.rs +++ b/chain/network/src/stats/metrics.rs @@ -277,15 +277,6 @@ pub(crate) static PEER_MANAGER_MESSAGES_TIME: Lazy = Lazy::new(|| ) .unwrap() }); -pub(crate) static ROUTING_TABLE_MESSAGES_TIME: Lazy = Lazy::new(|| { - try_create_histogram_vec( - "near_routing_actor_messages_time", - "Time that routing table actor spends on handling different types of messages", - &["message"], - Some(exponential_buckets(0.0001, 2., 15).unwrap()), - ) - .unwrap() -}); pub(crate) static ROUTED_MESSAGE_DROPPED: Lazy = Lazy::new(|| { try_create_int_counter_vec( "near_routed_message_dropped", diff --git a/chain/network/src/testonly/stream.rs b/chain/network/src/testonly/stream.rs index d1329cb1266..0ba206eec8a 100644 --- a/chain/network/src/testonly/stream.rs +++ b/chain/network/src/testonly/stream.rs @@ -27,12 +27,12 @@ impl Stream { return None; } - pub async fn read(&mut self) -> PeerMessage { + pub async fn read(&mut self) -> Result { 'read: loop { - let n = self.stream.stream.read_u32_le().await.unwrap() as usize; + let n = self.stream.stream.read_u32_le().await? as usize; let mut buf = BytesMut::new(); buf.resize(n, 0); - self.stream.stream.read_exact(&mut buf[..]).await.unwrap(); + self.stream.stream.read_exact(&mut buf[..]).await?; for enc in [Encoding::Proto, Encoding::Borsh] { if let Ok(msg) = PeerMessage::deserialize(enc, &buf[..]) { // If deserialize() succeeded but we expected different encoding, ignore the @@ -44,7 +44,7 @@ impl Stream { if enc == Encoding::Proto { self.protocol_buffers_supported = true; } - return msg; + return Ok(msg); } } panic!("unknown encoding"); From 21cd263815ba6d56291dc6cd6a1993691d8f6426 Mon Sep 17 00:00:00 2001 From: Marcelo Diop-Gonzalez Date: Wed, 23 Nov 2022 10:28:04 -0500 Subject: [PATCH 027/188] fix(mirror): fix bug in handling of access keys that have been deleted (#8104) right now we're just storing the source and target nonces for added keys, but then we don't update anything if the key is deleted and readded. So to fix this, we'll store only the target chain nonce on disk, along with the hashes of txs and receipts that might update it in the future. A big downside to this is that the logic around handling nonces is much more complicated and probably more bug prone. But doing this will make it easier to get around the problem of access keys added by FunctionCalls by just sending extra transactions. This PR also adds traffic to the corresponding pytest that fails before this change, but passes after (except for a few mishaps where the AddKey tx hasn't made it on chain by the time we want to use it) --- pytest/tools/mirror/test.py | 187 +++++-- tools/mirror/src/chain_tracker.rs | 826 +++++++++++++++++++++------- tools/mirror/src/lib.rs | 871 ++++++++++++++---------------- 3 files changed, 1175 insertions(+), 709 deletions(-) diff --git a/pytest/tools/mirror/test.py b/pytest/tools/mirror/test.py index 30ef1da51a7..64ae1571300 100755 --- a/pytest/tools/mirror/test.py +++ b/pytest/tools/mirror/test.py @@ -146,23 +146,17 @@ def create_forked_chain(config, near_root): json.dump(validators, f) try: + # we want to set transaction-validity-period to a bigger number + # because the mirror code sets the block hash on transactions up to a couple minutes + # before sending them, and that can cause problems for the default localnet + # setting of transaction_validity_period. Not really worth changing the code since + # transaction_validity_period is large on mainnet and testnet anyway subprocess.check_output([ - neard, - 'amend-genesis', - '--genesis-file-in', - genesis_file_in, - '--records-file-in', - records_file_in, - '--genesis-file-out', - genesis_file_out, - '--records-file-out', - records_file_out, - '--validators', - validators_file, - '--chain-id', - 'foonet', - '--epoch-length', - '20', + neard, 'amend-genesis', '--genesis-file-in', genesis_file_in, + '--records-file-in', records_file_in, '--genesis-file-out', + genesis_file_out, '--records-file-out', records_file_out, + '--validators', validators_file, '--chain-id', 'foonet', + '--transaction-validity-period', '10000' ], stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: @@ -224,15 +218,25 @@ def start_mirror(near_root, source_home, target_home, boot_node): # we'll test out adding an access key and then sending txs signed with it # since that hits some codepaths we want to test -def send_add_access_key(node, creator_key, nonce, block_hash): - k = key.Key.from_random('test0') - action = transaction.create_full_access_key_action(k.decoded_pk()) - tx = transaction.sign_and_serialize_transaction('test0', nonce, [action], - block_hash, 'test0', - creator_key.decoded_pk(), - creator_key.decoded_sk()) +def send_add_access_key(node, key, target_key, nonce, block_hash): + action = transaction.create_full_access_key_action(target_key.decoded_pk()) + tx = transaction.sign_and_serialize_transaction(target_key.account_id, + nonce, [action], block_hash, + key.account_id, + key.decoded_pk(), + key.decoded_sk()) + node.send_tx(tx) + + +def send_delete_access_key(node, key, target_key, nonce, block_hash): + action = transaction.create_delete_access_key_action( + target_key.decoded_pk()) + tx = transaction.sign_and_serialize_transaction(target_key.account_id, + nonce, [action], block_hash, + target_key.account_id, + key.decoded_pk(), + key.decoded_sk()) node.send_tx(tx) - return k def create_subaccount(node, signer_key, nonce, block_hash): @@ -255,6 +259,17 @@ def create_subaccount(node, signer_key, nonce, block_hash): return k +def send_transfers(nodes, nonces, block_hash): + for sender in range(len(nonces)): + receiver = (sender + 1) % len(nonces) + receiver_id = nodes[receiver].signer_key.account_id + + tx = transaction.sign_payment_tx(nodes[sender].signer_key, receiver_id, + 300, nonces[sender], block_hash) + nodes[sender].send_tx(tx) + nonces[sender] += 1 + + # a key that we added with an AddKey tx or implicit account transfer. # just for nonce handling convenience class AddedKey: @@ -266,6 +281,10 @@ def __init__(self, key): def send_if_inited(self, node, transfers, block_hash): if self.nonce is None: self.nonce = node.get_nonce_for_pk(self.key.account_id, self.key.pk) + if self.nonce is not None: + logger.info( + f'added key {self.key.account_id} {self.key.pk} inited @ {self.nonce}' + ) if self.nonce is not None: for (receiver_id, amount) in transfers: @@ -274,6 +293,9 @@ def send_if_inited(self, node, transfers, block_hash): self.nonce, block_hash) node.send_tx(tx) + def inited(self): + return self.nonce is not None + class ImplicitAccount: @@ -293,6 +315,9 @@ def transfer(self, node, sender_key, amount, block_hash, nonce): def send_if_inited(self, node, transfers, block_hash): self.key.send_if_inited(node, transfers, block_hash) + def inited(self): + return self.key.inited() + def count_total_txs(node, min_height=0): total = 0 @@ -351,14 +376,18 @@ def main(): config_changes[i] = {"tracked_shards": [0, 1, 2, 3], "archive": True} config = load_config() - near_root, node_dirs = init_cluster(num_nodes=NUM_VALIDATORS, - num_observers=1, - num_shards=4, - config=config, - genesis_config_changes=[ - ["epoch_length", 10], - ], - client_config_changes=config_changes) + near_root, node_dirs = init_cluster( + num_nodes=NUM_VALIDATORS, + num_observers=1, + num_shards=4, + config=config, + # set epoch length to a larger number because otherwise there + # are often problems with validators getting kicked for missing + # only one block or chunk + genesis_config_changes=[ + ["epoch_length", 100], + ], + client_config_changes=config_changes) nodes = [spin_up_node(config, near_root, node_dirs[0], 0)] @@ -368,15 +397,14 @@ def main(): nodes.append( spin_up_node(config, near_root, node_dirs[i], i, boot_node=nodes[0])) - - ctx = utils.TxContext([i for i in range(len(nodes))], nodes) + nonces = [2] * len(nodes) implicit_account1 = ImplicitAccount() for height, block_hash in utils.poll_blocks(nodes[0], timeout=TIMEOUT): implicit_account1.transfer(nodes[0], nodes[0].signer_key, 10**24, base58.b58decode(block_hash.encode('utf8')), - ctx.next_nonce) - ctx.next_nonce += 1 + nonces[0]) + nonces[0] += 1 break for height, block_hash in utils.poll_blocks(nodes[0], timeout=TIMEOUT): @@ -385,7 +413,7 @@ def main(): implicit_account1.send_if_inited(nodes[0], [('test2', height), ('test3', height)], block_hash_bytes) - ctx.send_moar_txs(block_hash, 10, use_routing=False) + send_transfers(nodes, nonces, block_hash_bytes) if height > 12: break @@ -416,27 +444,33 @@ def main(): restarted = False subaccount_key = AddedKey( - create_subaccount(nodes[0], nodes[0].signer_key, ctx.next_nonce, + create_subaccount(nodes[0], nodes[0].signer_key, nonces[0], block_hash_bytes)) - ctx.next_nonce += 1 - - new_key = AddedKey( - send_add_access_key(nodes[0], nodes[0].signer_key, ctx.next_nonce, - block_hash_bytes)) - ctx.next_nonce += 1 + nonces[0] += 1 + k = key.Key.from_random('test0') + new_key = AddedKey(k) + send_add_access_key(nodes[0], nodes[0].signer_key, k, nonces[0], + block_hash_bytes) + nonces[0] += 1 + + test0_deleted_height = None + test0_readded_key = None + implicit_added = None + implicit_deleted = None implicit_account2 = ImplicitAccount() + # here we are gonna send a tiny amount (1 yoctoNEAR) to the implicit account and # then wait a bit before properly initializing it. This hits a corner case where the # mirror binary needs to properly look for the second tx's outcome to find the starting # nonce because the first one failed implicit_account2.transfer(nodes[0], nodes[0].signer_key, 1, - block_hash_bytes, ctx.next_nonce) - ctx.next_nonce += 1 + block_hash_bytes, nonces[0]) + nonces[0] += 1 time.sleep(2) implicit_account2.transfer(nodes[0], nodes[0].signer_key, 10**24, - block_hash_bytes, ctx.next_nonce) - ctx.next_nonce += 1 + block_hash_bytes, nonces[0]) + nonces[0] += 1 for height, block_hash in utils.poll_blocks(nodes[0], timeout=TIMEOUT): code = p.poll() @@ -446,16 +480,20 @@ def main(): block_hash_bytes = base58.b58decode(block_hash.encode('utf8')) - ctx.send_moar_txs(block_hash, 10, use_routing=False) + if test0_deleted_height is None: + send_transfers(nodes, nonces, block_hash_bytes) + else: + send_transfers(nodes[1:], nonces[1:], block_hash_bytes) implicit_account1.send_if_inited( nodes[0], [('test2', height), ('test1', height), (implicit_account2.account_id(), height)], block_hash_bytes) - implicit_account2.send_if_inited( - nodes[1], [('test2', height), ('test0', height), - (implicit_account1.account_id(), height)], - block_hash_bytes) + if not implicit_deleted: + implicit_account2.send_if_inited( + nodes[1], [('test2', height), ('test0', height), + (implicit_account1.account_id(), height)], + block_hash_bytes) new_key.send_if_inited(nodes[2], [('test1', height), ('test2', height), (implicit_account1.account_id(), height), @@ -466,6 +504,49 @@ def main(): (implicit_account2.account_id(), height)], block_hash_bytes) + if implicit_added is None: + # wait for 15 blocks after we started to get some "normal" traffic + # from this implicit account that's closer to what we usually see from + # these (most people aren't adding access keys to implicit accounts much). + # then after that we add an access key and delete the original one to test + # some more code paths + if implicit_account2.inited( + ) and height - start_source_height >= 15: + k = key.Key.from_random(implicit_account2.account_id()) + implicit_added = AddedKey(k) + send_add_access_key(nodes[0], implicit_account2.key.key, k, + implicit_account2.key.nonce, + block_hash_bytes) + implicit_account2.key.nonce += 1 + else: + implicit_added.send_if_inited(nodes[1], [('test0', height)], + block_hash_bytes) + if implicit_added.inited() and not implicit_deleted: + send_delete_access_key(nodes[0], implicit_added.key, + implicit_account2.key.key, + implicit_added.nonce, block_hash_bytes) + implicit_added.nonce += 1 + implicit_deleted = True + + if test0_deleted_height is None and new_key.inited( + ) and height - start_source_height >= 15: + send_delete_access_key(nodes[0], new_key.key, nodes[0].signer_key, + new_key.nonce + 1, block_hash_bytes) + new_key.nonce += 1 + test0_deleted_height = height + + if test0_readded_key is None and test0_deleted_height is not None and height - test0_deleted_height >= 5: + send_add_access_key(nodes[0], new_key.key, nodes[0].signer_key, + new_key.nonce + 1, block_hash_bytes) + test0_readded_key = AddedKey(nodes[0].signer_key) + new_key.nonce += 1 + + if test0_readded_key is not None: + test0_readded_key.send_if_inited( + nodes[1], [('test3', height), + (implicit_account2.account_id(), height)], + block_hash_bytes) + if not restarted and height - start_source_height >= 50: logger.info('stopping mirror process') p.terminate() diff --git a/tools/mirror/src/chain_tracker.rs b/tools/mirror/src/chain_tracker.rs index 1fb399d6046..e93b36d153d 100644 --- a/tools/mirror/src/chain_tracker.rs +++ b/tools/mirror/src/chain_tracker.rs @@ -1,14 +1,21 @@ -use crate::{MappedBlock, MappedTx}; +use crate::{ + ChainObjectId, LatestTargetNonce, MappedBlock, MappedTx, NonceUpdater, ReceiptInfo, + TargetChainTx, TargetNonce, TxInfo, TxOutcome, TxRef, +}; +use actix::Addr; +use near_client::ViewClientActor; use near_crypto::PublicKey; use near_indexer::StreamerMessage; use near_indexer_primitives::IndexerTransactionWithOutcome; use near_primitives::hash::CryptoHash; +use near_primitives::transaction::Transaction; use near_primitives::types::{AccountId, BlockHeight}; use near_primitives_core::types::{Gas, Nonce, ShardId}; +use rocksdb::DB; use std::cmp::Ordering; use std::collections::hash_map; use std::collections::HashMap; -use std::collections::{BTreeSet, VecDeque}; +use std::collections::{BTreeSet, HashSet, VecDeque}; use std::fmt::Write; use std::pin::Pin; use std::time::{Duration, Instant}; @@ -88,72 +95,6 @@ impl Ord for TxId { } } -// we want a reference to transactions in .queued_blocks that need to have nonces -// set later. To avoid having the struct be self referential we keep this struct -// with enough info to look it up later. -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct TxRef { - height: BlockHeight, - shard_id: ShardId, - tx_idx: usize, -} - -struct TxAwaitingNonceCursor<'a> { - txs: &'a [TxRef], - idx: usize, -} - -impl<'a> TxAwaitingNonceCursor<'a> { - fn new(txs: &'a [TxRef]) -> Self { - Self { txs, idx: 0 } - } -} - -pub(crate) struct TxAwaitingNonceIter<'a> { - queued_blocks: &'a VecDeque, - iter: hash_map::Iter<'a, BlockHeight, Vec>, - cursor: Option>, -} - -impl<'a> TxAwaitingNonceIter<'a> { - fn new( - queued_blocks: &'a VecDeque, - txs_awaiting_nonce: &'a HashMap>, - ) -> Self { - let mut iter = txs_awaiting_nonce.iter(); - let cursor = iter.next().map(|(_height, txs)| TxAwaitingNonceCursor::new(txs)); - Self { queued_blocks, iter, cursor } - } -} - -impl<'a> Iterator for TxAwaitingNonceIter<'a> { - type Item = (&'a TxRef, &'a crate::TxAwaitingNonce); - - fn next(&mut self) -> Option { - match &mut self.cursor { - Some(c) => { - let tx_ref = &c.txs[c.idx]; - c.idx += 1; - if c.idx == c.txs.len() { - self.cursor = - self.iter.next().map(|(_height, txs)| TxAwaitingNonceCursor::new(txs)); - } - let block_idx = self - .queued_blocks - .binary_search_by(|b| b.source_height.cmp(&tx_ref.height)) - .unwrap(); - let block = &self.queued_blocks[block_idx]; - let chunk = block.chunks.iter().find(|c| c.shard_id == tx_ref.shard_id).unwrap(); - match &chunk.txs[tx_ref.tx_idx] { - crate::TargetChainTx::AwaitingNonce(tx) => Some((tx_ref, tx)), - crate::TargetChainTx::Ready(_) => unreachable!(), - } - } - None => None, - } - } -} - fn gas_pretty(gas: Gas) -> String { if gas < 1000 { format!("{} gas", gas) @@ -168,6 +109,16 @@ fn gas_pretty(gas: Gas) -> String { } } +#[derive(Clone, Debug)] +struct NonceInfo { + target_nonce: TargetNonce, + // the last height we have queued that references this access key. After + // we send the txs at that height, we'll delete this from memory so that + // the amount of memory we're using for these doesn't keep growing as we run for a while + last_height: BlockHeight, + txs_awaiting_nonce: BTreeSet, +} + // Keeps the queue of upcoming transactions and provides them in regular intervals via next_batch() // Also keeps track of txs we've sent so far and looks for them on chain, for metrics/logging purposes. #[derive(Default)] @@ -175,8 +126,10 @@ pub(crate) struct TxTracker { sent_txs: HashMap, txs_by_signer: HashMap<(AccountId, PublicKey), BTreeSet>, queued_blocks: VecDeque, - txs_awaiting_nonce: HashMap>, - pending_access_keys: HashMap<(AccountId, PublicKey), usize>, + // for each updater (a tx or receipt hash, or a queued transaction we haven't sent yet), keeps + // a set of access keys who might be updated by it + updater_to_keys: HashMap>, + nonces: HashMap<(AccountId, PublicKey), NonceInfo>, height_queued: Option, send_time: Option>>, // Config value in the target chain, used to judge how long to wait before sending a new batch of txs @@ -198,89 +151,182 @@ impl TxTracker { self.queued_blocks.len() } - pub(crate) fn pending_access_keys_iter<'a>( - &'a self, - ) -> impl Iterator { - self.pending_access_keys.iter().map(|(x, _)| x) - } + async fn read_target_nonce<'a>( + &'a mut self, + target_view_client: &Addr, + db: &DB, + access_key: &(AccountId, PublicKey), + source_height: BlockHeight, + ) -> anyhow::Result<&'a mut NonceInfo> { + if !self.nonces.contains_key(access_key) { + let nonce = + crate::fetch_access_key_nonce(target_view_client, &access_key.0, &access_key.1) + .await?; - pub(crate) fn tx_awaiting_nonce_iter<'a>(&'a self) -> TxAwaitingNonceIter<'a> { - TxAwaitingNonceIter::new(&self.queued_blocks, &self.txs_awaiting_nonce) - } + let info = match crate::read_target_nonce(db, &access_key.0, &access_key.1)? { + Some(mut t) => { + // lazily update it here. things might have changed since we last restarted + for id in t.pending_outcomes.iter() { + let access_keys = crate::read_access_key_outcome(db, id)?.unwrap(); + match crate::fetch_outcome(target_view_client, id).await? { + TxOutcome::TxPending { .. } => {} + TxOutcome::ReceiptPending(receipt_id) => { + if matches!(id, ChainObjectId::Tx(_)) { + self.tx_to_receipt(db, id, &receipt_id, access_keys)?; + } + } + TxOutcome::Failure | TxOutcome::Success => { + self.on_outcome_finished(target_view_client, db, id, access_keys) + .await? + } + TxOutcome::Unknown => { + tracing::warn!(target: "mirror", "can't find {:?} on chain even though it's stored on disk", id) + } + }; + } + // TODO: this ugliness is because we are not keeping track of what we have + // modified on disk and what has been modified in memory. need to fix w/ a struct StoreUpdate + t = crate::read_target_nonce(db, &access_key.0, &access_key.1)?.unwrap(); + t.nonce = std::cmp::max(t.nonce, nonce); + crate::put_target_nonce(db, &access_key.0, &access_key.1, &t)?; - fn pending_access_keys_deref( - &mut self, - source_signer_id: AccountId, - source_public_key: PublicKey, - ) { - match self.pending_access_keys.entry((source_signer_id, source_public_key)) { - hash_map::Entry::Occupied(mut e) => { - let ref_count = e.get_mut(); - if *ref_count == 1 { - e.remove(); - } else { - *ref_count -= 1; + NonceInfo { + target_nonce: TargetNonce { + nonce: t.nonce.clone(), + pending_outcomes: t + .pending_outcomes + .into_iter() + .map(NonceUpdater::ChainObjectId) + .collect(), + }, + last_height: source_height, + txs_awaiting_nonce: BTreeSet::new(), + } } - } - hash_map::Entry::Vacant(_) => unreachable!(), + None => { + let t = LatestTargetNonce { nonce, pending_outcomes: HashSet::new() }; + crate::put_target_nonce(db, &access_key.0, &access_key.1, &t)?; + NonceInfo { + target_nonce: TargetNonce { + nonce: t.nonce, + pending_outcomes: HashSet::new(), + }, + last_height: source_height, + txs_awaiting_nonce: BTreeSet::new(), + } + } + }; + self.nonces.insert(access_key.clone(), info); } + Ok(self.nonces.get_mut(access_key).unwrap()) } - // We now know of a valid nonce for the transaction referenced by tx_ref. - // Set the nonce and mark the tx as ready to be sent later. - pub(crate) fn set_tx_nonce(&mut self, tx_ref: &TxRef, nonce: Nonce) { - let block_idx = - self.queued_blocks.binary_search_by(|b| b.source_height.cmp(&tx_ref.height)).unwrap(); + pub(crate) async fn next_nonce<'a>( + &'a mut self, + target_view_client: &Addr, + db: &DB, + signer_id: &AccountId, + public_key: &PublicKey, + source_height: BlockHeight, + ) -> anyhow::Result<&'a TargetNonce> { + let info = self + .read_target_nonce( + target_view_client, + db, + &(signer_id.clone(), public_key.clone()), + source_height, + ) + .await?; + if source_height > info.last_height { + info.last_height = source_height; + } + if let Some(nonce) = &mut info.target_nonce.nonce { + *nonce += 1; + } + Ok(&info.target_nonce) + } + + fn get_tx(&mut self, tx_ref: &TxRef) -> &mut TargetChainTx { + let block_idx = self + .queued_blocks + .binary_search_by(|b| b.source_height.cmp(&tx_ref.source_height)) + .unwrap(); let block = &mut self.queued_blocks[block_idx]; let chunk = block.chunks.iter_mut().find(|c| c.shard_id == tx_ref.shard_id).unwrap(); - let tx = &mut chunk.txs[tx_ref.tx_idx]; + &mut chunk.txs[tx_ref.tx_idx] + } - match self.txs_awaiting_nonce.entry(tx_ref.height) { - hash_map::Entry::Occupied(mut e) => { - let txs = e.get_mut(); - if txs.len() == 1 { - assert!(&txs[0] == tx_ref); - e.remove(); - } else { - let idx = txs.iter().position(|t| t == tx_ref).unwrap(); - txs.swap_remove(idx); - } + async fn insert_access_key_updates( + &mut self, + target_view_client: &Addr, + db: &DB, + tx_ref: &TxRef, + nonce_updates: &HashSet<(AccountId, PublicKey)>, + source_height: BlockHeight, + ) -> anyhow::Result<()> { + for access_key in nonce_updates.iter() { + let info = + self.read_target_nonce(target_view_client, db, access_key, source_height).await?; + + if info.last_height < source_height { + info.last_height = source_height; } - hash_map::Entry::Vacant(_) => unreachable!(), + info.target_nonce.pending_outcomes.insert(NonceUpdater::TxRef(tx_ref.clone())); } - let (source_signer_id, source_public_key) = match &tx { - crate::TargetChainTx::AwaitingNonce(tx) => { - (tx.source_signer_id.clone(), tx.source_public.clone()) - } - crate::TargetChainTx::Ready(_) => unreachable!(), - }; - - tx.set_nonce(nonce); - self.pending_access_keys_deref(source_signer_id, source_public_key); + if !nonce_updates.is_empty() { + assert!(self + .updater_to_keys + .insert(NonceUpdater::TxRef(tx_ref.clone()), nonce_updates.clone()) + .is_none()); + } + Ok(()) } - pub(crate) fn queue_block(&mut self, block: MappedBlock) { + pub(crate) async fn queue_block( + &mut self, + block: MappedBlock, + target_view_client: &Addr, + db: &DB, + ) -> anyhow::Result<()> { self.height_queued = Some(block.source_height); - let mut txs_awaiting_nonce = Vec::new(); for c in block.chunks.iter() { for (tx_idx, tx) in c.txs.iter().enumerate() { - if let crate::TargetChainTx::AwaitingNonce(tx) = tx { - txs_awaiting_nonce.push(TxRef { - height: block.source_height, - shard_id: c.shard_id, - tx_idx, - }); - *self - .pending_access_keys - .entry((tx.source_signer_id.clone(), tx.source_public.clone())) - .or_default() += 1; - } + let tx_ref = + TxRef { source_height: block.source_height, shard_id: c.shard_id, tx_idx }; + match tx { + crate::TargetChainTx::Ready(tx) => { + self.insert_access_key_updates( + target_view_client, + db, + &tx_ref, + &tx.nonce_updates, + block.source_height, + ) + .await?; + } + crate::TargetChainTx::AwaitingNonce(tx) => { + let info = self + .nonces + .get_mut(&( + tx.target_tx.signer_id.clone(), + tx.target_tx.public_key.clone(), + )) + .unwrap(); + info.txs_awaiting_nonce.insert(tx_ref.clone()); + self.insert_access_key_updates( + target_view_client, + db, + &tx_ref, + &tx.nonce_updates, + block.source_height, + ) + .await?; + } + }; } } - if !txs_awaiting_nonce.is_empty() { - self.txs_awaiting_nonce.insert(block.source_height, txs_awaiting_nonce); - } self.queued_blocks.push_back(block); + Ok(()) } pub(crate) fn next_batch_time(&self) -> Instant { @@ -290,26 +336,71 @@ impl TxTracker { } } - pub(crate) async fn next_batch(&mut self) -> Option { - if let Some(sleep) = &mut self.send_time { - sleep.await; + pub(crate) async fn next_batch( + &mut self, + target_view_client: &Addr, + db: &DB, + ) -> anyhow::Result<&mut MappedBlock> { + // sleep until 20 milliseconds before we want to send transactions before we check for nonces + // in the target chain. In the second or so between now and then, we might process another block + // that will set the nonces. + if let Some(s) = &self.send_time { + tokio::time::sleep_until(s.as_ref().deadline() - Duration::from_millis(20)).await; } - let block = self.queued_blocks.pop_front(); - if let Some(block) = &block { - self.txs_awaiting_nonce.remove(&block.source_height); - for chunk in block.chunks.iter() { - for tx in chunk.txs.iter() { - match &tx { - crate::TargetChainTx::AwaitingNonce(tx) => self.pending_access_keys_deref( - tx.source_signer_id.clone(), - tx.source_public.clone(), - ), - crate::TargetChainTx::Ready(_) => {} - } + let mut needed_access_keys = HashSet::new(); + for c in self.queued_blocks[0].chunks.iter_mut() { + for tx in c.txs.iter_mut() { + if let TargetChainTx::AwaitingNonce(t) = tx { + needed_access_keys + .insert((t.target_tx.signer_id.clone(), t.target_tx.public_key.clone())); } } } - block + for access_key in needed_access_keys.iter() { + self.try_set_nonces(target_view_client, db, access_key, None).await?; + } + let block = &mut self.queued_blocks[0]; + for c in block.chunks.iter_mut() { + for (tx_idx, tx) in c.txs.iter_mut().enumerate() { + match tx { + TargetChainTx::AwaitingNonce(_) => { + let tx_ref = TxRef { + source_height: block.source_height, + shard_id: c.shard_id, + tx_idx, + }; + tx.try_set_nonce(None); + match tx { + TargetChainTx::Ready(t) => { + tracing::debug!( + target: "mirror", "Prepared {} for ({}, {:?}) with nonce {} even though there are still pending outcomes that may affect the access key", + &tx_ref, &t.target_tx.transaction.signer_id, &t.target_tx.transaction.public_key, t.target_tx.transaction.nonce + ); + self.nonces + .get_mut(&( + t.target_tx.transaction.signer_id.clone(), + t.target_tx.transaction.public_key.clone(), + )) + .unwrap() + .txs_awaiting_nonce + .remove(&tx_ref); + } + TargetChainTx::AwaitingNonce(t) => { + tracing::warn!( + target: "mirror", "Could not prepare {} for ({}, {:?}). Nonce unknown", + &tx_ref, &t.target_tx.signer_id, &t.target_tx.public_key, + ); + } + }; + } + TargetChainTx::Ready(_) => {} + }; + } + } + if let Some(sleep) = &mut self.send_time { + sleep.await; + } + Ok(block) } fn remove_tx(&mut self, tx: &IndexerTransactionWithOutcome) { @@ -437,47 +528,211 @@ impl TxTracker { tracing::debug!(target: "mirror", "received target block #{}:\n{}", msg.block.header.height, log_message); } - pub(crate) fn on_target_block(&mut self, msg: &StreamerMessage) { - self.record_block_timestamp(msg); - self.log_target_block(msg); + fn tx_to_receipt( + &mut self, + db: &DB, + id: &ChainObjectId, + receipt_id: &ReceiptInfo, + access_keys: HashSet<(AccountId, PublicKey)>, + ) -> anyhow::Result<()> { + crate::delete_access_key_outcome(db, id)?; - for s in msg.shards.iter() { - if let Some(c) = &s.chunk { - for tx in c.transactions.iter() { + let updater = NonceUpdater::ChainObjectId(id.clone()); + if let Some(keys) = self.updater_to_keys.remove(&updater) { + assert!(access_keys == keys); + } + + let receipt_id = ChainObjectId::Receipt(receipt_id.clone()); + let new_updater = NonceUpdater::ChainObjectId(receipt_id.clone()); + + for access_key in access_keys.iter() { + let mut n = crate::read_target_nonce(db, &access_key.0, &access_key.1)?.unwrap(); + assert!(n.pending_outcomes.remove(id)); + n.pending_outcomes.insert(receipt_id.clone()); + crate::put_target_nonce(db, &access_key.0, &access_key.1, &n)?; + + if let Some(info) = self.nonces.get(access_key) { + let txs_awaiting_nonce = info.txs_awaiting_nonce.clone(); + + for r in txs_awaiting_nonce.iter() { + let tx = self.get_tx(r); + + match tx { + TargetChainTx::AwaitingNonce(t) => { + assert!(t.target_nonce.pending_outcomes.remove(&updater)); + t.target_nonce.pending_outcomes.insert(new_updater.clone()); + } + TargetChainTx::Ready(_) => unreachable!(), + }; + } + + let info = self.nonces.get_mut(access_key).unwrap(); + assert!(info.target_nonce.pending_outcomes.remove(&updater)); + info.target_nonce.pending_outcomes.insert(new_updater.clone()); + } + } + crate::put_access_key_outcome(db, receipt_id.clone(), access_keys.clone())?; + self.updater_to_keys.insert(new_updater, access_keys); + Ok(()) + } + + async fn try_set_nonces( + &mut self, + target_view_client: &Addr, + db: &DB, + access_key: &(AccountId, PublicKey), + id: Option<&ChainObjectId>, + ) -> anyhow::Result<()> { + let mut n = crate::read_target_nonce(db, &access_key.0, &access_key.1)?.unwrap(); + if let Some(id) = id { + n.pending_outcomes.remove(id); + } + let mut nonce = + crate::fetch_access_key_nonce(target_view_client, &access_key.0, &access_key.1).await?; + n.nonce = std::cmp::max(n.nonce, nonce); + + crate::put_target_nonce(db, &access_key.0, &access_key.1, &n)?; + + let updater = id.map(|id| NonceUpdater::ChainObjectId(id.clone())); + if let Some(info) = self.nonces.get_mut(access_key) { + if let Some(updater) = &updater { + info.target_nonce.pending_outcomes.remove(updater); + } + let txs_awaiting_nonce = info.txs_awaiting_nonce.clone(); + let mut to_remove = Vec::new(); + + for r in txs_awaiting_nonce.iter() { + let tx = self.get_tx(r); + + match tx { + TargetChainTx::AwaitingNonce(t) => { + if let Some(updater) = &updater { + t.target_nonce.pending_outcomes.remove(updater); + } + if let Some(nonce) = &mut nonce { + *nonce += 1; + } + + if t.target_nonce.pending_outcomes.is_empty() { + to_remove.push(r.clone()); + tx.try_set_nonce(nonce); + match tx { + TargetChainTx::Ready(t) => { + tracing::debug!(target: "mirror", "set nonce for {:?}'s {} to {}", access_key, r, t.target_tx.transaction.nonce); + } + _ => { + tracing::warn!(target: "mirror", "Couldn't set nonce for {:?}'s {}", access_key, r); + } + } + } else { + t.target_nonce.nonce = std::cmp::max(t.target_nonce.nonce, nonce); + } + } + TargetChainTx::Ready(_) => unreachable!(), + }; + } + + let info = self.nonces.get_mut(access_key).unwrap(); + for r in to_remove.iter() { + info.txs_awaiting_nonce.remove(r); + } + info.target_nonce.nonce = std::cmp::max(info.target_nonce.nonce, nonce); + } + Ok(()) + } + + async fn on_outcome_finished( + &mut self, + target_view_client: &Addr, + db: &DB, + id: &ChainObjectId, + access_keys: HashSet<(AccountId, PublicKey)>, + ) -> anyhow::Result<()> { + let updater = NonceUpdater::ChainObjectId(id.clone()); + if let Some(keys) = self.updater_to_keys.remove(&updater) { + assert!(access_keys == keys); + } + + for access_key in access_keys.iter() { + self.try_set_nonces(target_view_client, db, &access_key, Some(id)).await?; + } + crate::delete_access_key_outcome(db, id) + } + + pub(crate) async fn on_target_block( + &mut self, + target_view_client: &Addr, + db: &DB, + msg: StreamerMessage, + ) -> anyhow::Result<()> { + self.record_block_timestamp(&msg); + self.log_target_block(&msg); + + for s in msg.shards { + if let Some(c) = s.chunk { + for tx in c.transactions { if self.sent_txs.remove(&tx.transaction.hash).is_some() { crate::metrics::TRANSACTIONS_INCLUDED.inc(); - self.remove_tx(tx); + self.remove_tx(&tx); + } + let id = ChainObjectId::Tx(TxInfo { + hash: tx.transaction.hash, + signer_id: tx.transaction.signer_id, + receiver_id: tx.transaction.receiver_id, + }); + if let Some(access_keys) = crate::read_access_key_outcome(db, &id)? { + match crate::fetch_outcome(target_view_client, &id).await? { + TxOutcome::TxPending{..} | TxOutcome::Unknown => anyhow::bail!("can't find outcome for target chain tx {} even though HEAD block includes it", &tx.transaction.hash), + TxOutcome::ReceiptPending(receipt_id) => self.tx_to_receipt(db, &id, &receipt_id, access_keys)?, + TxOutcome::Failure | TxOutcome::Success => self.on_outcome_finished(target_view_client, db, &id, access_keys).await?, + }; + } + } + for r in c.receipts { + let id = ChainObjectId::Receipt(ReceiptInfo { + id: r.receipt_id, + receiver_id: r.receiver_id.clone(), + }); + if let Some(access_keys) = crate::read_access_key_outcome(db, &id)? { + match crate::fetch_outcome(target_view_client, &id).await? { + TxOutcome::TxPending{..} => unreachable!(), + TxOutcome::ReceiptPending(_) | TxOutcome::Unknown => anyhow::bail!("can't find outcome for target chain receipt {} even though HEAD block includes it", &r.receipt_id), + TxOutcome::Failure | TxOutcome::Success => self.on_outcome_finished(target_view_client, db, &id, access_keys).await?, + }; } } } } + Ok(()) } - fn on_tx_sent( + async fn on_tx_sent( &mut self, - tx: &MappedTx, - source_shard_id: ShardId, - source_height: BlockHeight, + target_view_client: &Addr, + db: &DB, + tx_ref: TxRef, + tx: MappedTx, target_height: BlockHeight, now: Instant, - ) { + access_keys_to_remove: &mut HashSet<(AccountId, PublicKey)>, + ) -> anyhow::Result<()> { let hash = tx.target_tx.get_hash(); if self.sent_txs.contains_key(&hash) { tracing::warn!(target: "mirror", "transaction sent twice: {}", &hash); - return; + return Ok(()); } + let access_key = ( + tx.target_tx.transaction.signer_id.clone(), + tx.target_tx.transaction.public_key.clone(), + ); // TODO: don't keep adding txs if we're not ever finding them on chain, since we'll OOM eventually // if that happens. - self.sent_txs - .insert(hash, TxSendInfo::new(tx, source_shard_id, source_height, target_height, now)); - let txs = self - .txs_by_signer - .entry(( - tx.target_tx.transaction.signer_id.clone(), - tx.target_tx.transaction.public_key.clone(), - )) - .or_default(); + self.sent_txs.insert( + hash, + TxSendInfo::new(&tx, tx_ref.shard_id, tx_ref.source_height, target_height, now), + ); + let txs = self.txs_by_signer.entry(access_key.clone()).or_default(); if let Some(highest_nonce) = txs.iter().next_back() { if highest_nonce.nonce > tx.target_tx.transaction.nonce { @@ -490,10 +745,91 @@ impl TxTracker { if !txs.insert(TxId { hash, nonce: tx.target_tx.transaction.nonce }) { tracing::warn!(target: "mirror", "inserted tx {} twice into txs_by_signer", &hash); } + + let source_height = tx_ref.source_height; + let updater = NonceUpdater::TxRef(tx_ref); + + let new_updater = NonceUpdater::ChainObjectId(ChainObjectId::Tx(TxInfo { + hash: tx.target_tx.get_hash(), + signer_id: tx.target_tx.transaction.signer_id.clone(), + receiver_id: tx.target_tx.transaction.receiver_id.clone(), + })); + if !tx.nonce_updates.is_empty() { + assert!(&tx.nonce_updates == &self.updater_to_keys.remove(&updater).unwrap()); + for access_key in tx.nonce_updates.iter() { + let mut t = match crate::read_target_nonce(db, &access_key.0, &access_key.1)? { + Some(t) => t, + None => { + let nonce = crate::fetch_access_key_nonce( + target_view_client, + &access_key.0, + &access_key.1, + ) + .await?; + LatestTargetNonce { nonce, pending_outcomes: HashSet::new() } + } + }; + t.pending_outcomes.insert(ChainObjectId::Tx(TxInfo { + hash, + signer_id: tx.target_tx.transaction.signer_id.clone(), + receiver_id: tx.target_tx.transaction.receiver_id.clone(), + })); + crate::put_target_nonce(db, &access_key.0, &access_key.1, &t)?; + + let info = self.nonces.get_mut(access_key).unwrap(); + assert!(info.target_nonce.pending_outcomes.remove(&updater)); + info.target_nonce.pending_outcomes.insert(new_updater.clone()); + let txs_awaiting_nonce = info.txs_awaiting_nonce.clone(); + if info.last_height <= source_height { + access_keys_to_remove.insert(access_key.clone()); + } + + for r in txs_awaiting_nonce.iter() { + let t = self.get_tx(r); + + match t { + TargetChainTx::AwaitingNonce(t) => { + assert!(t.target_nonce.pending_outcomes.remove(&updater)); + t.target_nonce.pending_outcomes.insert(new_updater.clone()); + } + TargetChainTx::Ready(_) => unreachable!(), + }; + } + } + + crate::put_access_key_outcome( + db, + ChainObjectId::Tx(TxInfo { + hash, + signer_id: tx.target_tx.transaction.signer_id.clone(), + receiver_id: tx.target_tx.transaction.receiver_id.clone(), + }), + tx.nonce_updates, + )?; + } + let mut t = crate::read_target_nonce( + db, + &tx.target_tx.transaction.signer_id, + &tx.target_tx.transaction.public_key, + )? + .unwrap(); + t.nonce = std::cmp::max(t.nonce, Some(tx.target_tx.transaction.nonce)); + crate::put_target_nonce( + db, + &tx.target_tx.transaction.signer_id, + &tx.target_tx.transaction.public_key, + &t, + )?; + if self.nonces.get(&access_key).unwrap().last_height <= source_height { + access_keys_to_remove.insert(access_key); + } + Ok(()) } // among the last 10 blocks, what's the second longest time between their timestamps? // probably there's a better heuristic to use than that but this will do for now. + // TODO: it's possible these tiimestamps are just increasing by one nanosecond each time + // if block producers' clocks are off. should handle that case fn second_longest_recent_block_delay(&self) -> Option { if self.recent_block_timestamps.len() < 5 { return None; @@ -543,24 +879,145 @@ impl TxTracker { Some(delay) } - // We just successfully sent some transactions. Remember them so we can see if they really show up on chain. - pub(crate) fn on_txs_sent( + fn on_tx_skipped( &mut self, - txs: &[(ShardId, Vec)], + tx_ref: &TxRef, + tx: &Transaction, + nonce_updates: &HashSet<(AccountId, PublicKey)>, source_height: BlockHeight, + access_keys_to_remove: &mut HashSet<(AccountId, PublicKey)>, + ) -> anyhow::Result<()> { + let updater = NonceUpdater::TxRef(tx_ref.clone()); + if let Some(keys) = self.updater_to_keys.remove(&updater) { + assert!(&keys == nonce_updates); + + for access_key in keys { + if let Some(info) = self.nonces.get(&access_key) { + let txs_awaiting_nonce = info.txs_awaiting_nonce.clone(); + let mut to_remove = Vec::new(); + for r in txs_awaiting_nonce.iter() { + let target_tx = self.get_tx(r); + match target_tx { + TargetChainTx::AwaitingNonce(tx) => { + assert!(tx.target_nonce.pending_outcomes.remove(&updater)); + if tx.target_nonce.pending_outcomes.is_empty() { + target_tx.try_set_nonce(None); + match target_tx { + TargetChainTx::Ready(t) => { + tracing::debug!(target: "mirror", "After skipping {} setting nonce for {:?}'s {} to {}", tx_ref, &access_key, r, t.target_tx.transaction.nonce); + } + _ => { + tracing::warn!(target: "mirror", "After skipping {} could not set nonce for {:?}'s {}", tx_ref, &access_key, r); + } + } + to_remove.push(r.clone()); + } + } + TargetChainTx::Ready(_) => unreachable!(), + } + } + + let info = self.nonces.get_mut(&access_key).unwrap(); + for r in to_remove.iter() { + info.txs_awaiting_nonce.remove(r); + } + + if info.last_height <= source_height { + access_keys_to_remove.insert(access_key); + } + } + } + } + let access_key = (tx.signer_id.clone(), tx.public_key.clone()); + if self.nonces.get(&access_key).unwrap().last_height <= source_height { + access_keys_to_remove.insert(access_key); + } + Ok(()) + } + + fn pop_block(&mut self) -> MappedBlock { + let block = self.queued_blocks.pop_front().unwrap(); + for chunk in block.chunks.iter() { + for (tx_idx, tx) in chunk.txs.iter().enumerate() { + let access_key = match tx { + crate::TargetChainTx::Ready(tx) => ( + tx.target_tx.transaction.signer_id.clone(), + tx.target_tx.transaction.public_key.clone(), + ), + crate::TargetChainTx::AwaitingNonce(tx) => { + (tx.target_tx.signer_id.clone(), tx.target_tx.public_key.clone()) + } + }; + let tx_ref = + TxRef { source_height: block.source_height, shard_id: chunk.shard_id, tx_idx }; + self.nonces.get_mut(&access_key).unwrap().txs_awaiting_nonce.remove(&tx_ref); + } + } + block + } + + // We just successfully sent some transactions. Remember them so we can see if they really show up on chain. + pub(crate) async fn on_txs_sent( + &mut self, + target_view_client: &Addr, + db: &DB, target_height: BlockHeight, - ) { - let num_txs: usize = txs.iter().map(|(_, txs)| txs.len()).sum(); - tracing::info!( - target: "mirror", "Sent {} transactions from source #{} with target HEAD @ #{}", - num_txs, source_height, target_height - ); + ) -> anyhow::Result<()> { + let block = self.pop_block(); + let source_height = block.source_height; + let mut total_sent = 0; let now = Instant::now(); - for (shard_id, txs) in txs.iter() { - for tx in txs.iter() { - self.on_tx_sent(tx, *shard_id, source_height, target_height, now); + let mut access_keys_to_remove = HashSet::new(); + + for chunk in block.chunks.into_iter() { + for (tx_idx, tx) in chunk.txs.into_iter().enumerate() { + let tx_ref = + TxRef { source_height: block.source_height, shard_id: chunk.shard_id, tx_idx }; + match tx { + crate::TargetChainTx::Ready(t) => { + if t.sent_successfully { + self.on_tx_sent( + target_view_client, + db, + tx_ref, + t, + target_height, + now, + &mut access_keys_to_remove, + ) + .await?; + total_sent += 1; + } else { + self.on_tx_skipped( + &tx_ref, + &t.target_tx.transaction, + &t.nonce_updates, + block.source_height, + &mut access_keys_to_remove, + )?; + } + } + crate::TargetChainTx::AwaitingNonce(t) => { + self.on_tx_skipped( + &tx_ref, + &t.target_tx, + &t.nonce_updates, + block.source_height, + &mut access_keys_to_remove, + )?; + } + } } } + crate::set_next_source_height(db, block.source_height + 1)?; + + for access_key in access_keys_to_remove { + assert!(self.nonces.remove(&access_key).is_some()); + } + tracing::info!( + target: "mirror", "Sent {} transactions from source #{} with target HEAD @ #{}", + total_sent, source_height, target_height + ); let block_delay = self .second_longest_recent_block_delay() @@ -571,5 +1028,6 @@ impl TxTracker { self.send_time = Some(Box::pin(tokio::time::sleep(block_delay))); } } + Ok(()) } } diff --git a/tools/mirror/src/lib.rs b/tools/mirror/src/lib.rs index 932749f32ad..018be33f65c 100644 --- a/tools/mirror/src/lib.rs +++ b/tools/mirror/src/lib.rs @@ -5,8 +5,8 @@ use near_chain_configs::GenesisValidationMode; use near_client::{ClientActor, ViewClientActor}; use near_client::{ProcessTxRequest, ProcessTxResponse}; use near_client_primitives::types::{ - GetBlock, GetBlockError, GetChunk, GetChunkError, GetExecutionOutcome, - GetExecutionOutcomeError, GetExecutionOutcomeResponse, Query, QueryError, + GetChunk, GetChunkError, GetExecutionOutcome, GetExecutionOutcomeError, + GetExecutionOutcomeResponse, Query, QueryError, }; use near_crypto::{PublicKey, SecretKey}; use near_indexer::{Indexer, StreamerMessage}; @@ -16,7 +16,7 @@ use near_primitives::transaction::{ Action, AddKeyAction, DeleteKeyAction, SignedTransaction, Transaction, }; use near_primitives::types::{ - AccountId, BlockHeight, BlockId, BlockReference, Finality, TransactionOrReceiptId, + AccountId, BlockHeight, BlockReference, Finality, TransactionOrReceiptId, }; use near_primitives::views::{ ExecutionStatusView, QueryRequest, QueryResponseKind, SignedTransactionView, @@ -48,6 +48,7 @@ enum DBCol { // and there's no entry in the DB, then the key was present in the genesis // state. Otherwise, we map tx nonces according to the values in this column. Nonces, + AccessKeyOutcomes, } impl DBCol { @@ -55,20 +56,86 @@ impl DBCol { match self { Self::Misc => "miscellaneous", Self::Nonces => "nonces", + Self::AccessKeyOutcomes => "access_key_outcomes", } } } +// TODO: maybe move these type defs to `mod types` or something +#[derive(BorshDeserialize, BorshSerialize, Clone, Debug, PartialEq, Eq, PartialOrd, Hash)] +struct TxInfo { + hash: CryptoHash, + signer_id: AccountId, + receiver_id: AccountId, +} + +#[derive(BorshDeserialize, BorshSerialize, Clone, Debug, PartialEq, Eq, PartialOrd, Hash)] +struct ReceiptInfo { + id: CryptoHash, + receiver_id: AccountId, +} + +#[derive(BorshDeserialize, BorshSerialize, Clone, Debug, PartialEq, Eq, PartialOrd, Hash)] +enum ChainObjectId { + Tx(TxInfo), + Receipt(ReceiptInfo), +} + +// we want a reference to transactions in .queued_blocks that need to have nonces +// set later. To avoid having the struct be self referential we keep this struct +// with enough info to look it up later. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +struct TxRef { + source_height: BlockHeight, + shard_id: ShardId, + tx_idx: usize, +} + +impl std::fmt::Display for TxRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "source #{} shard {} tx {}", self.source_height, self.shard_id, self.tx_idx) + } +} + +impl PartialOrd for TxRef { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for TxRef { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.source_height + .cmp(&other.source_height) + .then_with(|| self.tx_idx.cmp(&other.tx_idx)) + .then_with(|| self.shard_id.cmp(&other.shard_id)) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +enum NonceUpdater { + TxRef(TxRef), + ChainObjectId(ChainObjectId), +} + // returns bytes that serve as the key corresponding to this pair in the Nonces column fn nonce_col_key(account_id: &AccountId, public_key: &PublicKey) -> Vec { (account_id.clone(), public_key.clone()).try_to_vec().unwrap() } -#[derive(Clone, BorshDeserialize, BorshSerialize, Debug, PartialEq, Eq, PartialOrd, Hash)] -struct TxIds { - tx_hash: CryptoHash, - signer_id: AccountId, - receiver_id: AccountId, +// this serves a similar purpose to `LatestTargetNonce`. The difference is +// that this one keeps track of what's in memory. So for example if the last +// height we sent transactions for was 10, and we have a set of transactions +// queued up for height 12, one of which is an AddKey for ('foo.near', 'ed25519:...'), +// then we'lll want to remember that for txs of height > 12 that use that +// signer id and public key, but we don't want to store that on disk. The `LatestTargetNonce` +// on disk will only record the transactions/receipts updating the nonce that we actually sent +// or saw on chain + +#[derive(Clone, Debug)] +struct TargetNonce { + nonce: Option, + pending_outcomes: HashSet, } // For a given AddKey Action, records the starting nonces of the @@ -77,46 +144,81 @@ struct TxIds { // ignored, and it's set to block_height*1000000, so to generate // transactions with valid nonces, we need to map valid source chain // nonces to valid target chain nonces. -#[derive(BorshDeserialize, BorshSerialize, Debug, Default)] -struct NonceDiff { - source_start: Option, - target_start: Option, - pending_source_txs: HashSet, +#[derive(BorshDeserialize, BorshSerialize, Debug)] +struct LatestTargetNonce { + nonce: Option, + pending_outcomes: HashSet, } -#[derive(thiserror::Error, Debug)] -pub(crate) enum MapNonceError { - #[error("Source chain access key not yet on chain")] - SourceKeyNotOnChain, - #[error("Target chain access key not yet on chain")] - TargetKeyNotOnChain, - #[error("Nonce arithmetic overflow: {0} + {1}")] - AddOverflow(Nonce, Nonce), - #[error("Nonce arithmetic overflow: {0} - {1}")] - SubOverflow(Nonce, Nonce), +// TODO: move DB related stuff to its own file and add a way to +// keep track of updates in memory and write them all in one transaction +fn read_target_nonce( + db: &DB, + account_id: &AccountId, + public_key: &PublicKey, +) -> anyhow::Result> { + let db_key = nonce_col_key(account_id, public_key); + Ok(db + .get_cf(db.cf_handle(DBCol::Nonces.name()).unwrap(), &db_key)? + .map(|v| LatestTargetNonce::try_from_slice(&v).unwrap())) } -impl NonceDiff { - fn set_source(&mut self, nonce: Nonce) { - self.source_start = Some(nonce); - self.pending_source_txs.clear(); - } +fn put_target_nonce( + db: &DB, + account_id: &AccountId, + public_key: &PublicKey, + nonce: &LatestTargetNonce, +) -> anyhow::Result<()> { + tracing::debug!(target: "mirror", "storing {:?} in DB for ({}, {:?})", &nonce, account_id, public_key); + let db_key = nonce_col_key(account_id, public_key); + db.put_cf(db.cf_handle(DBCol::Nonces.name()).unwrap(), &db_key, &nonce.try_to_vec().unwrap())?; + Ok(()) +} - fn map(&self, nonce: Nonce) -> Result { - let source_start = self.source_start.ok_or(MapNonceError::SourceKeyNotOnChain)?; - let target_start = self.target_start.ok_or(MapNonceError::TargetKeyNotOnChain)?; - if target_start > source_start { - let diff = target_start - source_start; - nonce.checked_add(diff).ok_or_else(|| MapNonceError::AddOverflow(nonce, diff)) - } else { - let diff = source_start - target_start; - nonce.checked_sub(diff).ok_or_else(|| MapNonceError::SubOverflow(nonce, diff)) - } - } +fn read_access_key_outcome( + db: &DB, + id: &ChainObjectId, +) -> anyhow::Result>> { + Ok(db + .get_cf(db.cf_handle(DBCol::AccessKeyOutcomes.name()).unwrap(), &id.try_to_vec().unwrap())? + .map(|v| HashSet::try_from_slice(&v).unwrap())) +} - fn known(&self) -> bool { - self.source_start.is_some() && self.target_start.is_some() - } +fn put_access_key_outcome( + db: &DB, + id: ChainObjectId, + access_keys: HashSet<(AccountId, PublicKey)>, +) -> anyhow::Result<()> { + tracing::debug!(target: "mirror", "storing {:?} in DB for {:?}", &access_keys, &id); + Ok(db.put_cf( + db.cf_handle(DBCol::AccessKeyOutcomes.name()).unwrap(), + &id.try_to_vec().unwrap(), + &access_keys.try_to_vec().unwrap(), + )?) +} + +fn delete_access_key_outcome(db: &DB, id: &ChainObjectId) -> anyhow::Result<()> { + tracing::debug!(target: "mirror", "deleting {:?} from DB", &id); + Ok(db.delete_cf( + db.cf_handle(DBCol::AccessKeyOutcomes.name()).unwrap(), + &id.try_to_vec().unwrap(), + )?) +} + +fn set_next_source_height(db: &DB, height: BlockHeight) -> anyhow::Result<()> { + // TODO: we should instead save something like the + // (block_height, shard_id, idx_in_chunk) of the last + // transaction sent. Currently we set next_source_height after + // sending all of the transactions in that chunk, so if we get + // SIGTERM or something in the middle of sending a batch of + // txs, we'll send some that we already sent next time we + // start. Not a giant problem but kind of unclean. + db.put_cf( + db.cf_handle(DBCol::Misc.name()).unwrap(), + "next_source_height", + height.try_to_vec().unwrap(), + )?; + Ok(()) } struct TxMirror { @@ -130,7 +232,6 @@ struct TxMirror { target_min_block_production_delay: Duration, tracked_shards: Vec, secret: Option<[u8; crate::secret::SECRET_LEN]>, - next_source_height: Option, } fn open_db>(home: P, config: &NearConfig) -> anyhow::Result { @@ -152,28 +253,44 @@ fn open_db>(home: P, config: &NearConfig) -> anyhow::Result { // except for the nonce field. #[derive(Debug)] struct TxAwaitingNonce { - source_public: PublicKey, source_signer_id: AccountId, source_receiver_id: AccountId, source_tx_index: usize, target_private: SecretKey, target_tx: Transaction, + nonce_updates: HashSet<(AccountId, PublicKey)>, + target_nonce: TargetNonce, } impl TxAwaitingNonce { fn new( source_tx: &SignedTransactionView, source_tx_index: usize, - target_tx: Transaction, + actions: Vec, + target_nonce: &TargetNonce, + ref_hash: &CryptoHash, + target_signer_id: AccountId, + target_receiver_id: AccountId, target_private: SecretKey, + target_public_key: PublicKey, + nonce_updates: HashSet<(AccountId, PublicKey)>, ) -> Self { + let mut target_tx = Transaction::new( + target_signer_id, + target_public_key, + target_receiver_id, + source_tx.nonce, + ref_hash.clone(), + ); + target_tx.actions = actions; Self { - source_public: source_tx.public_key.clone(), source_signer_id: source_tx.signer_id.clone(), source_receiver_id: source_tx.receiver_id.clone(), source_tx_index, target_private, target_tx, + nonce_updates, + target_nonce: target_nonce.clone(), } } } @@ -187,19 +304,42 @@ struct MappedTx { source_receiver_id: AccountId, source_tx_index: usize, target_tx: SignedTransaction, + nonce_updates: HashSet<(AccountId, PublicKey)>, + sent_successfully: bool, } impl MappedTx { fn new( source_tx: &SignedTransactionView, source_tx_index: usize, - target_tx: SignedTransaction, + actions: Vec, + nonce: Nonce, + ref_hash: &CryptoHash, + target_signer_id: AccountId, + target_receiver_id: AccountId, + target_secret_key: &SecretKey, + target_public_key: PublicKey, + nonce_updates: HashSet<(AccountId, PublicKey)>, ) -> Self { + let mut target_tx = Transaction::new( + target_signer_id, + target_public_key, + target_receiver_id, + nonce, + ref_hash.clone(), + ); + target_tx.actions = actions; + let target_tx = SignedTransaction::new( + target_secret_key.sign(&target_tx.get_hash_and_size().0.as_ref()), + target_tx, + ); Self { source_signer_id: source_tx.signer_id.clone(), source_receiver_id: source_tx.receiver_id.clone(), source_tx_index, target_tx, + nonce_updates, + sent_successfully: false, } } } @@ -211,8 +351,6 @@ enum TargetChainTx { } impl TargetChainTx { - // For an AwaitingNonce(_), set the nonce and sign the transaction, changing self into Ready(_). - // must not be called if self is Ready(_) fn set_nonce(&mut self, nonce: Nonce) { match self { Self::AwaitingNonce(t) => { @@ -221,19 +359,82 @@ impl TargetChainTx { t.target_private.sign(&t.target_tx.get_hash_and_size().0.as_ref()), t.target_tx.clone(), ); - tracing::debug!( - target: "mirror", "prepared a transaction for ({:?}, {:?}) that was previously waiting for the access key to appear on chain", - &t.source_signer_id, &t.source_public, - ); *self = Self::Ready(MappedTx { source_signer_id: t.source_signer_id.clone(), source_receiver_id: t.source_receiver_id.clone(), source_tx_index: t.source_tx_index, target_tx, + nonce_updates: t.nonce_updates.clone(), + sent_successfully: false, }); } Self::Ready(_) => unreachable!(), - } + }; + } + + // For an AwaitingNonce(_), set the nonce and sign the transaction, changing self into Ready(_). + // must not be called if self is Ready(_) + fn try_set_nonce(&mut self, nonce: Option) { + let nonce = match self { + Self::AwaitingNonce(t) => match std::cmp::max(t.target_nonce.nonce, nonce) { + Some(n) => n, + None => return, + }, + Self::Ready(_) => unreachable!(), + }; + self.set_nonce(nonce); + } + + fn new_ready( + source_tx: &SignedTransactionView, + source_tx_index: usize, + actions: Vec, + nonce: Nonce, + ref_hash: &CryptoHash, + target_signer_id: AccountId, + target_receiver_id: AccountId, + target_secret_key: &SecretKey, + target_public_key: PublicKey, + nonce_updates: HashSet<(AccountId, PublicKey)>, + ) -> Self { + Self::Ready(MappedTx::new( + source_tx, + source_tx_index, + actions, + nonce, + &ref_hash, + target_signer_id, + target_receiver_id, + target_secret_key, + target_public_key, + nonce_updates, + )) + } + + fn new_awaiting_nonce( + source_tx: &SignedTransactionView, + source_tx_index: usize, + actions: Vec, + target_nonce: &TargetNonce, + ref_hash: &CryptoHash, + target_signer_id: AccountId, + target_receiver_id: AccountId, + target_private: SecretKey, + target_public_key: PublicKey, + nonce_updates: HashSet<(AccountId, PublicKey)>, + ) -> Self { + Self::AwaitingNonce(TxAwaitingNonce::new( + source_tx, + source_tx_index, + actions, + target_nonce, + &ref_hash, + target_signer_id, + target_receiver_id, + target_private, + target_public_key, + nonce_updates, + )) } } @@ -252,12 +453,11 @@ struct MappedBlock { async fn account_exists( view_client: &Addr, account_id: &AccountId, - prev_block: &CryptoHash, ) -> anyhow::Result { match view_client .send( Query::new( - BlockReference::BlockId(BlockId::Hash(prev_block.clone())), + BlockReference::Finality(Finality::None), QueryRequest::ViewAccount { account_id: account_id.clone() }, ) .with_span_context(), @@ -281,16 +481,11 @@ async fn fetch_access_key_nonce( view_client: &Addr, account_id: &AccountId, public_key: &PublicKey, - block_hash: Option<&CryptoHash>, ) -> anyhow::Result> { - let block_ref = match block_hash { - Some(h) => BlockReference::BlockId(BlockId::Hash(h.clone())), - None => BlockReference::Finality(Finality::None), - }; match view_client .send( Query::new( - block_ref, + BlockReference::Finality(Finality::None), QueryRequest::ViewAccessKey { account_id: account_id.clone(), public_key: public_key.clone(), @@ -298,7 +493,8 @@ async fn fetch_access_key_nonce( ) .with_span_context(), ) - .await? + .await + .unwrap() { Ok(res) => match res.kind { QueryResponseKind::AccessKey(access_key) => Ok(Some(access_key.nonce)), @@ -306,29 +502,42 @@ async fn fetch_access_key_nonce( panic!("Received unexpected QueryResponse after Querying Access Key: {:?}", other); } }, - Err(_) => Ok(None), + Err(e) => match &e { + QueryError::UnknownAccessKey { .. } => Ok(None), + _ => Err(e.into()), + }, } } #[derive(Clone, Debug)] enum TxOutcome { - Success(CryptoHash), - Pending, + Unknown, + TxPending(TxInfo), + ReceiptPending(ReceiptInfo), + Success, Failure, } +async fn fetch_outcome( + view_client: &Addr, + id: &ChainObjectId, +) -> anyhow::Result { + match id.clone() { + ChainObjectId::Tx(id) => fetch_tx_outcome(view_client, id).await, + ChainObjectId::Receipt(id) => fetch_receipt_outcome(view_client, id).await, + } +} + async fn fetch_tx_outcome( view_client: &Addr, - transaction_hash: CryptoHash, - signer_id: &AccountId, - receiver_id: &AccountId, + id: TxInfo, ) -> anyhow::Result { - let receipt_id = match view_client + match view_client .send( GetExecutionOutcome { id: TransactionOrReceiptId::Transaction { - transaction_hash, - sender_id: signer_id.clone(), + transaction_hash: id.hash, + sender_id: id.signer_id.clone(), }, } .with_span_context(), @@ -338,28 +547,38 @@ async fn fetch_tx_outcome( { Ok(GetExecutionOutcomeResponse { outcome_proof, .. }) => { match outcome_proof.outcome.status { - ExecutionStatusView::SuccessReceiptId(id) => id, + ExecutionStatusView::SuccessReceiptId(receipt_id) => { + fetch_receipt_outcome( + view_client, + ReceiptInfo { id: receipt_id, receiver_id: id.receiver_id.clone() }, + ) + .await + } ExecutionStatusView::SuccessValue(_) => unreachable!(), ExecutionStatusView::Failure(_) | ExecutionStatusView::Unknown => { - return Ok(TxOutcome::Failure) + Ok(TxOutcome::Failure) } } } + Err(GetExecutionOutcomeError::UnknownTransactionOrReceipt { .. }) => Ok(TxOutcome::Unknown), Err( GetExecutionOutcomeError::NotConfirmed { .. } | GetExecutionOutcomeError::UnknownBlock { .. }, - ) => return Ok(TxOutcome::Pending), - Err(e) => { - return Err(e) - .with_context(|| format!("failed fetching outcome for tx {}", transaction_hash)) - } - }; + ) => Ok(TxOutcome::TxPending(id)), + Err(e) => Err(e).with_context(|| format!("failed fetching outcome for tx {}", &id.hash)), + } +} + +async fn fetch_receipt_outcome( + view_client: &Addr, + id: ReceiptInfo, +) -> anyhow::Result { match view_client .send( GetExecutionOutcome { id: TransactionOrReceiptId::Receipt { - receipt_id, - receiver_id: receiver_id.clone(), + receipt_id: id.id.clone(), + receiver_id: id.receiver_id.clone(), }, } .with_span_context(), @@ -370,22 +589,7 @@ async fn fetch_tx_outcome( Ok(GetExecutionOutcomeResponse { outcome_proof, .. }) => { match outcome_proof.outcome.status { ExecutionStatusView::SuccessReceiptId(_) | ExecutionStatusView::SuccessValue(_) => { - // the view client code actually modifies the outcome's block_hash field to be the - // next block with a new chunk in the relevant shard, so go backwards one block, - // since that's what we'll want to give in the query for AccessKeys - let block = view_client - .send( - GetBlock(BlockReference::BlockId(BlockId::Hash( - outcome_proof.block_hash, - ))) - .with_span_context(), - ) - .await - .unwrap() - .with_context(|| { - format!("failed fetching block {}", &outcome_proof.block_hash) - })?; - Ok(TxOutcome::Success(block.header.prev_hash)) + Ok(TxOutcome::Success) } ExecutionStatusView::Failure(_) | ExecutionStatusView::Unknown => { Ok(TxOutcome::Failure) @@ -396,25 +600,11 @@ async fn fetch_tx_outcome( GetExecutionOutcomeError::NotConfirmed { .. } | GetExecutionOutcomeError::UnknownBlock { .. } | GetExecutionOutcomeError::UnknownTransactionOrReceipt { .. }, - ) => Ok(TxOutcome::Pending), - Err(e) => { - Err(e).with_context(|| format!("failed fetching outcome for receipt {}", &receipt_id)) - } + ) => Ok(TxOutcome::ReceiptPending(id)), + Err(e) => Err(e).with_context(|| format!("failed fetching outcome for receipt {}", &id.id)), } } -async fn block_hash_to_height( - view_client: &Addr, - hash: &CryptoHash, -) -> anyhow::Result { - Ok(view_client - .send(GetBlock(BlockReference::BlockId(BlockId::Hash(hash.clone()))).with_span_context()) - .await - .unwrap()? - .header - .height) -} - impl TxMirror { fn new>( source_home: P, @@ -459,34 +649,21 @@ impl TxMirror { .min_block_production_delay, tracked_shards: target_config.config.tracked_shards.clone(), secret, - next_source_height: None, }) } fn get_next_source_height(&mut self) -> anyhow::Result { - if let Some(height) = self.next_source_height { - return Ok(height); - } let height = self.db.get_cf(self.db.cf_handle(DBCol::Misc.name()).unwrap(), "next_source_height")?; match height { - Some(h) => { - let height = BlockHeight::try_from_slice(&h).unwrap(); - self.next_source_height = Some(height); - Ok(height) - } + Some(h) => Ok(BlockHeight::try_from_slice(&h).unwrap()), None => Ok(self.target_genesis_height), } } - async fn send_transactions( - &mut self, - block: MappedBlock, - ) -> anyhow::Result)>> { - let mut sent = vec![]; - for chunk in block.chunks { - let mut txs = vec![]; - for tx in chunk.txs { + async fn send_transactions(&mut self, block: &mut MappedBlock) -> anyhow::Result<()> { + for chunk in block.chunks.iter_mut() { + for tx in chunk.txs.iter_mut() { match tx { TargetChainTx::Ready(tx) => { match self @@ -503,15 +680,15 @@ impl TxMirror { { ProcessTxResponse::RequestRouted => { crate::metrics::TRANSACTIONS_SENT.with_label_values(&["ok"]).inc(); - txs.push(tx); + tx.sent_successfully = true; } ProcessTxResponse::InvalidTx(e) => { // TODO: here if we're getting an error because the tx was already included, it is possible // that some other instance of this code ran and made progress already. For now we can assume // only once instance of this code will run, but this is the place to detect if that's not the case. tracing::error!( - target: "mirror", "Tried to send an invalid tx from source #{} shard {}: {:?}", - block.source_height, chunk.shard_id, e + target: "mirror", "Tried to send an invalid tx for ({}, {:?}) from source #{} shard {} tx {}: {:?}", + &tx.target_tx.transaction.signer_id, &tx.target_tx.transaction.public_key, block.source_height, chunk.shard_id, tx.source_tx_index, e ); crate::metrics::TRANSACTIONS_SENT .with_label_values(&["invalid"]) @@ -530,198 +707,23 @@ impl TxMirror { } TargetChainTx::AwaitingNonce(tx) => { // TODO: here we should just save this transaction for later and send it when it's known - tracing::warn!(target: "mirror", "skipped sending transaction with signer {} because valid target chain nonce not known", &tx.source_signer_id) + tracing::warn!( + target: "mirror", "skipped sending transaction for ({}, {:?}) because valid target chain nonce not known", + &tx.target_tx.signer_id, &tx.target_tx.public_key + ); } } } - sent.push((chunk.shard_id, txs)); - } - Ok(sent) - } - - fn read_nonce_diff( - &self, - account_id: &AccountId, - public_key: &PublicKey, - ) -> anyhow::Result> { - let db_key = nonce_col_key(account_id, public_key); - // TODO: cache this? - Ok(self - .db - .get_cf(self.db.cf_handle(DBCol::Nonces.name()).unwrap(), &db_key)? - .map(|v| NonceDiff::try_from_slice(&v).unwrap())) - } - - fn put_nonce_diff( - &self, - account_id: &AccountId, - public_key: &PublicKey, - diff: &NonceDiff, - ) -> anyhow::Result<()> { - tracing::debug!(target: "mirror", "storing {:?} in DB for ({:?}, {:?})", &diff, account_id, public_key); - let db_key = nonce_col_key(account_id, public_key); - self.db.put_cf( - self.db.cf_handle(DBCol::Nonces.name()).unwrap(), - &db_key, - &diff.try_to_vec().unwrap(), - )?; - Ok(()) - } - - // If the access key was present in the genesis records, just - // return the same nonce. Otherwise, we need to change the - // nonce. So check if we already know what the difference in - // nonces is, and if not, try to fetch that info and store it. - // `source_signer_id` and `target_signer_id` are the same unless - // it's an implicit account - async fn map_nonce( - &self, - source_signer_id: &AccountId, - target_signer_id: &AccountId, - source_public: &PublicKey, - target_public: &PublicKey, - nonce: Nonce, - ) -> anyhow::Result> { - let mut diff = match self.read_nonce_diff(source_signer_id, source_public)? { - Some(m) => m, - // If it's not stored in the database, it's an access key that was present in the genesis - // records, so we don't need to do anything to the nonce. - None => return Ok(Ok(nonce)), - }; - if diff.known() { - return Ok(diff.map(nonce)); - } - - self.update_nonces( - source_signer_id, - target_signer_id, - source_public, - target_public, - &mut diff, - ) - .await?; - Ok(diff.map(nonce)) - } - - async fn update_nonces( - &self, - source_signer_id: &AccountId, - target_signer_id: &AccountId, - source_public: &PublicKey, - target_public: &PublicKey, - diff: &mut NonceDiff, - ) -> anyhow::Result<()> { - let mut rewrite = false; - if diff.source_start.is_none() { - self.update_source_nonce(source_signer_id, source_public, diff).await?; - rewrite |= diff.source_start.is_some(); - } - if diff.target_start.is_none() { - diff.target_start = fetch_access_key_nonce( - &self.target_view_client, - target_signer_id, - target_public, - None, - ) - .await?; - rewrite |= diff.target_start.is_some(); - } - - if rewrite { - self.put_nonce_diff(source_signer_id, source_public, diff)?; } Ok(()) } - async fn update_source_nonce( - &self, - account_id: &AccountId, - public_key: &PublicKey, - diff: &mut NonceDiff, - ) -> anyhow::Result<()> { - let mut block_height = 0; - let mut block_hash = CryptoHash::default(); - let mut failed_txs = Vec::new(); - - // first find the earliest block hash where the access key should exist - for tx in diff.pending_source_txs.iter() { - match fetch_tx_outcome( - &self.source_view_client, - tx.tx_hash.clone(), - &tx.signer_id, - &tx.receiver_id, - ) - .await? - { - TxOutcome::Success(hash) => { - let height = - block_hash_to_height(&self.source_view_client, &hash).await.with_context( - || format!("failed fetching block height of block {}", &hash), - )?; - if &block_hash == &CryptoHash::default() || block_height > height { - block_height = height; - block_hash = hash; - } - } - TxOutcome::Failure => { - failed_txs.push(tx.clone()); - } - TxOutcome::Pending => {} - } - } - if &block_hash == &CryptoHash::default() { - // no need to do this if block_hash is set because set_source() below will clear it - for tx in failed_txs.iter() { - diff.pending_source_txs.remove(tx); - } - return Ok(()); - } - let nonce = fetch_access_key_nonce( - &self.source_view_client, - account_id, - public_key, - Some(&block_hash), - ) - .await? - .ok_or_else(|| { - anyhow::anyhow!( - "expected access key to exist for {}, {} after finding successful receipt in {}", - &account_id, - &public_key, - &block_hash - ) - })?; - diff.set_source(nonce); - Ok(()) - } - - // we have a situation where nonces need to be mapped (AddKey actions - // or implicit account transfers). So store the initial nonce data in the DB. - async fn store_source_nonce( - &self, - tx: &SignedTransactionView, - public_key: &PublicKey, - ) -> anyhow::Result<()> { - // TODO: probably better to use a merge operator here. Not urgent, though. - let mut diff = self.read_nonce_diff(&tx.receiver_id, &public_key)?.unwrap_or_default(); - if diff.source_start.is_some() { - return Ok(()); - } - diff.pending_source_txs.insert(TxIds { - tx_hash: tx.hash.clone(), - signer_id: tx.signer_id.clone(), - receiver_id: tx.receiver_id.clone(), - }); - self.update_source_nonce(&tx.receiver_id, &public_key, &mut diff).await?; - self.put_nonce_diff(&tx.receiver_id, &public_key, &diff) - } - async fn map_actions( &self, tx: &SignedTransactionView, - prev_block: &CryptoHash, - ) -> anyhow::Result> { + ) -> anyhow::Result<(Vec, HashSet<(AccountId, PublicKey)>)> { let mut actions = Vec::new(); + let mut nonce_updates = HashSet::new(); for a in tx.actions.iter() { // this try_from() won't fail since the ActionView was constructed from the Action @@ -729,13 +731,15 @@ impl TxMirror { match &action { Action::AddKey(add_key) => { - self.store_source_nonce(tx, &add_key.public_key).await?; - - let replacement = - crate::key_mapping::map_key(&add_key.public_key, self.secret.as_ref()); + let public_key = + crate::key_mapping::map_key(&add_key.public_key, self.secret.as_ref()) + .public_key(); + let receiver_id = + 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 { - public_key: replacement.public_key(), + public_key, access_key: add_key.access_key.clone(), })); } @@ -747,15 +751,19 @@ impl TxMirror { actions.push(Action::DeleteKey(DeleteKeyAction { public_key })); } Action::Transfer(_) => { - if tx.receiver_id.is_implicit() - && !account_exists(&self.source_view_client, &tx.receiver_id, prev_block) + if tx.receiver_id.is_implicit() && tx.actions.len() == 1 { + let target_account = + crate::key_mapping::map_account(&tx.receiver_id, self.secret.as_ref()); + if !account_exists(&self.target_view_client, &target_account) .await .with_context(|| { format!("failed checking existence for account {}", &tx.receiver_id) })? - { - let public_key = crate::key_mapping::implicit_account_key(&tx.receiver_id); - self.store_source_nonce(tx, &public_key).await?; + { + let public_key = + crate::key_mapping::implicit_account_key(&target_account); + nonce_updates.insert((target_account, public_key)); + } } actions.push(action); } @@ -764,7 +772,7 @@ impl TxMirror { _ => actions.push(action), }; } - Ok(actions) + Ok((actions, nonce_updates)) } // fetch the source chain block at `source_height`, and prepare a @@ -774,20 +782,8 @@ impl TxMirror { &self, source_height: BlockHeight, ref_hash: CryptoHash, + tracker: &mut crate::chain_tracker::TxTracker, ) -> anyhow::Result> { - let prev_hash = match self - .source_view_client - .send( - GetBlock(BlockReference::BlockId(BlockId::Height(source_height))) - .with_span_context(), - ) - .await - .unwrap() - { - Ok(b) => b.header.prev_hash, - Err(GetBlockError::UnknownBlock { .. }) => return Ok(None), - Err(e) => return Err(e.into()), - }; let mut chunks = Vec::new(); for shard_id in self.tracked_shards.iter() { let mut txs = Vec::new(); @@ -815,8 +811,9 @@ impl TxMirror { } let mut num_not_ready = 0; + for (idx, source_tx) in chunk.transactions.into_iter().enumerate() { - let actions = self.map_actions(&source_tx, &prev_hash).await?; + let (actions, tx_nonce_updates) = self.map_actions(&source_tx).await?; if actions.is_empty() { // If this is a tx containing only stake actions, skip it. continue; @@ -827,63 +824,68 @@ impl TxMirror { let target_signer_id = crate::key_mapping::map_account(&source_tx.signer_id, self.secret.as_ref()); - match self - .map_nonce( - &source_tx.signer_id, + let target_receiver_id = + crate::key_mapping::map_account(&source_tx.receiver_id, self.secret.as_ref()); + + let target_nonce = tracker + .next_nonce( + &self.target_view_client, + &self.db, &target_signer_id, - &source_tx.public_key, &public_key, - source_tx.nonce, + source_height, ) - .await? - { - Ok(nonce) => { - let mut target_tx = Transaction::new( - target_signer_id, - public_key, - crate::key_mapping::map_account( - &source_tx.receiver_id, - self.secret.as_ref(), - ), - nonce, - ref_hash.clone(), - ); - target_tx.actions = actions; - let target_tx = SignedTransaction::new( - mapped_key.sign(&target_tx.get_hash_and_size().0.as_ref()), - target_tx, - ); - txs.push(TargetChainTx::Ready(MappedTx::new(&source_tx, idx, target_tx))); - } - Err(e) => match e { - MapNonceError::AddOverflow(..) - | MapNonceError::SubOverflow(..) - | MapNonceError::SourceKeyNotOnChain => { - tracing::error!(target: "mirror", "error mapping nonce for ({:?}, {:?}): {:?}", &source_tx.signer_id, &public_key, e); - continue; - } - MapNonceError::TargetKeyNotOnChain => { - let mut target_tx = Transaction::new( - crate::key_mapping::map_account( - &source_tx.signer_id, - self.secret.as_ref(), - ), + .await?; + if target_nonce.pending_outcomes.is_empty() { + match target_nonce.nonce { + Some(nonce) => { + let target_tx = TargetChainTx::new_ready( + &source_tx, + idx, + actions, + nonce, + &ref_hash, + target_signer_id, + target_receiver_id, + &mapped_key, public_key, - crate::key_mapping::map_account( - &source_tx.receiver_id, - self.secret.as_ref(), - ), - source_tx.nonce, - ref_hash.clone(), + tx_nonce_updates, ); - target_tx.actions = actions; - txs.push(TargetChainTx::AwaitingNonce(TxAwaitingNonce::new( - &source_tx, idx, target_tx, mapped_key, - ))); + txs.push(target_tx); + } + None => { num_not_ready += 1; + let target_tx = TargetChainTx::new_awaiting_nonce( + &source_tx, + idx, + actions, + target_nonce, + &ref_hash, + target_signer_id, + target_receiver_id, + mapped_key, + public_key, + tx_nonce_updates, + ); + txs.push(target_tx); } - }, - }; + } + } else { + num_not_ready += 1; + let target_tx = TargetChainTx::new_awaiting_nonce( + &source_tx, + idx, + actions, + target_nonce, + &ref_hash, + target_signer_id, + target_receiver_id, + mapped_key, + public_key, + tx_nonce_updates, + ); + txs.push(target_tx); + } } if num_not_ready == 0 { tracing::debug!( @@ -892,7 +894,7 @@ impl TxMirror { ); } else { tracing::debug!( - target: "mirror", "prepared {} transacations for source chain #{} shard {} {} of which are \ + target: "mirror", "prepared {} transacations for source chain #{} shard {} with {} \ still waiting for the corresponding access keys to make it on chain", txs.len(), source_height, shard_id, num_not_ready, ); @@ -924,11 +926,11 @@ impl TxMirror { for height in start_height..=source_head { if let Some(b) = self - .fetch_txs(height, ref_hash) + .fetch_txs(height, ref_hash, tracker) .await .with_context(|| format!("Can't fetch source #{} transactions", height))? { - tracker.queue_block(b); + tracker.queue_block(b, &self.target_view_client, &self.db).await?; if tracker.num_blocks_queued() > 100 { return Ok(()); } @@ -944,77 +946,6 @@ impl TxMirror { Ok(()) } - fn set_next_source_height(&mut self, height: BlockHeight) -> anyhow::Result<()> { - self.next_source_height = Some(height); - // TODO: we should instead save something like the - // (block_height, shard_id, idx_in_chunk) of the last - // transaction sent. Currently we set next_source_height after - // sending all of the transactions in that chunk, so if we get - // SIGTERM or something in the middle of sending a batch of - // txs, we'll send some that we already sent next time we - // start. Not a giant problem but kind of unclean. - self.db.put_cf( - self.db.cf_handle(DBCol::Misc.name()).unwrap(), - "next_source_height", - height.try_to_vec().unwrap(), - )?; - Ok(()) - } - - // Go through any upcoming batches of transactions that we haven't - // been able to set a valid nonce for yet, and see if we can now - // do that. - async fn set_nonces( - &self, - tracker: &mut crate::chain_tracker::TxTracker, - ) -> anyhow::Result<()> { - let next_batch_time = tracker.next_batch_time(); - let mut txs_ready = Vec::new(); - let mut keys_mapped = HashSet::new(); - - for (source_signer_id, source_public_key) in tracker.pending_access_keys_iter() { - let mut diff = self.read_nonce_diff(source_signer_id, source_public_key)?.unwrap(); - let target_signer_id = - crate::key_mapping::map_account(source_signer_id, self.secret.as_ref()); - let target_public_key = - crate::key_mapping::map_key(source_public_key, self.secret.as_ref()).public_key(); - self.update_nonces( - &source_signer_id, - &target_signer_id, - &source_public_key, - &target_public_key, - &mut diff, - ) - .await?; - if diff.known() { - keys_mapped.insert((source_signer_id.clone(), source_public_key.clone())); - } - } - for (tx_ref, tx) in tracker.tx_awaiting_nonce_iter() { - if keys_mapped.contains(&(tx.source_signer_id.clone(), tx.source_public.clone())) { - let nonce = self - .map_nonce( - &tx.source_signer_id, - &tx.target_tx.signer_id, - &tx.source_public, - &tx.target_tx.public_key, - tx.target_tx.nonce, - ) - .await? - .unwrap(); - txs_ready.push((tx_ref.clone(), nonce)); - } - - if Instant::now() > next_batch_time - Duration::from_millis(20) { - break; - } - } - for (tx_ref, nonce) in txs_ready { - tracker.set_tx_nonce(&tx_ref, nonce); - } - Ok(()) - } - async fn main_loop( &mut self, mut tracker: crate::chain_tracker::TxTracker, @@ -1024,24 +955,20 @@ impl TxMirror { loop { tokio::select! { // time to send a batch of transactions - mapped_block = tracker.next_batch(), if tracker.num_blocks_queued() > 0 => { - let mapped_block = mapped_block.unwrap(); - let source_height = mapped_block.source_height; - let sent = self.send_transactions(mapped_block).await?; - tracker.on_txs_sent(&sent, source_height, target_height); + mapped_block = tracker.next_batch(&self.target_view_client, &self.db), if tracker.num_blocks_queued() > 0 => { + self.send_transactions(mapped_block?).await?; + tracker.on_txs_sent(&self.target_view_client, &self.db, target_height).await?; // now we have one second left until we need to send more transactions. In the // meantime, we might as well prepare some more batches of transactions. // TODO: continue in best effort fashion on error - self.set_next_source_height(source_height+1)?; self.queue_txs(&mut tracker, target_head, true).await?; } msg = self.target_stream.recv() => { let msg = msg.unwrap(); - tracker.on_target_block(&msg); - self.set_nonces(&mut tracker).await?; target_head = msg.block.header.hash; target_height = msg.block.header.height; + tracker.on_target_block(&self.target_view_client, &self.db, msg).await?; } // If we don't have any upcoming sets of transactions to send already built, we probably fell behind in the source // chain and can't fetch the transactions. Check if we have them now here. From 37232dbc9af61cf60d1ed0b7248ddd8f683d5110 Mon Sep 17 00:00:00 2001 From: posvyatokum Date: Wed, 23 Nov 2022 17:22:46 +0000 Subject: [PATCH 028/188] [store] add update_cold_head (#8059) Adding update_cold_head function and integration test for it Changing cold_db argument from dyn Database to ColdDB in cold storage functions Making NodeStorage's ColdDB generic for testing purposes. Thus adding Phantom field for the case when cold_store feature is not provided Adding NodeStorage::new_with_cold for testing purposes Adding NodeStorage::cold_db to access cold_storage as ColdDB --- core/store/src/cold_storage.rs | 66 +++++++++++++------ core/store/src/lib.rs | 58 +++++++++++----- core/store/src/test_utils.rs | 6 ++ .../src/tests/client/cold_storage.rs | 59 ++++++++++++++--- 4 files changed, 140 insertions(+), 49 deletions(-) diff --git a/core/store/src/cold_storage.rs b/core/store/src/cold_storage.rs index 2526c9c015d..82e3e68b748 100644 --- a/core/store/src/cold_storage.rs +++ b/core/store/src/cold_storage.rs @@ -1,10 +1,10 @@ use crate::columns::DBKeyType; -use crate::refcount::add_positive_refcount; +use crate::db::ColdDB; use crate::trie::TrieRefcountChange; -use crate::{DBCol, DBTransaction, Database, Store, TrieChanges}; +use crate::{DBCol, DBTransaction, Database, Store, TrieChanges, HEAD_KEY}; -use borsh::BorshDeserialize; -use near_primitives::block::Block; +use borsh::{BorshDeserialize, BorshSerialize}; +use near_primitives::block::{Block, BlockHeader, Tip}; use near_primitives::hash::CryptoHash; use near_primitives::shard_layout::ShardLayout; use near_primitives::sharding::ShardChunk; @@ -23,6 +23,7 @@ struct StoreWithCache<'a> { } /// Updates provided cold database from provided hot store with information about block at `height`. +/// Block as `height` has to be final. /// Wraps hot store in `StoreWithCache` for optimizing reads. /// /// First, we read from hot store information necessary @@ -40,8 +41,8 @@ struct StoreWithCache<'a> { /// 1. add it to `DBCol::is_cold` list /// 2. define `DBCol::key_type` for it (if it isn't already defined) /// 3. add new clause in `get_keys_from_store` for new key types used for this column (if there are any) -pub fn update_cold_db( - cold_db: &dyn Database, +pub fn update_cold_db( + cold_db: &ColdDB, hot_store: &Store, shard_layout: &ShardLayout, height: &BlockHeight, @@ -68,8 +69,8 @@ pub fn update_cold_db( /// Gets values for given keys in a column from provided hot_store. /// Creates a transaction based on that values with set DBOp s. /// Writes that transaction to cold_db. -fn copy_from_store( - cold_db: &dyn Database, +fn copy_from_store( + cold_db: &ColdDB, hot_store: &mut StoreWithCache, col: DBCol, keys: Vec, @@ -84,30 +85,53 @@ fn copy_from_store( // added. let data = hot_store.get(col, &key)?; if let Some(value) = data { - // Database checks col.is_rc() on read and write - // And in every way expects rc columns to be written with rc - // // TODO: As an optimisation, we might consider breaking the // abstraction layer. Since we’re always writing to cold database, // rather than using `cold_db: &dyn Database` argument we cloud have // `cold_db: &ColdDB` and then some custom function which lets us // write raw bytes. - if col.is_rc() { - transaction.update_refcount( - col, - key, - add_positive_refcount(&value, std::num::NonZeroU32::new(1).unwrap()), - ); - } else { - transaction.set(col, key, value); - } + transaction.set(col, key, value); } } cold_db.write(transaction)?; return Ok(()); } -pub fn test_cold_genesis_update(cold_db: &dyn Database, hot_store: &Store) -> io::Result<()> { +/// This function sets HEAD key in BlockMisc column to the Tip that reflect provided height. +/// This function should be used after all of the blocks from genesis to `height` inclusive had been copied. +/// +/// This method relies on the fact that BlockHeight and BlockHeader are not garbage collectable. +/// (to construct the Tip we query hot_store for block hash and block header) +/// If this is to change, caller should be careful about `height` not being garbage collected in hot storage yet. +pub fn update_cold_head( + cold_db: &ColdDB, + hot_store: &Store, + height: &BlockHeight, +) -> io::Result<()> { + tracing::debug!(target: "store", "update HEAD of cold db to {}", height); + + let mut store = StoreWithCache { store: hot_store, cache: StoreCache::new() }; + + let height_key = height.to_le_bytes(); + let block_hash_key = store.get_or_err(DBCol::BlockHeight, &height_key)?.as_slice().to_vec(); + + let mut transaction = DBTransaction::new(); + transaction.set( + DBCol::BlockMisc, + HEAD_KEY.to_vec(), + Tip::from_header( + &store.get_ser_or_err::(DBCol::BlockHeader, &block_hash_key)?, + ) + .try_to_vec()?, + ); + cold_db.write(transaction)?; + return Ok(()); +} + +pub fn test_cold_genesis_update( + cold_db: &ColdDB, + hot_store: &Store, +) -> io::Result<()> { let mut store_with_cache = StoreWithCache { store: hot_store, cache: StoreCache::new() }; for col in DBCol::iter() { if col.is_cold() { diff --git a/core/store/src/lib.rs b/core/store/src/lib.rs index 7b93e22c0ae..e90cdc46c42 100644 --- a/core/store/src/lib.rs +++ b/core/store/src/lib.rs @@ -1,5 +1,6 @@ use std::fs::File; use std::io::{BufReader, BufWriter, Read, Write}; +use std::marker::PhantomData; use std::path::Path; use std::sync::Arc; use std::{fmt, io}; @@ -73,12 +74,13 @@ pub enum Temperature { /// will provide interface to access hot and cold storage. This is in contrast /// to [`Store`] which will abstract access to only one of the temperatures of /// the storage. -pub struct NodeStorage { +pub struct NodeStorage { hot_storage: Arc, #[cfg(feature = "cold_store")] - cold_storage: Option>, + cold_storage: Option>>, #[cfg(not(feature = "cold_store"))] cold_storage: Option, + _phantom: PhantomData, } /// Node’s single storage source. @@ -117,6 +119,21 @@ impl NodeStorage { ) } + /// Constructs new object backed by given database. + fn from_rocksdb( + hot_storage: crate::db::RocksDB, + #[cfg(feature = "cold_store")] cold_storage: Option, + #[cfg(not(feature = "cold_store"))] cold_storage: Option, + ) -> Self { + let hot_storage = Arc::new(hot_storage); + #[cfg(feature = "cold_store")] + let cold_storage = cold_storage + .map(|cold_db| Arc::new(crate::db::ColdDB::new(hot_storage.clone(), cold_db))); + #[cfg(not(feature = "cold_store"))] + let cold_storage = cold_storage.map(|_| unreachable!()); + Self { hot_storage, cold_storage, _phantom: PhantomData {} } + } + /// Initialises an opener for a new temporary test store. /// /// As per the name, this is meant for tests only. The created store will @@ -147,24 +164,11 @@ impl NodeStorage { /// possibly [`crate::test_utils::create_test_store`] (depending whether you /// need [`NodeStorage`] or [`Store`] object. pub fn new(storage: Arc) -> Self { - Self { hot_storage: storage, cold_storage: None } - } - - /// Constructs new object backed by given database. - fn from_rocksdb( - hot_storage: crate::db::RocksDB, - #[cfg(feature = "cold_store")] cold_storage: Option, - #[cfg(not(feature = "cold_store"))] cold_storage: Option, - ) -> Self { - let hot_storage = Arc::new(hot_storage); - #[cfg(feature = "cold_store")] - let cold_storage = cold_storage - .map(|cold_db| Arc::new(crate::db::ColdDB::new(hot_storage.clone(), cold_db))); - #[cfg(not(feature = "cold_store"))] - let cold_storage = cold_storage.map(|_| unreachable!()); - Self { hot_storage, cold_storage } + Self { hot_storage: storage, cold_storage: None, _phantom: PhantomData {} } } +} +impl NodeStorage { /// Returns storage for given temperature. /// /// Some data live only in hot and some only in cold storage (which is at @@ -219,7 +223,9 @@ impl NodeStorage { Temperature::Cold => self.cold_storage.unwrap(), } } +} +impl NodeStorage { /// Returns whether the storage has a cold database. pub fn has_cold(&self) -> bool { self.cold_storage.is_some() @@ -237,6 +243,22 @@ impl NodeStorage { metadata::DbKind::Hot | metadata::DbKind::Cold => unreachable!(), }) } + + #[cfg(feature = "cold_store")] + pub fn new_with_cold(hot: Arc, cold: D) -> Self { + Self { + hot_storage: hot.clone(), + cold_storage: Some(Arc::new(crate::db::ColdDB::::new(hot, cold))), + _phantom: PhantomData:: {}, + } + } + + #[cfg(feature = "cold_store")] + pub fn cold_db(&self) -> io::Result<&Arc>> { + self.cold_storage + .as_ref() + .map_or(Err(io::Error::new(io::ErrorKind::NotFound, "ColdDB Not Found")), |c| Ok(c)) + } } impl Store { diff --git a/core/store/src/test_utils.rs b/core/store/src/test_utils.rs index 9246594f3c9..cc4ae321dc3 100644 --- a/core/store/src/test_utils.rs +++ b/core/store/src/test_utils.rs @@ -19,6 +19,12 @@ pub fn create_test_node_storage() -> NodeStorage { NodeStorage::new(TestDB::new()) } +/// Creates an in-memory node storage with ColdDB +#[cfg(feature = "cold_store")] +pub fn create_test_node_storage_with_cold() -> NodeStorage { + NodeStorage::new_with_cold(TestDB::new(), TestDB::default()) +} + /// Creates an in-memory database. pub fn create_test_store() -> Store { create_test_node_storage().get_store(crate::Temperature::Hot) diff --git a/integration-tests/src/tests/client/cold_storage.rs b/integration-tests/src/tests/client/cold_storage.rs index 75b561dd468..fbadf2f3704 100644 --- a/integration-tests/src/tests/client/cold_storage.rs +++ b/integration-tests/src/tests/client/cold_storage.rs @@ -5,13 +5,16 @@ use near_chain_configs::Genesis; use near_client::test_utils::TestEnv; use near_crypto::{InMemorySigner, KeyType}; use near_o11y::testonly::init_test_logger; +use near_primitives::block::Tip; use near_primitives::sharding::ShardChunk; use near_primitives::transaction::{ Action, DeployContractAction, FunctionCallAction, SignedTransaction, }; -use near_store::cold_storage::{test_cold_genesis_update, test_get_store_reads, update_cold_db}; -use near_store::db::TestDB; -use near_store::{DBCol, NodeStorage, Store, Temperature}; +use near_store::cold_storage::{ + test_cold_genesis_update, test_get_store_reads, update_cold_db, update_cold_head, +}; +use near_store::test_utils::create_test_node_storage_with_cold; +use near_store::{DBCol, Store, Temperature, HEAD_KEY}; use nearcore::config::GenesisExt; use strum::IntoEnumIterator; @@ -56,8 +59,6 @@ fn check_iter( fn test_storage_after_commit_of_cold_update() { init_test_logger(); - let cold_db = TestDB::new(); - let epoch_length = 5; let max_height = epoch_length * 4; @@ -70,9 +71,13 @@ fn test_storage_after_commit_of_cold_update() { .runtime_adapters(create_nightshade_runtimes(&genesis, 1)) .build(); + // TODO construct cold_db with appropriate hot storage + let store = create_test_node_storage_with_cold(); + let mut last_hash = *env.clients[0].chain.genesis().hash(); - test_cold_genesis_update(&*cold_db, &env.clients[0].runtime_adapter.store()).unwrap(); + test_cold_genesis_update(&*store.cold_db().unwrap(), &env.clients[0].runtime_adapter.store()) + .unwrap(); let state_reads = test_get_store_reads(DBCol::State); let state_changes_reads = test_get_store_reads(DBCol::StateChanges); @@ -129,7 +134,7 @@ fn test_storage_after_commit_of_cold_update() { env.process_block(0, block.clone(), Provenance::PRODUCED); update_cold_db( - &*cold_db, + &*store.cold_db().unwrap(), &env.clients[0].runtime_adapter.store(), &env.clients[0] .runtime_adapter @@ -152,8 +157,6 @@ fn test_storage_after_commit_of_cold_update() { // assert that we don't read StateChanges from db again after iter_prefix assert_eq!(state_changes_reads, test_get_store_reads(DBCol::StateChanges)); - let cold_store = NodeStorage::new(cold_db).get_store(Temperature::Hot); - // We still need to filter out one chunk let mut no_check_rules: Vec) -> bool>> = vec![]; no_check_rules.push(Box::new(move |col, value| -> bool { @@ -170,7 +173,7 @@ fn test_storage_after_commit_of_cold_update() { if col.is_cold() { let num_checks = check_iter( &env.clients[0].runtime_adapter.store(), - &cold_store, + &store.get_store(Temperature::Cold), col, &no_check_rules, ); @@ -183,3 +186,39 @@ fn test_storage_after_commit_of_cold_update() { } } } + +/// Producing 10 * 5 blocks and updating HEAD of cold storage after each one. +/// After every update checking that HEAD of cold db and FINAL_HEAD of hot store are equal. +#[test] +fn test_cold_db_head_update() { + init_test_logger(); + + let epoch_length = 5; + let max_height = epoch_length * 10; + + 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) + .runtime_adapters(create_nightshade_runtimes(&genesis, 1)) + .build(); + + let store = create_test_node_storage_with_cold(); + + for h in 1..max_height { + env.produce_block(0, h); + update_cold_head(&*store.cold_db().unwrap(), &env.clients[0].runtime_adapter.store(), &h) + .unwrap(); + + assert_eq!( + &store.get_store(Temperature::Cold).get_ser::(DBCol::BlockMisc, HEAD_KEY).unwrap(), + &env.clients[0] + .runtime_adapter + .store() + .get_ser::(DBCol::BlockMisc, HEAD_KEY) + .unwrap() + ); + } +} From 045498b8f34fb555b157993b9572535556a06889 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Thu, 24 Nov 2022 13:15:49 +0000 Subject: [PATCH 029/188] refactor: cleaning up RuntimeConfig (#8100) refactor: cleaning up RuntimeConfig Sorry. This is a humongous change. But it's really just a lot of the same boilerplate code changes. The key changes are: - Action fees are now accessed through one single function `RuntimeFeesConfig::fee(&self, cost: ActionCosts)` instead of selecting a specific field deep down in the structure - Action fees are stored in an EnumMap instead of by hard-coded fields - The JSON RPC now has `RuntimeConfigView` to keep compatibility - Creation of `RuntimeConfig` no longer goes thorough serde JSON serialization + deserialization The goal of this PR is to make gas profiling based on parameters possible, which in turn makes parameter weights possible to implement much more easily. The key here is that we no longer have to lookup the gas value up independently from the profile key. Today: ```rust self.gas_counter.pay_action_base( &self.fees_config.action_creation_config.deploy_contract_cost, sir, ActionCosts::deploy_contract_base, )?; ``` After PR: ```rust self.pay_action_base(ActionCosts::deploy_contract_base, sir)?; ``` Also it gives us simplified fee lookup on `RuntimeFeesConfig`. Before, we need things like: ```rust let exec_gas = self.cfg.action_receipt_creation_config.exec_fee() + self.cfg.action_creation_config.add_key_cost.function_call_cost.exec_fee() + num_bytes * self .cfg .action_creation_config .add_key_cost .function_call_cost_per_byte .exec_fee(); ``` which becomes ```rust 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(); ``` The final state after all refactoring is done is described in #8033. This PR contains step 2 and 4 combined because I was not able to separate them due to unforeseen dependencies we have in the code. The hardest part should be done after the current PR and what follows should be small PRs that get rid of types one by one. # Test We have to make sure this is a pure refactoring change without side-effects. Luckily, we have `test_json_unchanged` that detects changes in any protocol version. Otherwise, we rely on tests that have gas profile snapshots to make sure the gas fee lookup translation did not have errors. --- Cargo.lock | 4 + chain/indexer/src/streamer/utils.rs | 4 +- chain/rosetta-rpc/src/adapters/mod.rs | 4 +- .../rosetta-rpc/src/adapters/transactions.rs | 6 +- chain/rosetta-rpc/src/utils.rs | 17 +- core/chain-configs/src/genesis_config.rs | 6 +- core/primitives-core/Cargo.toml | 1 + core/primitives-core/src/config.rs | 22 +- core/primitives-core/src/parameter.rs | 26 ++ core/primitives-core/src/runtime/fees.rs | 199 +++++++-------- core/primitives/Cargo.toml | 1 + core/primitives/src/runtime/config.rs | 41 ++- core/primitives/src/runtime/config_store.rs | 37 ++- core/primitives/src/runtime/mod.rs | 2 +- .../primitives/src/runtime/parameter_table.rs | 143 +++++++---- core/primitives/src/views.rs | 239 +++++++++++++++++- integration-tests/src/node/runtime_node.rs | 3 +- .../src/tests/client/process_blocks.rs | 19 +- .../src/tests/nearcore/rpc_nodes.rs | 13 +- .../src/tests/runtime/deployment.rs | 2 +- .../src/tests/standard_cases/mod.rs | 9 +- .../src/tests/standard_cases/runtime.rs | 2 +- integration-tests/src/tests/test_errors.rs | 2 +- nearcore/tests/economics.rs | 2 +- runtime/near-vm-logic/src/gas_counter.rs | 43 ---- runtime/near-vm-logic/src/logic.rs | 137 ++++------ runtime/runtime-params-estimator/Cargo.toml | 1 + .../src/costs_to_runtime_config.rs | 49 ++-- .../src/function_call.rs | 2 +- .../src/gas_metering.rs | 2 +- runtime/runtime-params-estimator/src/main.rs | 4 +- runtime/runtime/Cargo.toml | 1 + runtime/runtime/src/actions.rs | 4 +- runtime/runtime/src/balance_checker.rs | 11 +- runtime/runtime/src/config.rs | 67 ++--- runtime/runtime/src/genesis.rs | 2 +- runtime/runtime/src/lib.rs | 51 ++-- runtime/runtime/src/verifier.rs | 14 +- .../runtime/tests/runtime_group_tools/mod.rs | 5 +- .../runtime_group_tools/random_config.rs | 32 +-- test-utils/testlib/src/fees_utils.rs | 143 +++++------ 41 files changed, 785 insertions(+), 587 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31bcee94268..b9aa23ad774 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3414,6 +3414,7 @@ dependencies = [ "chrono", "derive_more", "easy-ext", + "enum-map", "hex", "insta", "near-crypto", @@ -3443,6 +3444,7 @@ dependencies = [ "borsh", "bs58", "derive_more", + "enum-map", "near-account-id", "num-rational", "serde", @@ -3799,6 +3801,7 @@ dependencies = [ "assert_matches", "borsh", "byteorder", + "enum-map", "hex", "indicatif", "near-chain-configs", @@ -4920,6 +4923,7 @@ dependencies = [ "cfg-if 1.0.0", "chrono", "clap 3.1.18", + "enum-map", "genesis-populate", "hex", "indicatif", diff --git a/chain/indexer/src/streamer/utils.rs b/chain/indexer/src/streamer/utils.rs index 0cdf447cce2..85cdacd9a80 100644 --- a/chain/indexer/src/streamer/utils.rs +++ b/chain/indexer/src/streamer/utils.rs @@ -19,11 +19,13 @@ pub(crate) async fn convert_transactions_sir_into_local_receipts( let prev_block = fetch_block(&client, block.header.prev_hash).await?; let prev_block_gas_price = prev_block.header.gas_price; + let runtime_config = + node_runtime::config::RuntimeConfig::from(protocol_config.runtime_config.clone()); let local_receipts: Vec = txs.into_iter() .map(|tx| { let cost = tx_cost( - &protocol_config.runtime_config.transaction_costs, + &runtime_config.fees, &near_primitives::transaction::Transaction { signer_id: tx.transaction.signer_id.clone(), public_key: tx.transaction.public_key.clone(), diff --git a/chain/rosetta-rpc/src/adapters/mod.rs b/chain/rosetta-rpc/src/adapters/mod.rs index 098bb5893e0..e93dbec7fef 100644 --- a/chain/rosetta-rpc/src/adapters/mod.rs +++ b/chain/rosetta-rpc/src/adapters/mod.rs @@ -694,11 +694,13 @@ mod tests { use actix::System; use near_actix_test_utils::run_actix; use near_client::test_utils::setup_no_network; + use near_primitives::runtime::config::RuntimeConfig; + use near_primitives::views::RuntimeConfigView; #[test] fn test_convert_block_changes_to_transactions() { run_actix(async { - let runtime_config = near_primitives::runtime::config::RuntimeConfig::test(); + let runtime_config: RuntimeConfigView = RuntimeConfig::test().into(); let (_client, view_client) = setup_no_network( vec!["test".parse().unwrap()], "other".parse().unwrap(), diff --git a/chain/rosetta-rpc/src/adapters/transactions.rs b/chain/rosetta-rpc/src/adapters/transactions.rs index b219ad3a7cb..8281f1a897b 100644 --- a/chain/rosetta-rpc/src/adapters/transactions.rs +++ b/chain/rosetta-rpc/src/adapters/transactions.rs @@ -235,7 +235,7 @@ impl<'a> RosettaTransactions<'a> { /// Returns Rosetta transactions which map to given account changes. pub(crate) async fn convert_block_changes_to_transactions( view_client_addr: &Addr, - runtime_config: &near_primitives::runtime::config::RuntimeConfig, + runtime_config: &near_primitives::views::RuntimeConfigView, block_hash: &CryptoHash, accounts_changes: near_primitives::views::StateChangesView, mut accounts_previous_state: std::collections::HashMap< @@ -315,7 +315,7 @@ pub(crate) async fn convert_block_changes_to_transactions( } fn convert_account_update_to_operations( - runtime_config: &near_primitives::runtime::config::RuntimeConfig, + runtime_config: &near_primitives::views::RuntimeConfigView, operations: &mut Vec, account_id: &near_primitives::types::AccountId, previous_account_state: Option<&near_primitives::views::AccountView>, @@ -455,7 +455,7 @@ fn convert_account_update_to_operations( } fn convert_account_delete_to_operations( - runtime_config: &near_primitives::runtime::config::RuntimeConfig, + runtime_config: &near_primitives::views::RuntimeConfigView, operations: &mut Vec, account_id: &near_primitives::types::AccountId, previous_account_state: Option, diff --git a/chain/rosetta-rpc/src/utils.rs b/chain/rosetta-rpc/src/utils.rs index 4fc094b2af8..2ebffe84d2e 100644 --- a/chain/rosetta-rpc/src/utils.rs +++ b/chain/rosetta-rpc/src/utils.rs @@ -269,14 +269,14 @@ where } } +/// Tokens not locked due to staking (=liquid) but reserved for state. fn get_liquid_balance_for_storage( - mut account: near_primitives::account::Account, - runtime_config: &near_primitives::runtime::config::RuntimeConfig, + account: &near_primitives::account::Account, + storage_amount_per_byte: near_primitives::types::Balance, ) -> near_primitives::types::Balance { - account.set_amount(0); - near_primitives::runtime::get_insufficient_storage_stake(&account, runtime_config) - .expect("get_insufficient_storage_stake never fails when state is consistent") - .unwrap_or(0) + let required_amount = + near_primitives::types::Balance::from(account.storage_usage()) * storage_amount_per_byte; + required_amount.saturating_sub(account.locked()) } pub(crate) struct RosettaAccountBalances { @@ -292,12 +292,13 @@ impl RosettaAccountBalances { pub fn from_account>( account: T, - runtime_config: &near_primitives::runtime::config::RuntimeConfig, + runtime_config: &near_primitives::views::RuntimeConfigView, ) -> Self { let account = account.into(); let amount = account.amount(); let locked = account.locked(); - let liquid_for_storage = get_liquid_balance_for_storage(account, runtime_config); + let liquid_for_storage = + get_liquid_balance_for_storage(&account, runtime_config.storage_amount_per_byte); Self { liquid_for_storage, liquid: amount.saturating_sub(liquid_for_storage), locked } } diff --git a/core/chain-configs/src/genesis_config.rs b/core/chain-configs/src/genesis_config.rs index 3d1f9f8ad72..ad1d958f557 100644 --- a/core/chain-configs/src/genesis_config.rs +++ b/core/chain-configs/src/genesis_config.rs @@ -11,6 +11,7 @@ use std::{fmt, io}; use anyhow::Context; use chrono::{DateTime, Utc}; +use near_primitives::views::RuntimeConfigView; use num_rational::Rational32; use serde::de::{self, DeserializeSeed, IgnoredAny, MapAccess, SeqAccess, Visitor}; use serde::{Deserialize, Deserializer, Serialize}; @@ -594,6 +595,7 @@ impl GenesisChangeConfig { // Note: this type cannot be placed in primitives/src/view.rs because of `RuntimeConfig` dependency issues. // Ideally we should create `RuntimeConfigView`, but given the deeply nested nature and the number of fields inside // `RuntimeConfig`, it should be its own endeavor. +// TODO: This has changed, there is now `RuntimeConfigView`. Reconsider if moving this is possible now. #[derive(Serialize, Deserialize, Debug)] pub struct ProtocolConfigView { /// Current Protocol Version @@ -636,7 +638,7 @@ pub struct ProtocolConfigView { /// Gas price adjustment rate pub gas_price_adjustment_rate: Rational32, /// Runtime configuration (mostly economics constants). - pub runtime_config: RuntimeConfig, + pub runtime_config: RuntimeConfigView, /// Number of blocks for which a given transaction is valid pub transaction_validity_period: NumBlocks, /// Protocol treasury rate @@ -683,7 +685,7 @@ impl From for ProtocolConfigView { online_min_threshold: genesis_config.online_min_threshold, online_max_threshold: genesis_config.online_max_threshold, gas_price_adjustment_rate: genesis_config.gas_price_adjustment_rate, - runtime_config, + runtime_config: RuntimeConfigView::from(runtime_config), transaction_validity_period: genesis_config.transaction_validity_period, protocol_reward_rate: genesis_config.protocol_reward_rate, max_inflation_rate: genesis_config.max_inflation_rate, diff --git a/core/primitives-core/Cargo.toml b/core/primitives-core/Cargo.toml index db6e06e60f6..bc013f52dd1 100644 --- a/core/primitives-core/Cargo.toml +++ b/core/primitives-core/Cargo.toml @@ -18,6 +18,7 @@ base64.workspace = true borsh.workspace = true bs58.workspace = true derive_more.workspace = true +enum-map.workspace = true num-rational.workspace = true serde.workspace = true serde_repr.workspace = true diff --git a/core/primitives-core/src/config.rs b/core/primitives-core/src/config.rs index bf23b15c2c9..b8f41f858f7 100644 --- a/core/primitives-core/src/config.rs +++ b/core/primitives-core/src/config.rs @@ -5,6 +5,13 @@ use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use strum::{Display, EnumCount}; +/// 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, Serialize, Deserialize, PartialEq, Eq)] pub struct VMConfig { /// Costs for runtime externals @@ -128,7 +135,7 @@ pub enum StackLimiterVersion { } impl StackLimiterVersion { - fn v0() -> StackLimiterVersion { + pub fn v0() -> StackLimiterVersion { StackLimiterVersion::V0 } } @@ -650,7 +657,18 @@ pub enum ExtCosts { // Type of an action, used in fees logic. #[derive( - Copy, Clone, Hash, PartialEq, Eq, Debug, PartialOrd, Ord, EnumCount, Display, strum::EnumIter, + Copy, + Clone, + Hash, + PartialEq, + Eq, + Debug, + PartialOrd, + Ord, + EnumCount, + Display, + strum::EnumIter, + enum_map::Enum, )] #[allow(non_camel_case_types)] pub enum ActionCosts { diff --git a/core/primitives-core/src/parameter.rs b/core/primitives-core/src/parameter.rs index 4c3096cab25..872f3575fec 100644 --- a/core/primitives-core/src/parameter.rs +++ b/core/primitives-core/src/parameter.rs @@ -1,5 +1,7 @@ use std::slice; +use crate::config::ActionCosts; + /// Protocol configuration parameter which may change between protocol versions. #[derive( Clone, @@ -313,3 +315,27 @@ impl Parameter { .iter() } } + +// TODO: consider renaming parameters to "action_{ActionCosts}" and deleting +// `FeeParameter` all together. +impl From for FeeParameter { + fn from(other: ActionCosts) -> Self { + match other { + ActionCosts::create_account => Self::ActionCreateAccount, + ActionCosts::delete_account => Self::ActionDeleteAccount, + ActionCosts::deploy_contract_base => Self::ActionDeployContract, + ActionCosts::deploy_contract_byte => Self::ActionDeployContractPerByte, + ActionCosts::function_call_base => Self::ActionFunctionCall, + ActionCosts::function_call_byte => Self::ActionFunctionCallPerByte, + ActionCosts::transfer => Self::ActionTransfer, + ActionCosts::stake => Self::ActionStake, + ActionCosts::add_full_access_key => Self::ActionAddFullAccessKey, + ActionCosts::add_function_call_key_base => Self::ActionAddFunctionCallKey, + ActionCosts::add_function_call_key_byte => Self::ActionAddFunctionCallKeyPerByte, + ActionCosts::delete_key => Self::ActionDeleteKey, + ActionCosts::new_action_receipt => Self::ActionReceiptCreation, + ActionCosts::new_data_receipt_base => Self::DataReceiptCreationBase, + ActionCosts::new_data_receipt_byte => Self::DataReceiptCreationPerByte, + } + } +} diff --git a/core/primitives-core/src/runtime/fees.rs b/core/primitives-core/src/runtime/fees.rs index f02b4fed20d..29acd6b3182 100644 --- a/core/primitives-core/src/runtime/fees.rs +++ b/core/primitives-core/src/runtime/fees.rs @@ -3,10 +3,12 @@ //! * sir -- sender is receiver. Receipts that are directed by an account to itself are guaranteed //! to not be cross-shard which is cheaper than cross-shard. Conversely, when sender is not a //! receiver it might or might not be a cross-shard communication. +use enum_map::EnumMap; use serde::{Deserialize, Serialize}; +use crate::config::ActionCosts; use crate::num_rational::Rational; -use crate::types::Gas; +use crate::types::{Balance, Gas}; /// Costs associated with an object that can only be sent over the network (and executed /// by the receiver). @@ -38,23 +40,16 @@ impl Fee { } /// The minimum fee to send and execute. - fn min_send_and_exec_fee(&self) -> Gas { + pub fn min_send_and_exec_fee(&self) -> Gas { std::cmp::min(self.send_sir, self.send_not_sir) + self.execution } } -#[derive(Debug, Serialize, Deserialize, Clone, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct RuntimeFeesConfig { - /// Describes the cost of creating an action receipt, `ActionReceipt`, excluding the actual cost - /// of actions. - /// - `send` cost is burned when a receipt is created using `promise_create` or - /// `promise_batch_create` - /// - `exec` cost is burned when the receipt is being executed. - pub action_receipt_creation_config: Fee, - /// Describes the cost of creating a data receipt, `DataReceipt`. - pub data_receipt_creation_config: DataReceiptCreationConfig, - /// Describes the cost of creating a certain action, `Action`. Includes all variants. - pub action_creation_config: ActionCreationConfig, + /// Gas fees for sending and executing actions. + pub action_fees: EnumMap, + /// Describes fees for storage. pub storage_usage_config: StorageUsageConfig, @@ -127,8 +122,11 @@ pub struct AccessKeyCreationConfig { } /// Describes cost of storage per block -#[derive(Debug, Serialize, Deserialize, Clone, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct StorageUsageConfig { + /// Amount of yN per byte required to have on the account. See + /// for details. + pub storage_amount_per_byte: Balance, /// Number of bytes for an account record, including rounding up for account id. pub num_bytes_account: u64, /// Additional number of bytes for a k/v record @@ -136,129 +134,102 @@ pub struct StorageUsageConfig { } impl RuntimeFeesConfig { + /// Access action fee by `ActionCosts`. + pub fn fee(&self, cost: ActionCosts) -> &Fee { + &self.action_fees[cost] + } + pub fn test() -> Self { - #[allow(clippy::unreadable_literal)] Self { - action_receipt_creation_config: Fee { - send_sir: 108059500000, - send_not_sir: 108059500000, - execution: 108059500000, - }, - data_receipt_creation_config: DataReceiptCreationConfig { - base_cost: Fee { - send_sir: 4697339419375, - send_not_sir: 4697339419375, - execution: 4697339419375, - }, - cost_per_byte: Fee { - send_sir: 59357464, - send_not_sir: 59357464, - execution: 59357464, - }, - }, - action_creation_config: ActionCreationConfig { - create_account_cost: Fee { + storage_usage_config: StorageUsageConfig::test(), + burnt_gas_reward: Rational::new(3, 10), + pessimistic_gas_price_inflation_ratio: Rational::new(103, 100), + action_fees: enum_map::enum_map! { + ActionCosts::create_account => Fee { send_sir: 99607375000, send_not_sir: 99607375000, execution: 99607375000, }, - deploy_contract_cost: Fee { + ActionCosts::delete_account => Fee { + send_sir: 147489000000, + send_not_sir: 147489000000, + execution: 147489000000, + }, + ActionCosts::deploy_contract_base => Fee { send_sir: 184765750000, send_not_sir: 184765750000, execution: 184765750000, }, - deploy_contract_cost_per_byte: Fee { + ActionCosts::deploy_contract_byte => Fee { send_sir: 6812999, send_not_sir: 6812999, execution: 6812999, }, - function_call_cost: Fee { + ActionCosts::function_call_base => Fee { send_sir: 2319861500000, send_not_sir: 2319861500000, execution: 2319861500000, }, - function_call_cost_per_byte: Fee { + ActionCosts::function_call_byte => Fee { send_sir: 2235934, send_not_sir: 2235934, execution: 2235934, }, - transfer_cost: Fee { + ActionCosts::transfer => Fee { send_sir: 115123062500, send_not_sir: 115123062500, execution: 115123062500, }, - stake_cost: Fee { + ActionCosts::stake => Fee { send_sir: 141715687500, send_not_sir: 141715687500, execution: 102217625000, }, - add_key_cost: AccessKeyCreationConfig { - full_access_cost: Fee { - send_sir: 101765125000, - send_not_sir: 101765125000, - execution: 101765125000, - }, - function_call_cost: Fee { - send_sir: 102217625000, - send_not_sir: 102217625000, - execution: 102217625000, - }, - function_call_cost_per_byte: Fee { - send_sir: 1925331, - send_not_sir: 1925331, - execution: 1925331, - }, + ActionCosts::add_full_access_key => Fee { + send_sir: 101765125000, + send_not_sir: 101765125000, + execution: 101765125000, + }, + ActionCosts::add_function_call_key_base => Fee { + send_sir: 102217625000, + send_not_sir: 102217625000, + execution: 102217625000, + }, + ActionCosts::add_function_call_key_byte => Fee { + send_sir: 1925331, + send_not_sir: 1925331, + execution: 1925331, }, - delete_key_cost: Fee { + ActionCosts::delete_key => Fee { send_sir: 94946625000, send_not_sir: 94946625000, execution: 94946625000, }, - delete_account_cost: Fee { - send_sir: 147489000000, - send_not_sir: 147489000000, - execution: 147489000000, + ActionCosts::new_action_receipt => Fee { + send_sir: 108059500000, + send_not_sir: 108059500000, + execution: 108059500000, + }, + ActionCosts::new_data_receipt_base => Fee { + send_sir: 4697339419375, + send_not_sir: 4697339419375, + execution: 4697339419375, + }, + ActionCosts::new_data_receipt_byte => Fee { + send_sir: 59357464, + send_not_sir: 59357464, + execution: 59357464, }, }, - storage_usage_config: StorageUsageConfig { - // See Account in core/primitives/src/account.rs for the data structure. - // TODO(2291): figure out value for the mainnet. - num_bytes_account: 100, - num_extra_bytes_record: 40, - }, - burnt_gas_reward: Rational::new(3, 10), - pessimistic_gas_price_inflation_ratio: Rational::new(103, 100), } } pub fn free() -> Self { - let free = Fee { send_sir: 0, send_not_sir: 0, execution: 0 }; - RuntimeFeesConfig { - action_receipt_creation_config: free.clone(), - data_receipt_creation_config: DataReceiptCreationConfig { - base_cost: free.clone(), - cost_per_byte: free.clone(), - }, - action_creation_config: ActionCreationConfig { - create_account_cost: free.clone(), - deploy_contract_cost: free.clone(), - deploy_contract_cost_per_byte: free.clone(), - function_call_cost: free.clone(), - function_call_cost_per_byte: free.clone(), - transfer_cost: free.clone(), - stake_cost: free.clone(), - add_key_cost: AccessKeyCreationConfig { - full_access_cost: free.clone(), - function_call_cost: free.clone(), - function_call_cost_per_byte: free.clone(), - }, - delete_key_cost: free.clone(), - delete_account_cost: free, - }, - storage_usage_config: StorageUsageConfig { - num_bytes_account: 0, - num_extra_bytes_record: 0, + Self { + action_fees: enum_map::enum_map! { + _ => Fee { send_sir: 0, send_not_sir: 0, execution: 0 } }, + storage_usage_config: StorageUsageConfig::free(), burnt_gas_reward: Rational::from_integer(0), pessimistic_gas_price_inflation_ratio: Rational::from_integer(0), } @@ -269,8 +240,22 @@ impl RuntimeFeesConfig { /// This amount is used to determine how many receipts can be created, send and executed for /// some amount of prepaid gas using function calls. pub fn min_receipt_with_function_call_gas(&self) -> Gas { - self.action_receipt_creation_config.min_send_and_exec_fee() - + self.action_creation_config.function_call_cost.min_send_and_exec_fee() + self.fee(ActionCosts::new_action_receipt).min_send_and_exec_fee() + + self.fee(ActionCosts::function_call_base).min_send_and_exec_fee() + } +} + +impl StorageUsageConfig { + pub fn test() -> Self { + Self { + num_bytes_account: 100, + num_extra_bytes_record: 40, + storage_amount_per_byte: 909 * 100_000_000_000_000_000, + } + } + + pub(crate) fn free() -> StorageUsageConfig { + Self { num_bytes_account: 0, num_extra_bytes_record: 0, storage_amount_per_byte: 0 } } } @@ -278,26 +263,26 @@ impl RuntimeFeesConfig { /// In case of implicit account creation they always include extra fees for the CreateAccount and /// AddFullAccessKey actions that are implicit. /// We can assume that no overflow will happen here. -pub fn transfer_exec_fee(cfg: &ActionCreationConfig, is_receiver_implicit: bool) -> Gas { +pub fn transfer_exec_fee(cfg: &RuntimeFeesConfig, is_receiver_implicit: bool) -> Gas { if is_receiver_implicit { - cfg.create_account_cost.exec_fee() - + cfg.add_key_cost.full_access_cost.exec_fee() - + cfg.transfer_cost.exec_fee() + cfg.fee(ActionCosts::create_account).exec_fee() + + cfg.fee(ActionCosts::add_full_access_key).exec_fee() + + cfg.fee(ActionCosts::transfer).exec_fee() } else { - cfg.transfer_cost.exec_fee() + cfg.fee(ActionCosts::transfer).exec_fee() } } pub fn transfer_send_fee( - cfg: &ActionCreationConfig, + cfg: &RuntimeFeesConfig, sender_is_receiver: bool, is_receiver_implicit: bool, ) -> Gas { if is_receiver_implicit { - cfg.create_account_cost.send_fee(sender_is_receiver) - + cfg.add_key_cost.full_access_cost.send_fee(sender_is_receiver) - + cfg.transfer_cost.send_fee(sender_is_receiver) + cfg.fee(ActionCosts::create_account).send_fee(sender_is_receiver) + + cfg.fee(ActionCosts::add_full_access_key).send_fee(sender_is_receiver) + + cfg.fee(ActionCosts::transfer).send_fee(sender_is_receiver) } else { - cfg.transfer_cost.send_fee(sender_is_receiver) + cfg.fee(ActionCosts::transfer).send_fee(sender_is_receiver) } } diff --git a/core/primitives/Cargo.toml b/core/primitives/Cargo.toml index c6663b07e4d..34d37428d87 100644 --- a/core/primitives/Cargo.toml +++ b/core/primitives/Cargo.toml @@ -21,6 +21,7 @@ cfg-if.workspace = true chrono.workspace = true derive_more.workspace = true easy-ext.workspace = true +enum-map.workspace = true hex.workspace = true num-rational.workspace = true once_cell.workspace = true diff --git a/core/primitives/src/runtime/config.rs b/core/primitives/src/runtime/config.rs index 90214d1cdea..6d75728ffef 100644 --- a/core/primitives/src/runtime/config.rs +++ b/core/primitives/src/runtime/config.rs @@ -1,26 +1,25 @@ //! Settings of the parameters of the runtime. -use serde::{Deserialize, Serialize}; - use crate::config::VMConfig; use crate::runtime::config_store::INITIAL_TESTNET_CONFIG; use crate::runtime::fees::RuntimeFeesConfig; use crate::runtime::parameter_table::ParameterTable; -use crate::serialize::dec_format; -use crate::types::{AccountId, Balance}; +use crate::types::AccountId; +use near_primitives_core::types::Balance; use super::parameter_table::InvalidConfigError; /// The structure that holds the parameters of the runtime, mostly economics. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct RuntimeConfig { - /// Amount of yN per byte required to have on the account. See - /// for details. - #[serde(with = "dec_format")] - pub storage_amount_per_byte: Balance, - /// Costs of different actions that need to be performed when sending and processing transaction - /// and receipts. - pub transaction_costs: RuntimeFeesConfig, - /// Config of wasm operations. + /// Action gas costs, storage fees, and economic constants around them. + /// + /// This contains parameters that are required by the WASM runtime and the + /// transaction runtime. + pub fees: RuntimeFeesConfig, + /// Config of wasm operations, also includes wasm gas costs. + /// + /// This contains all the configuration parameters that are only required by + /// the WASM runtime. pub wasm_config: VMConfig, /// Config that defines rules for account creation. pub account_creation_config: AccountCreationConfig, @@ -28,8 +27,7 @@ pub struct RuntimeConfig { impl RuntimeConfig { pub(crate) fn new(params: &ParameterTable) -> Result { - serde_json::from_value(params.runtime_config_json()) - .map_err(InvalidConfigError::WrongStructure) + RuntimeConfig::try_from(params) } pub fn initial_testnet_config() -> RuntimeConfig { @@ -41,9 +39,7 @@ impl RuntimeConfig { pub fn test() -> Self { RuntimeConfig { - // See https://nomicon.io/Economics/README.html#general-variables for how it was calculated. - storage_amount_per_byte: 909 * 100_000_000_000_000_000, - transaction_costs: RuntimeFeesConfig::test(), + fees: RuntimeFeesConfig::test(), wasm_config: VMConfig::test(), account_creation_config: AccountCreationConfig::default(), } @@ -51,16 +47,19 @@ impl RuntimeConfig { pub fn free() -> Self { Self { - storage_amount_per_byte: 0, - transaction_costs: RuntimeFeesConfig::free(), + fees: RuntimeFeesConfig::free(), wasm_config: VMConfig::free(), account_creation_config: AccountCreationConfig::default(), } } + + pub fn storage_amount_per_byte(&self) -> Balance { + self.fees.storage_usage_config.storage_amount_per_byte + } } /// The structure describes configuration for creation of new accounts. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountCreationConfig { /// The minimum length of the top-level account ID that is allowed to be created by any account. pub min_allowed_top_level_account_length: u8, diff --git a/core/primitives/src/runtime/config_store.rs b/core/primitives/src/runtime/config_store.rs index ed79debdf3b..7b337c82eba 100644 --- a/core/primitives/src/runtime/config_store.rs +++ b/core/primitives/src/runtime/config_store.rs @@ -69,7 +69,7 @@ impl RuntimeConfigStore { let mut config = runtime_config.clone(); store.insert(0, Arc::new(config.clone())); - config.storage_amount_per_byte = 10u128.pow(19); + config.fees.storage_usage_config.storage_amount_per_byte = 10u128.pow(19); store.insert(42, Arc::new(config)); } @@ -109,6 +109,7 @@ mod tests { use crate::version::ProtocolFeature::{ LowerDataReceiptAndEcrecoverBaseCost, LowerStorageCost, LowerStorageKeyLimit, }; + use near_primitives_core::config::ActionCosts; const GENESIS_PROTOCOL_VERSION: ProtocolVersion = 29; const RECEIPTS_DEPTH: u64 = 63; @@ -119,7 +120,7 @@ mod tests { for (protocol_version, config) in store.store.iter() { assert!( config.wasm_config.limit_config.max_total_prepaid_gas - / config.transaction_costs.min_receipt_with_function_call_gas() + / config.fees.min_receipt_with_function_call_gas() <= 63, "The maximum desired depth of receipts for protocol version {} should be at most {}", protocol_version, @@ -133,7 +134,7 @@ mod tests { let store = RuntimeConfigStore::new(None); let base_cfg = store.get_config(GENESIS_PROTOCOL_VERSION); let new_cfg = store.get_config(LowerStorageCost.protocol_version()); - assert!(base_cfg.storage_amount_per_byte > new_cfg.storage_amount_per_byte); + assert!(base_cfg.storage_amount_per_byte() > new_cfg.storage_amount_per_byte()); } #[test] @@ -158,12 +159,12 @@ mod tests { let base_cfg = store.get_config(LowerStorageCost.protocol_version()); let new_cfg = store.get_config(LowerDataReceiptAndEcrecoverBaseCost.protocol_version()); assert!( - base_cfg.transaction_costs.data_receipt_creation_config.base_cost.send_sir - > new_cfg.transaction_costs.data_receipt_creation_config.base_cost.send_sir + base_cfg.fees.fee(ActionCosts::new_data_receipt_base).send_sir + > new_cfg.fees.fee(ActionCosts::new_data_receipt_base).send_sir ); assert!( - base_cfg.transaction_costs.data_receipt_creation_config.cost_per_byte.send_sir - > new_cfg.transaction_costs.data_receipt_creation_config.cost_per_byte.send_sir + base_cfg.fees.fee(ActionCosts::new_data_receipt_byte).send_sir + > new_cfg.fees.fee(ActionCosts::new_data_receipt_byte).send_sir ); } @@ -179,12 +180,9 @@ mod tests { assert_eq!(config.as_ref(), &base_config); let config = store.get_config(LowerStorageCost.protocol_version()); - assert_eq!(base_config.storage_amount_per_byte, 100_000_000_000_000_000_000u128); - assert_eq!(config.storage_amount_per_byte, 10_000_000_000_000_000_000u128); - assert_eq!( - config.transaction_costs.data_receipt_creation_config.base_cost.send_sir, - 4_697_339_419_375 - ); + assert_eq!(base_config.storage_amount_per_byte(), 100_000_000_000_000_000_000u128); + assert_eq!(config.storage_amount_per_byte(), 10_000_000_000_000_000_000u128); + assert_eq!(config.fees.fee(ActionCosts::new_data_receipt_base).send_sir, 4_697_339_419_375); assert_ne!(config.as_ref(), &base_config); assert_ne!( config.as_ref(), @@ -199,10 +197,7 @@ mod tests { assert_eq!(**config, expected_config); let config = store.get_config(LowerDataReceiptAndEcrecoverBaseCost.protocol_version()); - assert_eq!( - config.transaction_costs.data_receipt_creation_config.base_cost.send_sir, - 36_486_732_312 - ); + 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(); @@ -240,11 +235,14 @@ mod tests { #[test] #[cfg(not(feature = "nightly"))] fn test_json_unchanged() { + use crate::views::RuntimeConfigView; + let store = RuntimeConfigStore::new(None); for version in store.store.keys() { let snapshot_name = format!("{version}.json"); - insta::assert_json_snapshot!(snapshot_name, store.get_config(*version)); + let config_view = RuntimeConfigView::from(store.get_config(*version).as_ref().clone()); + insta::assert_json_snapshot!(snapshot_name, config_view); } // Testnet initial config for old version was different, thus needs separate testing @@ -254,7 +252,8 @@ mod tests { for version in testnet_store.store.keys() { let snapshot_name = format!("testnet_{version}.json"); - insta::assert_json_snapshot!(snapshot_name, store.get_config(*version)); + let config_view = RuntimeConfigView::from(store.get_config(*version).as_ref().clone()); + insta::assert_json_snapshot!(snapshot_name, config_view); } } } diff --git a/core/primitives/src/runtime/mod.rs b/core/primitives/src/runtime/mod.rs index 3232eb5ee4f..7f7968e1c53 100644 --- a/core/primitives/src/runtime/mod.rs +++ b/core/primitives/src/runtime/mod.rs @@ -23,7 +23,7 @@ pub fn get_insufficient_storage_stake( runtime_config: &RuntimeConfig, ) -> Result, String> { let required_amount = Balance::from(account.storage_usage()) - .checked_mul(runtime_config.storage_amount_per_byte) + .checked_mul(runtime_config.storage_amount_per_byte()) .ok_or_else(|| { format!("Account's storage_usage {} overflows multiplication", account.storage_usage()) })?; diff --git a/core/primitives/src/runtime/parameter_table.rs b/core/primitives/src/runtime/parameter_table.rs index fb8c3ed4d18..50372ed6fff 100644 --- a/core/primitives/src/runtime/parameter_table.rs +++ b/core/primitives/src/runtime/parameter_table.rs @@ -1,5 +1,11 @@ +use super::config::{AccountCreationConfig, RuntimeConfig}; +use near_primitives_core::config::VMConfig; use near_primitives_core::parameter::{FeeParameter, Parameter}; +use near_primitives_core::runtime::fees::{RuntimeFeesConfig, StorageUsageConfig}; +use num_rational::Rational; +use serde::de::DeserializeOwned; use serde_json::json; +use std::any::Any; use std::collections::BTreeMap; pub(crate) struct ParameterTable { @@ -31,6 +37,10 @@ pub(crate) enum InvalidConfigError { NoOldValueExists(Parameter, String), #[error("expected old value `{1}` but found `{2}` for parameter `{0}` in config diff")] WrongOldValue(Parameter, String, String), + #[error("expected a value for `{0}` but found none")] + MissingParameter(Parameter), + #[error("expected a value of type `{2}` for `{1}` but could not parse it from `{3}`")] + WrongValueType(#[source] serde_json::Error, Parameter, &'static str, String), } impl std::str::FromStr for ParameterTable { @@ -46,27 +56,48 @@ impl std::str::FromStr for ParameterTable { } } -impl ParameterTable { - /// Transforms parameters stored in the table into a JSON representation of `RuntimeConfig`. - pub(crate) fn runtime_config_json(&self) -> serde_json::Value { - let storage_amount_per_byte = self.get(Parameter::StorageAmountPerByte); - let transaction_costs = self.transaction_costs_json(); - json!({ - "storage_amount_per_byte": storage_amount_per_byte, - "transaction_costs": transaction_costs, - "wasm_config": { - "ext_costs": self.json_map(Parameter::ext_costs(), "wasm_"), - "grow_mem_cost": self.get(Parameter::WasmGrowMemCost), - "regular_op_cost": self.get(Parameter::WasmRegularOpCost), - "limit_config": self.json_map(Parameter::vm_limits(), ""), +impl TryFrom<&ParameterTable> for RuntimeConfig { + type Error = InvalidConfigError; + + fn try_from(params: &ParameterTable) -> Result { + Ok(RuntimeConfig { + fees: RuntimeFeesConfig { + action_fees: enum_map::enum_map! { + action_cost => params.fee(action_cost) + }, + burnt_gas_reward: Rational::new( + params.get_parsed(Parameter::BurntGasRewardNumerator)?, + params.get_parsed(Parameter::BurntGasRewardDenominator)?, + ), + pessimistic_gas_price_inflation_ratio: Rational::new( + params.get_parsed(Parameter::PessimisticGasPriceInflationNumerator)?, + params.get_parsed(Parameter::PessimisticGasPriceInflationDenominator)?, + ), + storage_usage_config: StorageUsageConfig { + storage_amount_per_byte: params.get_u128(Parameter::StorageAmountPerByte)?, + num_bytes_account: params.get_parsed(Parameter::StorageNumBytesAccount)?, + num_extra_bytes_record: params + .get_parsed(Parameter::StorageNumExtraBytesRecord)?, + }, + }, + wasm_config: VMConfig { + ext_costs: serde_json::from_value(params.json_map(Parameter::ext_costs(), "wasm_")) + .map_err(InvalidConfigError::WrongStructure)?, + grow_mem_cost: params.get_parsed(Parameter::WasmGrowMemCost)?, + regular_op_cost: params.get_parsed(Parameter::WasmRegularOpCost)?, + limit_config: serde_json::from_value(params.json_map(Parameter::vm_limits(), "")) + .map_err(InvalidConfigError::WrongStructure)?, + }, + account_creation_config: AccountCreationConfig { + min_allowed_top_level_account_length: params + .get_parsed(Parameter::MinAllowedTopLevelAccountLength)?, + registrar_account_id: params.get_parsed(Parameter::RegistrarAccountId)?, }, - "account_creation_config": { - "min_allowed_top_level_account_length": self.get(Parameter::MinAllowedTopLevelAccountLength), - "registrar_account_id": self.get(Parameter::RegistrarAccountId), - } }) } +} +impl ParameterTable { pub(crate) fn apply_diff( &mut self, diff: ParameterTableDiff, @@ -103,44 +134,6 @@ impl ParameterTable { Ok(()) } - fn transaction_costs_json(&self) -> serde_json::Value { - json!( { - "action_receipt_creation_config": self.fee_json(FeeParameter::ActionReceiptCreation), - "data_receipt_creation_config": { - "base_cost": self.fee_json(FeeParameter::DataReceiptCreationBase), - "cost_per_byte": self.fee_json(FeeParameter::DataReceiptCreationPerByte), - }, - "action_creation_config": { - "create_account_cost": self.fee_json(FeeParameter::ActionCreateAccount), - "deploy_contract_cost": self.fee_json(FeeParameter::ActionDeployContract), - "deploy_contract_cost_per_byte": self.fee_json(FeeParameter::ActionDeployContractPerByte), - "function_call_cost": self.fee_json(FeeParameter::ActionFunctionCall), - "function_call_cost_per_byte": self.fee_json(FeeParameter::ActionFunctionCallPerByte), - "transfer_cost": self.fee_json(FeeParameter::ActionTransfer), - "stake_cost": self.fee_json(FeeParameter::ActionStake), - "add_key_cost": { - "full_access_cost": self.fee_json(FeeParameter::ActionAddFullAccessKey), - "function_call_cost": self.fee_json(FeeParameter::ActionAddFunctionCallKey), - "function_call_cost_per_byte": self.fee_json(FeeParameter::ActionAddFunctionCallKeyPerByte), - }, - "delete_key_cost": self.fee_json(FeeParameter::ActionDeleteKey), - "delete_account_cost": self.fee_json(FeeParameter::ActionDeleteAccount), - }, - "storage_usage_config": { - "num_bytes_account": self.get(Parameter::StorageNumBytesAccount), - "num_extra_bytes_record": self.get(Parameter::StorageNumExtraBytesRecord), - }, - "burnt_gas_reward": [ - self.get(Parameter::BurntGasRewardNumerator), - self.get(Parameter::BurntGasRewardDenominator) - ], - "pessimistic_gas_price_inflation_ratio": [ - self.get(Parameter::PessimisticGasPriceInflationNumerator), - self.get(Parameter::PessimisticGasPriceInflationDenominator) - ] - }) - } - fn json_map( &self, params: impl Iterator, @@ -161,6 +154,46 @@ impl ParameterTable { self.parameters.get(&key) } + /// Access action fee by `ActionCosts`. + fn fee( + &self, + cost: near_primitives_core::config::ActionCosts, + ) -> near_primitives_core::runtime::fees::Fee { + let json = self.fee_json(FeeParameter::from(cost)); + serde_json::from_value::(json) + .expect("just constructed a Fee JSON") + } + + /// Read and parse a parameter from the `ParameterTable`. + fn get_parsed( + &self, + key: Parameter, + ) -> Result { + let value = self.parameters.get(&key).ok_or(InvalidConfigError::MissingParameter(key))?; + serde_json::from_value(value.clone()).map_err(|parse_err| { + InvalidConfigError::WrongValueType( + parse_err, + key, + std::any::type_name::(), + value.to_string(), + ) + }) + } + + /// Read and parse a parameter from the `ParameterTable`. + fn get_u128(&self, key: Parameter) -> Result { + let value = self.parameters.get(&key).ok_or(InvalidConfigError::MissingParameter(key))?; + + near_primitives_core::serialize::dec_format::deserialize(value).map_err(|parse_err| { + InvalidConfigError::WrongValueType( + parse_err, + key, + std::any::type_name::(), + value.to_string(), + ) + }) + } + fn fee_json(&self, key: FeeParameter) -> serde_json::Value { json!( { "send_sir": self.get(format!("{key}_send_sir").parse().unwrap()), diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index dc13a627303..60376d0e8d6 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -10,7 +10,9 @@ use std::sync::Arc; use borsh::{BorshDeserialize, BorshSerialize}; use chrono::DateTime; -use near_primitives_core::config::ActionCosts; +use near_primitives_core::config::{ActionCosts, VMConfig}; +use near_primitives_core::runtime::fees::Fee; +use num_rational::Rational; use serde::{Deserialize, Serialize}; use near_crypto::{PublicKey, Signature}; @@ -30,6 +32,7 @@ use crate::merkle::{combine_hash, MerklePath}; use crate::network::PeerId; use crate::profile::Cost; use crate::receipt::{ActionReceipt, DataReceipt, DataReceiver, Receipt, ReceiptEnum}; +use crate::runtime::config::RuntimeConfig; use crate::serialize::{base64_format, dec_format, option_base64_format}; use crate::sharding::{ ChunkHash, ShardChunk, ShardChunkHeader, ShardChunkHeaderInner, ShardChunkHeaderInnerV2, @@ -1962,3 +1965,237 @@ pub type StateChangesView = Vec; /// Maintenance windows view are a vector of maintenance window. pub type MaintenanceWindowsView = Vec>; + +/// View that preserves JSON format of the runtime config. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RuntimeConfigView { + /// Amount of yN per byte required to have on the account. See + /// for details. + #[serde(with = "dec_format")] + pub storage_amount_per_byte: Balance, + /// Costs of different actions that need to be performed when sending and + /// processing transaction and receipts. + pub transaction_costs: RuntimeFeesConfigView, + /// Config of wasm operations. + /// + /// TODO: This should be refactored to `VMConfigView` to detach the config + /// format from RPC output. + pub wasm_config: VMConfig, + /// Config that defines rules for account creation. + pub account_creation_config: AccountCreationConfigView, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RuntimeFeesConfigView { + /// Describes the cost of creating an action receipt, `ActionReceipt`, excluding the actual cost + /// of actions. + /// - `send` cost is burned when a receipt is created using `promise_create` or + /// `promise_batch_create` + /// - `exec` cost is burned when the receipt is being executed. + pub action_receipt_creation_config: Fee, + /// Describes the cost of creating a data receipt, `DataReceipt`. + pub data_receipt_creation_config: DataReceiptCreationConfigView, + /// Describes the cost of creating a certain action, `Action`. Includes all variants. + pub action_creation_config: ActionCreationConfigView, + /// Describes fees for storage. + pub storage_usage_config: StorageUsageConfigView, + + /// Fraction of the burnt gas to reward to the contract account for execution. + pub burnt_gas_reward: Rational, + + /// Pessimistic gas price inflation ratio. + pub pessimistic_gas_price_inflation_ratio: Rational, +} + +/// The structure describes configuration for creation of new accounts. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AccountCreationConfigView { + /// The minimum length of the top-level account ID that is allowed to be created by any account. + pub min_allowed_top_level_account_length: u8, + /// The account ID of the account registrar. This account ID allowed to create top-level + /// accounts of any valid length. + pub registrar_account_id: AccountId, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Hash, PartialEq, Eq)] +pub struct DataReceiptCreationConfigView { + /// Base cost of creating a data receipt. + /// Both `send` and `exec` costs are burned when a new receipt has input dependencies. The gas + /// is charged for each input dependency. The dependencies are specified when a receipt is + /// created using `promise_then` and `promise_batch_then`. + /// NOTE: Any receipt with output dependencies will produce data receipts. Even if it fails. + /// Even if the last action is not a function call (in case of success it will return empty + /// value). + pub base_cost: Fee, + /// Additional cost per byte sent. + /// Both `send` and `exec` costs are burned when a function call finishes execution and returns + /// `N` bytes of data to every output dependency. For each output dependency the cost is + /// `(send(sir) + exec()) * N`. + pub cost_per_byte: Fee, +} + +/// Describes the cost of creating a specific action, `Action`. Includes all variants. +#[derive(Debug, Serialize, Deserialize, Clone, Hash, PartialEq, Eq)] +pub struct ActionCreationConfigView { + /// Base cost of creating an account. + pub create_account_cost: Fee, + + /// Base cost of deploying a contract. + pub deploy_contract_cost: Fee, + /// Cost per byte of deploying a contract. + pub deploy_contract_cost_per_byte: Fee, + + /// Base cost of calling a function. + pub function_call_cost: Fee, + /// Cost per byte of method name and arguments of calling a function. + pub function_call_cost_per_byte: Fee, + + /// Base cost of making a transfer. + pub transfer_cost: Fee, + + /// Base cost of staking. + pub stake_cost: Fee, + + /// Base cost of adding a key. + pub add_key_cost: AccessKeyCreationConfigView, + + /// Base cost of deleting a key. + pub delete_key_cost: Fee, + + /// Base cost of deleting an account. + pub delete_account_cost: Fee, +} + +/// Describes the cost of creating an access key. +#[derive(Debug, Serialize, Deserialize, Clone, Hash, PartialEq, Eq)] +pub struct AccessKeyCreationConfigView { + /// Base cost of creating a full access access-key. + pub full_access_cost: Fee, + /// Base cost of creating an access-key restricted to specific functions. + pub function_call_cost: Fee, + /// Cost per byte of method_names of creating a restricted access-key. + pub function_call_cost_per_byte: Fee, +} + +/// Describes cost of storage per block +#[derive(Debug, Serialize, Deserialize, Clone, Hash, PartialEq, Eq)] +pub struct StorageUsageConfigView { + /// Number of bytes for an account record, including rounding up for account id. + pub num_bytes_account: u64, + /// Additional number of bytes for a k/v record + pub num_extra_bytes_record: u64, +} + +impl From for RuntimeConfigView { + fn from(config: RuntimeConfig) -> Self { + Self { + storage_amount_per_byte: config.storage_amount_per_byte(), + transaction_costs: RuntimeFeesConfigView { + action_receipt_creation_config: config + .fees + .fee(ActionCosts::new_action_receipt) + .clone(), + data_receipt_creation_config: DataReceiptCreationConfigView { + base_cost: config.fees.fee(ActionCosts::new_data_receipt_base).clone(), + cost_per_byte: config.fees.fee(ActionCosts::new_data_receipt_byte).clone(), + }, + action_creation_config: ActionCreationConfigView { + create_account_cost: config.fees.fee(ActionCosts::create_account).clone(), + deploy_contract_cost: config + .fees + .fee(ActionCosts::deploy_contract_base) + .clone(), + deploy_contract_cost_per_byte: config + .fees + .fee(ActionCosts::deploy_contract_byte) + .clone(), + function_call_cost: config.fees.fee(ActionCosts::function_call_base).clone(), + function_call_cost_per_byte: config + .fees + .fee(ActionCosts::function_call_byte) + .clone(), + transfer_cost: config.fees.fee(ActionCosts::transfer).clone(), + stake_cost: config.fees.fee(ActionCosts::stake).clone(), + add_key_cost: AccessKeyCreationConfigView { + full_access_cost: config.fees.fee(ActionCosts::add_full_access_key).clone(), + function_call_cost: config + .fees + .fee(ActionCosts::add_function_call_key_base) + .clone(), + function_call_cost_per_byte: config + .fees + .fee(ActionCosts::add_function_call_key_byte) + .clone(), + }, + delete_key_cost: config.fees.fee(ActionCosts::delete_key).clone(), + delete_account_cost: config.fees.fee(ActionCosts::delete_account).clone(), + }, + storage_usage_config: StorageUsageConfigView { + num_bytes_account: config.fees.storage_usage_config.num_bytes_account, + num_extra_bytes_record: config.fees.storage_usage_config.num_extra_bytes_record, + }, + burnt_gas_reward: config.fees.burnt_gas_reward, + pessimistic_gas_price_inflation_ratio: config + .fees + .pessimistic_gas_price_inflation_ratio, + }, + wasm_config: config.wasm_config, + account_creation_config: AccountCreationConfigView { + min_allowed_top_level_account_length: config + .account_creation_config + .min_allowed_top_level_account_length, + registrar_account_id: config.account_creation_config.registrar_account_id, + }, + } + } +} + +// reverse direction: rosetta adapter uses this, also we use to test that all fields are present in view (TODO) +impl From for RuntimeConfig { + fn from(config: RuntimeConfigView) -> Self { + Self { + fees: near_primitives_core::runtime::fees::RuntimeFeesConfig { + storage_usage_config: near_primitives_core::runtime::fees::StorageUsageConfig { + storage_amount_per_byte: config.storage_amount_per_byte, + num_bytes_account: config + .transaction_costs + .storage_usage_config + .num_bytes_account, + num_extra_bytes_record: config + .transaction_costs + .storage_usage_config + .num_extra_bytes_record, + }, + burnt_gas_reward: config.transaction_costs.burnt_gas_reward, + pessimistic_gas_price_inflation_ratio: config + .transaction_costs + .pessimistic_gas_price_inflation_ratio, + action_fees: enum_map::enum_map! { + ActionCosts::create_account => config.transaction_costs.action_creation_config.create_account_cost.clone(), + ActionCosts::delete_account => config.transaction_costs.action_creation_config.delete_account_cost.clone(), + ActionCosts::deploy_contract_base => config.transaction_costs.action_creation_config.deploy_contract_cost.clone(), + ActionCosts::deploy_contract_byte => config.transaction_costs.action_creation_config.deploy_contract_cost_per_byte.clone(), + ActionCosts::function_call_base => config.transaction_costs.action_creation_config.function_call_cost.clone(), + ActionCosts::function_call_byte => config.transaction_costs.action_creation_config.function_call_cost_per_byte.clone(), + ActionCosts::transfer => config.transaction_costs.action_creation_config.transfer_cost.clone(), + ActionCosts::stake => config.transaction_costs.action_creation_config.stake_cost.clone(), + ActionCosts::add_full_access_key => config.transaction_costs.action_creation_config.add_key_cost.full_access_cost.clone(), + ActionCosts::add_function_call_key_base => config.transaction_costs.action_creation_config.add_key_cost.function_call_cost.clone(), + ActionCosts::add_function_call_key_byte => config.transaction_costs.action_creation_config.add_key_cost.function_call_cost_per_byte.clone(), + ActionCosts::delete_key => config.transaction_costs.action_creation_config.delete_key_cost.clone(), + ActionCosts::new_action_receipt => config.transaction_costs.action_receipt_creation_config.clone(), + ActionCosts::new_data_receipt_base => config.transaction_costs.data_receipt_creation_config.base_cost.clone(), + ActionCosts::new_data_receipt_byte => config.transaction_costs.data_receipt_creation_config.cost_per_byte.clone(), + + }, + }, + wasm_config: config.wasm_config, + account_creation_config: crate::runtime::config::AccountCreationConfig { + min_allowed_top_level_account_length: config + .account_creation_config + .min_allowed_top_level_account_length, + registrar_account_id: config.account_creation_config.registrar_account_id, + }, + } + } +} diff --git a/integration-tests/src/node/runtime_node.rs b/integration-tests/src/node/runtime_node.rs index 10abf2a0482..cce1b8db24c 100644 --- a/integration-tests/src/node/runtime_node.rs +++ b/integration-tests/src/node/runtime_node.rs @@ -109,8 +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.transaction_costs, node.genesis().config.min_gas_price); + let fee_helper = FeeHelper::new(runtime_config.fees, 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/process_blocks.rs b/integration-tests/src/tests/client/process_blocks.rs index 235ad273ded..b9e5508e155 100644 --- a/integration-tests/src/tests/client/process_blocks.rs +++ b/integration-tests/src/tests/client/process_blocks.rs @@ -9,6 +9,7 @@ use assert_matches::assert_matches; use futures::{future, FutureExt}; use near_chain::test_utils::ValidatorSchedule; use near_chunks::test_utils::MockClientAdapterForShardsManager; +use near_primitives::config::ActionCosts; use near_primitives::num_rational::{Ratio, Rational32}; use near_actix_test_utils::run_actix; @@ -1850,13 +1851,12 @@ fn test_gas_price_change() { init_test_logger(); let mut genesis = Genesis::test(vec!["test0".parse().unwrap(), "test1".parse().unwrap()], 1); let target_num_tokens_left = NEAR_BASE / 10 + 1; - let transaction_costs = RuntimeConfig::test().transaction_costs; + let transaction_costs = RuntimeConfig::test().fees; - let send_money_total_gas = - transaction_costs.action_creation_config.transfer_cost.send_fee(false) - + transaction_costs.action_receipt_creation_config.send_fee(false) - + transaction_costs.action_creation_config.transfer_cost.exec_fee() - + transaction_costs.action_receipt_creation_config.exec_fee(); + let send_money_total_gas = transaction_costs.fee(ActionCosts::transfer).send_fee(false) + + transaction_costs.fee(ActionCosts::new_action_receipt).send_fee(false) + + transaction_costs.fee(ActionCosts::transfer).exec_fee() + + transaction_costs.fee(ActionCosts::new_action_receipt).exec_fee(); let min_gas_price = target_num_tokens_left / send_money_total_gas as u128; let gas_limit = 1000000000000; let gas_price_adjustment_rate = Ratio::new(1, 10); @@ -2647,10 +2647,9 @@ fn test_execution_metadata() { let config = RuntimeConfigStore::test().get_config(PROTOCOL_VERSION).clone(); // Total costs for creating a function call receipt. - let expected_receipt_cost = config.transaction_costs.action_receipt_creation_config.execution - + config.transaction_costs.action_creation_config.function_call_cost.exec_fee() - + config.transaction_costs.action_creation_config.function_call_cost_per_byte.exec_fee() - * "main".len() as u64; + let expected_receipt_cost = config.fees.fee(ActionCosts::new_action_receipt).execution + + config.fees.fee(ActionCosts::function_call_base).exec_fee() + + config.fees.fee(ActionCosts::function_call_byte).exec_fee() * "main".len() as u64; // Profile for what's happening *inside* wasm vm during function call. let expected_profile = serde_json::json!([ diff --git a/integration-tests/src/tests/nearcore/rpc_nodes.rs b/integration-tests/src/tests/nearcore/rpc_nodes.rs index 7c212f3d27f..0bec8c9a655 100644 --- a/integration-tests/src/tests/nearcore/rpc_nodes.rs +++ b/integration-tests/src/tests/nearcore/rpc_nodes.rs @@ -22,7 +22,8 @@ use near_primitives::types::{ BlockId, BlockReference, EpochId, EpochReference, Finality, TransactionOrReceiptId, }; use near_primitives::version::ProtocolVersion; -use near_primitives::views::{ExecutionOutcomeView, ExecutionStatusView}; +use near_primitives::views::{ExecutionOutcomeView, ExecutionStatusView, RuntimeConfigView}; +use node_runtime::config::RuntimeConfig; use std::time::Duration; #[test] @@ -250,10 +251,16 @@ fn test_protocol_config_rpc() { let latest_runtime_config = runtime_config_store.get_config(ProtocolVersion::MAX); assert_ne!( config_response.config_view.runtime_config.storage_amount_per_byte, - intial_runtime_config.storage_amount_per_byte + intial_runtime_config.storage_amount_per_byte() ); + // compare JSON view assert_eq!( - config_response.config_view.runtime_config, + serde_json::json!(config_response.config_view.runtime_config), + serde_json::json!(RuntimeConfigView::from(latest_runtime_config.as_ref().clone())) + ); + // compare struct used by runtime + assert_eq!( + RuntimeConfig::from(config_response.config_view.runtime_config), latest_runtime_config.as_ref().clone() ); System::current().stop(); diff --git a/integration-tests/src/tests/runtime/deployment.rs b/integration-tests/src/tests/runtime/deployment.rs index f4fc1874c20..ccc0b6d5283 100644 --- a/integration-tests/src/tests/runtime/deployment.rs +++ b/integration-tests/src/tests/runtime/deployment.rs @@ -39,7 +39,7 @@ fn test_deploy_max_size_contract() { let max_transaction_size = config.wasm_config.limit_config.max_transaction_size; let contract_size = max_contract_size.min(max_transaction_size - tx_overhead); // Enough token to store contract + 1 NEAR for account - let token_balance = config.storage_amount_per_byte * contract_size as u128 + ONE_NEAR; + let token_balance = config.storage_amount_per_byte() * contract_size as u128 + ONE_NEAR; // Create test account let transaction_result = node_user diff --git a/integration-tests/src/tests/standard_cases/mod.rs b/integration-tests/src/tests/standard_cases/mod.rs index 6648baa6ae9..3d54381d98c 100644 --- a/integration-tests/src/tests/standard_cases/mod.rs +++ b/integration-tests/src/tests/standard_cases/mod.rs @@ -7,6 +7,7 @@ use assert_matches::assert_matches; use near_crypto::{InMemorySigner, KeyType}; use near_jsonrpc_primitives::errors::ServerError; use near_primitives::account::{AccessKey, AccessKeyPermission, FunctionCallPermission}; +use near_primitives::config::ActionCosts; use near_primitives::errors::{ ActionError, ActionErrorKind, InvalidAccessKeyError, InvalidTxError, TxExecutionError, }; @@ -34,7 +35,7 @@ use testlib::runtime_utils::{ const FUNCTION_CALL_AMOUNT: Balance = TESTING_INIT_BALANCE / 10; fn fee_helper(node: &impl Node) -> FeeHelper { - FeeHelper::new(RuntimeConfig::test().transaction_costs, node.genesis().config.min_gas_price) + FeeHelper::new(RuntimeConfig::test().fees, node.genesis().config.min_gas_price) } /// Adds given access key to the given account_id using signer2. @@ -403,12 +404,10 @@ 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.action_creation_config.create_account_cost.send_fee(false) + fee_helper.cfg.fee(ActionCosts::create_account).send_fee(false) + fee_helper .cfg - .action_creation_config - .add_key_cost - .full_access_cost + .fee(near_primitives::config::ActionCosts::add_full_access_key) .send_fee(false), ); diff --git a/integration-tests/src/tests/standard_cases/runtime.rs b/integration-tests/src/tests/standard_cases/runtime.rs index 1fdf3fed165..72a5b5a816f 100644 --- a/integration-tests/src/tests/standard_cases/runtime.rs +++ b/integration-tests/src/tests/standard_cases/runtime.rs @@ -19,7 +19,7 @@ fn create_runtime_with_expensive_storage() -> RuntimeNode { add_test_contract(&mut genesis, &bob_account()); // Set expensive state requirements and add alice more money. let mut runtime_config = RuntimeConfig::test(); - runtime_config.storage_amount_per_byte = TESTING_INIT_BALANCE / 1000; + runtime_config.fees.storage_usage_config.storage_amount_per_byte = TESTING_INIT_BALANCE / 1000; let records = genesis.force_read_records().as_mut(); match &mut records[0] { StateRecord::Account { account, .. } => account.set_amount(TESTING_INIT_BALANCE * 10000), diff --git a/integration-tests/src/tests/test_errors.rs b/integration-tests/src/tests/test_errors.rs index e17fea16d12..ab27eb833f0 100644 --- a/integration-tests/src/tests/test_errors.rs +++ b/integration-tests/src/tests/test_errors.rs @@ -67,7 +67,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.transaction_costs.clone(), + runtime_config.fees.clone(), node.genesis().config.min_gas_price, ); let signer = diff --git a/nearcore/tests/economics.rs b/nearcore/tests/economics.rs index 48841d218fa..79d5adad625 100644 --- a/nearcore/tests/economics.rs +++ b/nearcore/tests/economics.rs @@ -62,7 +62,7 @@ fn test_burn_mint() { .get_protocol_config(&EpochId::default()) .unwrap() .runtime_config - .transaction_costs; + .fees; let fee_helper = FeeHelper::new(transaction_costs, 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; diff --git a/runtime/near-vm-logic/src/gas_counter.rs b/runtime/near-vm-logic/src/gas_counter.rs index ae0363f1aaa..1284c01e935 100644 --- a/runtime/near-vm-logic/src/gas_counter.rs +++ b/runtime/near-vm-logic/src/gas_counter.rs @@ -2,7 +2,6 @@ use crate::{HostError, VMLogicError}; use near_primitives::types::TrieNodesCount; use near_primitives_core::config::ExtCosts::read_cached_trie_node; use near_primitives_core::config::ExtCosts::touching_trie_node; -use near_primitives_core::runtime::fees::Fee; use near_primitives_core::{ config::{ActionCosts, ExtCosts, ExtCostsConfig}, profile::ProfileData, @@ -214,48 +213,6 @@ impl GasCounter { self.burn_gas(base_fee) } - /// A helper function to pay per byte gas fee for batching an action. - /// # Args: - /// * `per_byte_fee`: the fee per byte; - /// * `num_bytes`: the number of bytes; - /// * `sir`: whether the receiver_id is same as the current account ID; - /// * `action`: what kind of action is charged for; - pub fn pay_action_per_byte( - &mut self, - per_byte_fee: &Fee, - num_bytes: u64, - sir: bool, - action: ActionCosts, - ) -> Result<()> { - let burn_gas = - num_bytes.checked_mul(per_byte_fee.send_fee(sir)).ok_or(HostError::IntegerOverflow)?; - let use_gas = burn_gas - .checked_add( - num_bytes.checked_mul(per_byte_fee.exec_fee()).ok_or(HostError::IntegerOverflow)?, - ) - .ok_or(HostError::IntegerOverflow)?; - self.update_profile_action(action, burn_gas); - self.deduct_gas(burn_gas, use_gas) - } - - /// A helper function to pay base cost gas fee for batching an action. - /// # Args: - /// * `base_fee`: base fee for the action; - /// * `sir`: whether the receiver_id is same as the current account ID; - /// * `action`: what kind of action is charged for; - pub fn pay_action_base( - &mut self, - base_fee: &Fee, - sir: bool, - action: ActionCosts, - ) -> Result<()> { - let burn_gas = base_fee.send_fee(sir); - let use_gas = - burn_gas.checked_add(base_fee.exec_fee()).ok_or(HostError::IntegerOverflow)?; - self.update_profile_action(action, burn_gas); - self.deduct_gas(burn_gas, use_gas) - } - /// A helper function to pay base cost gas fee for batching an action. /// # Args: /// * `burn_gas`: amount of gas to burn; diff --git a/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs index c7427e2c4da..f545ebc7015 100644 --- a/runtime/near-vm-logic/src/logic.rs +++ b/runtime/near-vm-logic/src/logic.rs @@ -9,13 +9,12 @@ use byteorder::ByteOrder; use near_crypto::Secp256K1Signature; use near_primitives::checked_feature; use near_primitives::config::ViewConfig; +use near_primitives::runtime::fees::RuntimeFeesConfig; use near_primitives::version::is_implicit_account_creation_enabled; use near_primitives_core::config::ExtCosts::*; use near_primitives_core::config::{ActionCosts, ExtCosts, VMConfig}; use near_primitives_core::profile::ProfileData; -use near_primitives_core::runtime::fees::{ - transfer_exec_fee, transfer_send_fee, RuntimeFeesConfig, -}; +use near_primitives_core::runtime::fees::{transfer_exec_fee, transfer_send_fee}; use near_primitives_core::types::{ AccountId, Balance, EpochHeight, Gas, ProtocolVersion, StorageUsage, }; @@ -33,7 +32,7 @@ pub struct VMLogic<'a> { ext: &'a mut dyn External, /// Part of Context API and Economics API that was extracted from the receipt. context: VMContext, - /// Parameters of Wasm and economic parameters. + /// All gas and economic parameters required during contract execution. config: &'a VMConfig, /// Fees for creating (async) actions on runtime. fees_config: &'a RuntimeFeesConfig, @@ -1224,19 +1223,16 @@ impl<'a> VMLogic<'a> { /// pay for the content transmitted through the dependency upon the actual creation of the /// DataReceipt. fn pay_gas_for_new_receipt(&mut self, sir: bool, data_dependencies: &[bool]) -> Result<()> { - let fees_config_cfg = &self.fees_config; - self.gas_counter.pay_action_base( - &fees_config_cfg.action_receipt_creation_config, - sir, - ActionCosts::new_action_receipt, - )?; + self.pay_action_base(ActionCosts::new_action_receipt, sir)?; let mut burn_gas = 0u64; for dep in data_dependencies { // Both creation and execution for data receipts are considered burnt gas. burn_gas = burn_gas - .checked_add(fees_config_cfg.data_receipt_creation_config.base_cost.send_fee(*dep)) + .checked_add( + self.fees_config.fee(ActionCosts::new_data_receipt_base).send_fee(*dep), + ) .ok_or(HostError::IntegerOverflow)? - .checked_add(fees_config_cfg.data_receipt_creation_config.base_cost.exec_fee()) + .checked_add(self.fees_config.fee(ActionCosts::new_data_receipt_base).exec_fee()) .ok_or(HostError::IntegerOverflow)?; } self.gas_counter.pay_action_accumulated( @@ -1564,11 +1560,7 @@ impl<'a> VMLogic<'a> { } let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; - self.gas_counter.pay_action_base( - &self.fees_config.action_creation_config.create_account_cost, - sir, - ActionCosts::create_account, - )?; + self.pay_action_base(ActionCosts::create_account, sir)?; self.receipt_manager.append_action_create_account(receipt_idx)?; Ok(()) @@ -1616,17 +1608,8 @@ impl<'a> VMLogic<'a> { let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; let num_bytes = code.len() as u64; - self.gas_counter.pay_action_base( - &self.fees_config.action_creation_config.deploy_contract_cost, - sir, - ActionCosts::deploy_contract_base, - )?; - self.gas_counter.pay_action_per_byte( - &self.fees_config.action_creation_config.deploy_contract_cost_per_byte, - num_bytes, - sir, - ActionCosts::deploy_contract_byte, - )?; + self.pay_action_base(ActionCosts::deploy_contract_base, sir)?; + self.pay_action_per_byte(ActionCosts::deploy_contract_byte, num_bytes, sir)?; self.receipt_manager.append_action_deploy_contract(receipt_idx, code)?; Ok(()) @@ -1737,17 +1720,8 @@ impl<'a> VMLogic<'a> { // Input can't be large enough to overflow let num_bytes = method_name.len() as u64 + arguments.len() as u64; - self.gas_counter.pay_action_base( - &self.fees_config.action_creation_config.function_call_cost, - sir, - ActionCosts::function_call_base, - )?; - self.gas_counter.pay_action_per_byte( - &self.fees_config.action_creation_config.function_call_cost_per_byte, - num_bytes, - sir, - ActionCosts::function_call_byte, - )?; + self.pay_action_base(ActionCosts::function_call_base, sir)?; + self.pay_action_per_byte(ActionCosts::function_call_byte, num_bytes, sir)?; // Prepaid gas self.gas_counter.prepay_gas(gas)?; @@ -1799,10 +1773,8 @@ impl<'a> VMLogic<'a> { is_implicit_account_creation_enabled(self.current_protocol_version) && receiver_id.is_implicit(); - let send_fee = - transfer_send_fee(&self.fees_config.action_creation_config, sir, is_receiver_implicit); - let exec_fee = - transfer_exec_fee(&self.fees_config.action_creation_config, is_receiver_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; let use_gas = burn_gas.checked_add(exec_fee).ok_or(HostError::IntegerOverflow)?; self.gas_counter.pay_action_accumulated(burn_gas, use_gas, ActionCosts::transfer)?; @@ -1848,13 +1820,7 @@ impl<'a> VMLogic<'a> { let public_key = self.get_vec_from_memory_or_register(public_key_ptr, public_key_len)?; let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; - - self.gas_counter.pay_action_base( - &self.fees_config.action_creation_config.stake_cost, - sir, - ActionCosts::stake, - )?; - + self.pay_action_base(ActionCosts::stake, sir)?; self.receipt_manager.append_action_stake(receipt_idx, amount, public_key)?; Ok(()) } @@ -1893,13 +1859,7 @@ impl<'a> VMLogic<'a> { let public_key = self.get_vec_from_memory_or_register(public_key_ptr, public_key_len)?; let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; - - self.gas_counter.pay_action_base( - &self.fees_config.action_creation_config.add_key_cost.full_access_cost, - sir, - ActionCosts::add_full_access_key, - )?; - + self.pay_action_base(ActionCosts::add_full_access_key, sir)?; self.receipt_manager.append_action_add_key_with_full_access( receipt_idx, public_key, @@ -1958,17 +1918,8 @@ impl<'a> VMLogic<'a> { // +1 is to account for null-terminating characters. let num_bytes = method_names.iter().map(|v| v.len() as u64 + 1).sum::(); - self.gas_counter.pay_action_base( - &self.fees_config.action_creation_config.add_key_cost.function_call_cost, - sir, - ActionCosts::add_function_call_key_base, - )?; - self.gas_counter.pay_action_per_byte( - &self.fees_config.action_creation_config.add_key_cost.function_call_cost_per_byte, - num_bytes, - sir, - ActionCosts::add_function_call_key_byte, - )?; + 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( receipt_idx, @@ -2014,13 +1965,7 @@ impl<'a> VMLogic<'a> { let public_key = self.get_vec_from_memory_or_register(public_key_ptr, public_key_len)?; let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; - - self.gas_counter.pay_action_base( - &self.fees_config.action_creation_config.delete_key_cost, - sir, - ActionCosts::delete_key, - )?; - + self.pay_action_base(ActionCosts::delete_key, sir)?; self.receipt_manager.append_action_delete_key(receipt_idx, public_key)?; Ok(()) } @@ -2058,11 +2003,7 @@ impl<'a> VMLogic<'a> { self.read_and_parse_account_id(beneficiary_id_ptr, beneficiary_id_len)?; let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; - self.gas_counter.pay_action_base( - &self.fees_config.action_creation_config.delete_account_cost, - sir, - ActionCosts::delete_account, - )?; + self.pay_action_base(ActionCosts::delete_account, sir)?; self.receipt_manager.append_action_delete_account(receipt_idx, beneficiary_id)?; Ok(()) @@ -2195,7 +2136,6 @@ impl<'a> VMLogic<'a> { } .into()); } - let data_cfg = &self.fees_config.data_receipt_creation_config; for data_receiver in &self.context.output_data_receivers { let sir = data_receiver == &self.context.current_account_id; // We deduct for execution here too, because if we later have an OR combinator @@ -2207,10 +2147,12 @@ impl<'a> VMLogic<'a> { // The gas here is considered burnt, cause we'll prepay for it upfront. burn_gas = burn_gas .checked_add( - data_cfg - .cost_per_byte + self.fees_config + .fee(ActionCosts::new_data_receipt_byte) .send_fee(sir) - .checked_add(data_cfg.cost_per_byte.exec_fee()) + .checked_add( + self.fees_config.fee(ActionCosts::new_data_receipt_byte).exec_fee(), + ) .ok_or(HostError::IntegerOverflow)? .checked_mul(num_bytes) .ok_or(HostError::IntegerOverflow)?, @@ -2825,6 +2767,33 @@ impl<'a> VMLogic<'a> { self.gas_counter.process_gas_limit(new_burn_gas, new_used_gas) } + /// A helper function to pay base cost gas fee for batching an action. + pub fn pay_action_base(&mut self, action: ActionCosts, sir: bool) -> Result<()> { + let base_fee = self.fees_config.fee(action); + let burn_gas = base_fee.send_fee(sir); + let use_gas = + burn_gas.checked_add(base_fee.exec_fee()).ok_or(HostError::IntegerOverflow)?; + self.gas_counter.pay_action_accumulated(burn_gas, use_gas, action) + } + + /// A helper function to pay per byte gas fee for batching an action. + pub fn pay_action_per_byte( + &mut self, + action: ActionCosts, + num_bytes: u64, + sir: bool, + ) -> Result<()> { + let per_byte_fee = self.fees_config.fee(action); + let burn_gas = + num_bytes.checked_mul(per_byte_fee.send_fee(sir)).ok_or(HostError::IntegerOverflow)?; + let use_gas = burn_gas + .checked_add( + num_bytes.checked_mul(per_byte_fee.exec_fee()).ok_or(HostError::IntegerOverflow)?, + ) + .ok_or(HostError::IntegerOverflow)?; + self.gas_counter.pay_action_accumulated(burn_gas, use_gas, action) + } + /// VM independent setup before loading the executable. /// /// Does VM independent checks that happen after the instantiation of diff --git a/runtime/runtime-params-estimator/Cargo.toml b/runtime/runtime-params-estimator/Cargo.toml index 2a598fed4f0..368ec24d863 100644 --- a/runtime/runtime-params-estimator/Cargo.toml +++ b/runtime/runtime-params-estimator/Cargo.toml @@ -17,6 +17,7 @@ bytesize.workspace = true cfg-if.workspace = true chrono.workspace = true clap.workspace = true +enum-map.workspace = true hex.workspace = true indicatif.workspace = true libc.workspace = true 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 f6b65d0241d..3318afd4a98 100644 --- a/runtime/runtime-params-estimator/src/costs_to_runtime_config.rs +++ b/runtime/runtime-params-estimator/src/costs_to_runtime_config.rs @@ -1,12 +1,9 @@ use near_primitives::runtime::config::AccountCreationConfig; use near_primitives::runtime::config_store::RuntimeConfigStore; -use near_primitives::runtime::fees::{ - AccessKeyCreationConfig, ActionCreationConfig, DataReceiptCreationConfig, Fee, - RuntimeFeesConfig, -}; +use near_primitives::runtime::fees::{Fee, RuntimeFeesConfig}; use near_primitives::types::Gas; use near_primitives::version::PROTOCOL_VERSION; -use near_vm_logic::{ExtCostsConfig, VMConfig}; +use near_vm_logic::{ActionCosts, ExtCostsConfig, VMConfig}; use node_runtime::config::RuntimeConfig; use anyhow::Context; @@ -33,9 +30,7 @@ pub fn costs_to_runtime_config(cost_table: &CostTable) -> anyhow::Result anyhow::Result fee(Cost::ActionCreateAccount)?, + ActionCosts::delete_account => fee(Cost::ActionDeleteAccount)?, + ActionCosts::deploy_contract_base => fee(Cost::ActionDeployContractBase)?, + ActionCosts::deploy_contract_byte => fee(Cost::ActionDeployContractPerByte)?, + ActionCosts::function_call_base => fee(Cost::ActionFunctionCallBase)?, + ActionCosts::function_call_byte => fee(Cost::ActionFunctionCallPerByte)?, + ActionCosts::transfer => fee(Cost::ActionTransfer)?, + ActionCosts::stake => fee(Cost::ActionStake)?, + ActionCosts::add_full_access_key => fee(Cost::ActionAddFullAccessKey)?, + ActionCosts::add_function_call_key_base => fee(Cost::ActionAddFunctionAccessKeyBase)?, + ActionCosts::add_function_call_key_byte => fee(Cost::ActionAddFunctionAccessKeyPerByte)?, + ActionCosts::delete_key => fee(Cost::ActionDeleteKey)?, + ActionCosts::new_action_receipt => fee(Cost::ActionReceiptCreation)?, + ActionCosts::new_data_receipt_base => fee(Cost::DataReceiptCreationBase)?, + ActionCosts::new_data_receipt_byte => fee(Cost::DataReceiptCreationPerByte)?, }, ..actual_fees_config.clone() }; diff --git a/runtime/runtime-params-estimator/src/function_call.rs b/runtime/runtime-params-estimator/src/function_call.rs index 89a53a5b6ef..65d2ae1c981 100644 --- a/runtime/runtime-params-estimator/src/function_call.rs +++ b/runtime/runtime-params-estimator/src/function_call.rs @@ -70,7 +70,7 @@ fn compute_function_call_cost( let runtime_config = config_store.get_config(protocol_version).as_ref(); let vm_config = runtime_config.wasm_config.clone(); let runtime = vm_kind.runtime(vm_config).expect("runtime has not been enabled"); - let fees = runtime_config.transaction_costs.clone(); + let fees = runtime_config.fees.clone(); let mut fake_external = MockedExternal::new(); let fake_context = create_context(vec![]); let promise_results = vec![]; diff --git a/runtime/runtime-params-estimator/src/gas_metering.rs b/runtime/runtime-params-estimator/src/gas_metering.rs index f538e1c172a..a7fafac897f 100644 --- a/runtime/runtime-params-estimator/src/gas_metering.rs +++ b/runtime/runtime-params-estimator/src/gas_metering.rs @@ -135,7 +135,7 @@ pub(crate) fn compute_gas_metering_cost(config: &Config, contract: &ContractCode let vm_config_gas = runtime_config.wasm_config.clone(); let runtime = vm_kind.runtime(vm_config_gas).expect("runtime has not been enabled"); let runtime_free_gas = vm_kind.runtime(VMConfig::free()).expect("runtime has not been enabled"); - let fees = runtime_config.transaction_costs.clone(); + let fees = runtime_config.fees.clone(); let mut fake_external = MockedExternal::new(); let fake_context = create_context(vec![]); let promise_results = vec![]; diff --git a/runtime/runtime-params-estimator/src/main.rs b/runtime/runtime-params-estimator/src/main.rs index 67cda33f806..342e5b36a8f 100644 --- a/runtime/runtime-params-estimator/src/main.rs +++ b/runtime/runtime-params-estimator/src/main.rs @@ -5,6 +5,7 @@ use clap::Parser; use genesis_populate::GenesisBuilder; use near_chain_configs::GenesisValidationMode; use near_primitives::version::PROTOCOL_VERSION; +use near_primitives::views::RuntimeConfigView; use near_vm_runner::internal::VMKind; use replay::ReplayCmd; use runtime_params_estimator::config::{Config, GasMetric}; @@ -199,7 +200,8 @@ fn main() -> anyhow::Result<()> { println!("Generated RuntimeConfig:\n"); println!("{:#?}", runtime_config); - let str = serde_json::to_string_pretty(&runtime_config) + let config_view = RuntimeConfigView::from(runtime_config); + let str = serde_json::to_string_pretty(&config_view) .expect("Failed serializing the runtime config"); let output_path = state_dump_path.join("runtime_config.json"); diff --git a/runtime/runtime/Cargo.toml b/runtime/runtime/Cargo.toml index 326fd7cece9..08f6cd99f64 100644 --- a/runtime/runtime/Cargo.toml +++ b/runtime/runtime/Cargo.toml @@ -46,6 +46,7 @@ sandbox = ["near-vm-logic/sandbox", "near-vm-runner/sandbox"] [dev-dependencies] assert_matches.workspace = true +enum-map.workspace = true indicatif.workspace = true rayon.workspace = true serde_json.workspace = true diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 25fd563ab57..59c93618da3 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -110,7 +110,7 @@ pub(crate) fn execute_function_call( runtime_ext, context, &config.wasm_config, - &config.transaction_costs, + &config.fees, promise_results, apply_state.current_protocol_version, apply_state.cache.as_deref(), @@ -605,7 +605,7 @@ pub(crate) fn action_add_key( &add_key.access_key, ); }; - let storage_config = &apply_state.config.transaction_costs.storage_usage_config; + let storage_config = &apply_state.config.fees.storage_usage_config; account.set_storage_usage( account .storage_usage() diff --git a/runtime/runtime/src/balance_checker.rs b/runtime/runtime/src/balance_checker.rs index 5c6cbfcf8e7..ca6feb59918 100644 --- a/runtime/runtime/src/balance_checker.rs +++ b/runtime/runtime/src/balance_checker.rs @@ -15,6 +15,7 @@ use near_primitives::trie_key::TrieKey; use near_primitives::types::{AccountId, Balance}; use near_primitives::version::ProtocolVersion; use near_store::{get, get_account, get_postponed_receipt, TrieAccess, TrieUpdate}; +use near_vm_logic::ActionCosts; use std::collections::HashSet; /// Returns delayed receipts with given range of indices. @@ -45,7 +46,7 @@ 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.action_receipt_creation_config.exec_fee(), + transaction_costs.fee(ActionCosts::new_action_receipt).exec_fee(), total_prepaid_exec_fees( transaction_costs, &action_receipt.actions, @@ -384,10 +385,10 @@ mod tests { let deposit = 500_000_000; let gas_price = 100; let cfg = RuntimeFeesConfig::test(); - let exec_gas = cfg.action_receipt_creation_config.exec_fee() - + cfg.action_creation_config.transfer_cost.exec_fee(); - let send_gas = cfg.action_receipt_creation_config.send_fee(false) - + cfg.action_creation_config.transfer_cost.send_fee(false); + 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 total_validator_reward = send_gas as Balance * gas_price - contract_reward; diff --git a/runtime/runtime/src/config.rs b/runtime/runtime/src/config.rs index ee133a8788b..a6e516e51c3 100644 --- a/runtime/runtime/src/config.rs +++ b/runtime/runtime/src/config.rs @@ -1,5 +1,6 @@ //! Settings of the parameters of the runtime. +use near_vm_logic::ActionCosts; use num_bigint::BigUint; use num_traits::cast::ToPrimitive; use num_traits::pow::Pow; @@ -74,31 +75,34 @@ pub fn total_send_fees( receiver_id: &AccountId, current_protocol_version: ProtocolVersion, ) -> Result { - let cfg = &config.action_creation_config; let mut result = 0; for action in actions { use Action::*; let delta = match action { - CreateAccount(_) => cfg.create_account_cost.send_fee(sender_is_receiver), + CreateAccount(_) => { + config.fee(ActionCosts::create_account).send_fee(sender_is_receiver) + } DeployContract(DeployContractAction { code }) => { let num_bytes = code.len() as u64; - cfg.deploy_contract_cost.send_fee(sender_is_receiver) - + cfg.deploy_contract_cost_per_byte.send_fee(sender_is_receiver) * num_bytes + config.fee(ActionCosts::deploy_contract_base).send_fee(sender_is_receiver) + + config.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; - cfg.function_call_cost.send_fee(sender_is_receiver) - + cfg.function_call_cost_per_byte.send_fee(sender_is_receiver) * num_bytes + config.fee(ActionCosts::function_call_base).send_fee(sender_is_receiver) + + config.fee(ActionCosts::function_call_byte).send_fee(sender_is_receiver) + * num_bytes } Transfer(_) => { // Account for implicit account creation let is_receiver_implicit = is_implicit_account_creation_enabled(current_protocol_version) && receiver_id.is_implicit(); - transfer_send_fee(cfg, sender_is_receiver, is_receiver_implicit) + transfer_send_fee(config, sender_is_receiver, is_receiver_implicit) } - Stake(_) => cfg.stake_cost.send_fee(sender_is_receiver), + Stake(_) => config.fee(ActionCosts::stake).send_fee(sender_is_receiver), AddKey(AddKeyAction { access_key, .. }) => match &access_key.permission { AccessKeyPermission::FunctionCall(call_perm) => { let num_bytes = call_perm @@ -107,19 +111,20 @@ pub fn total_send_fees( // Account for null-terminating characters. .map(|name| name.as_bytes().len() as u64 + 1) .sum::(); - cfg.add_key_cost.function_call_cost.send_fee(sender_is_receiver) + config.fee(ActionCosts::add_function_call_key_base).send_fee(sender_is_receiver) + num_bytes - * cfg - .add_key_cost - .function_call_cost_per_byte + * config + .fee(ActionCosts::add_function_call_key_byte) .send_fee(sender_is_receiver) } AccessKeyPermission::FullAccess => { - cfg.add_key_cost.full_access_cost.send_fee(sender_is_receiver) + config.fee(ActionCosts::add_full_access_key).send_fee(sender_is_receiver) } }, - DeleteKey(_) => cfg.delete_key_cost.send_fee(sender_is_receiver), - DeleteAccount(_) => cfg.delete_account_cost.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) + } }; result = safe_add_gas(result, delta)?; } @@ -132,29 +137,28 @@ pub fn exec_fee( receiver_id: &AccountId, current_protocol_version: ProtocolVersion, ) -> Gas { - let cfg = &config.action_creation_config; use Action::*; match action { - CreateAccount(_) => cfg.create_account_cost.exec_fee(), + CreateAccount(_) => config.fee(ActionCosts::create_account).exec_fee(), DeployContract(DeployContractAction { code }) => { let num_bytes = code.len() as u64; - cfg.deploy_contract_cost.exec_fee() - + cfg.deploy_contract_cost_per_byte.exec_fee() * num_bytes + config.fee(ActionCosts::deploy_contract_base).exec_fee() + + config.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; - cfg.function_call_cost.exec_fee() - + cfg.function_call_cost_per_byte.exec_fee() * num_bytes + config.fee(ActionCosts::function_call_base).exec_fee() + + config.fee(ActionCosts::function_call_byte).exec_fee() * num_bytes } Transfer(_) => { // Account for implicit account creation let is_receiver_implicit = is_implicit_account_creation_enabled(current_protocol_version) && receiver_id.is_implicit(); - transfer_exec_fee(cfg, is_receiver_implicit) + transfer_exec_fee(config, is_receiver_implicit) } - Stake(_) => cfg.stake_cost.exec_fee(), + Stake(_) => config.fee(ActionCosts::stake).exec_fee(), AddKey(AddKeyAction { access_key, .. }) => match &access_key.permission { AccessKeyPermission::FunctionCall(call_perm) => { let num_bytes = call_perm @@ -163,13 +167,15 @@ pub fn exec_fee( // Account for null-terminating characters. .map(|name| name.as_bytes().len() as u64 + 1) .sum::(); - cfg.add_key_cost.function_call_cost.exec_fee() - + num_bytes * cfg.add_key_cost.function_call_cost_per_byte.exec_fee() + config.fee(ActionCosts::add_function_call_key_base).exec_fee() + + num_bytes * config.fee(ActionCosts::add_function_call_key_byte).exec_fee() + } + AccessKeyPermission::FullAccess => { + config.fee(ActionCosts::add_full_access_key).exec_fee() } - AccessKeyPermission::FullAccess => cfg.add_key_cost.full_access_cost.exec_fee(), }, - DeleteKey(_) => cfg.delete_key_cost.exec_fee(), - DeleteAccount(_) => cfg.delete_account_cost.exec_fee(), + DeleteKey(_) => config.fee(ActionCosts::delete_key).exec_fee(), + DeleteAccount(_) => config.fee(ActionCosts::delete_account).exec_fee(), } } @@ -181,7 +187,8 @@ pub fn tx_cost( sender_is_receiver: bool, current_protocol_version: ProtocolVersion, ) -> Result { - let mut gas_burnt: Gas = config.action_receipt_creation_config.send_fee(sender_is_receiver); + let mut gas_burnt: Gas = + config.fee(ActionCosts::new_action_receipt).send_fee(sender_is_receiver); gas_burnt = safe_add_gas( gas_burnt, total_send_fees( @@ -213,7 +220,7 @@ pub fn tx_cost( }; let mut gas_remaining = - safe_add_gas(prepaid_gas, config.action_receipt_creation_config.exec_fee())?; + safe_add_gas(prepaid_gas, config.fee(ActionCosts::new_action_receipt).exec_fee())?; gas_remaining = safe_add_gas( gas_remaining, total_prepaid_exec_fees( diff --git a/runtime/runtime/src/genesis.rs b/runtime/runtime/src/genesis.rs index b1918d58bd1..7df1a6765c0 100644 --- a/runtime/runtime/src/genesis.rs +++ b/runtime/runtime/src/genesis.rs @@ -35,7 +35,7 @@ pub struct StorageComputer<'a> { impl<'a> StorageComputer<'a> { pub fn new(config: &'a RuntimeConfig) -> Self { - Self { result: HashMap::new(), config: &config.transaction_costs.storage_usage_config } + Self { result: HashMap::new(), config: &config.fees.storage_usage_config } } /// Updates user's storage info based on the StateRecord. diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index 2eec9cbe1a7..46411939900 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -49,7 +49,7 @@ use near_store::{ }; use near_store::{set_access_key, set_code}; use near_vm_logic::types::PromiseResult; -use near_vm_logic::ReturnData; +use near_vm_logic::{ActionCosts, ReturnData}; pub use near_vm_runner::with_ext_cost_counter; use crate::actions::*; @@ -303,7 +303,7 @@ impl Runtime { // println!("enter apply_action"); let mut result = ActionResult::default(); let exec_fees = exec_fee( - &apply_state.config.transaction_costs, + &apply_state.config.fees, action, &receipt.receiver_id, apply_state.current_protocol_version, @@ -334,7 +334,7 @@ impl Runtime { match action { Action::CreateAccount(_) => { action_create_account( - &apply_state.config.transaction_costs, + &apply_state.config.fees, &apply_state.config.account_creation_config, account, actor_id, @@ -390,7 +390,7 @@ impl Runtime { debug_assert!(!is_refund); action_implicit_account_creation_transfer( state_update, - &apply_state.config.transaction_costs, + &apply_state.config.fees, account, actor_id, &receipt.receiver_id, @@ -422,7 +422,7 @@ impl Runtime { } Action::DeleteKey(delete_key) => { action_delete_key( - &apply_state.config.transaction_costs, + &apply_state.config.fees, state_update, account.as_mut().expect(EXPECT_ACCOUNT_EXISTS), &mut result, @@ -494,8 +494,7 @@ impl Runtime { let mut account = get_account(state_update, account_id)?; let mut actor_id = receipt.predecessor_id.clone(); let mut result = ActionResult::default(); - let exec_fee = - apply_state.config.transaction_costs.action_receipt_creation_config.exec_fee(); + let exec_fee = apply_state.config.fees.fee(ActionCosts::new_action_receipt).exec_fee(); result.gas_used = exec_fee; result.gas_burnt = exec_fee; // Executing actions one by one @@ -587,7 +586,7 @@ impl Runtime { action_receipt, &mut result, apply_state.current_protocol_version, - &apply_state.config.transaction_costs, + &apply_state.config.fees, )? }; stats.gas_deficit_amount = safe_add_balance(stats.gas_deficit_amount, gas_deficit_amount)?; @@ -619,8 +618,8 @@ impl Runtime { // Adding burnt gas reward for function calls if the account exists. let receiver_gas_reward = result.gas_burnt_for_function_call - * *apply_state.config.transaction_costs.burnt_gas_reward.numer() as u64 - / *apply_state.config.transaction_costs.burnt_gas_reward.denom() as u64; + * *apply_state.config.fees.burnt_gas_reward.numer() as u64 + / *apply_state.config.fees.burnt_gas_reward.denom() as u64; // The balance that the current account should receive as a reward for function call // execution. let receiver_reward = safe_gas_to_balance(apply_state.gas_price, receiver_gas_reward)? @@ -756,7 +755,7 @@ impl Runtime { &receipt.receiver_id, current_protocol_version, )?, - transaction_costs.action_receipt_creation_config.exec_fee(), + transaction_costs.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() { @@ -1361,7 +1360,7 @@ impl Runtime { } check_balance( - &apply_state.config.transaction_costs, + &apply_state.config.fees, &state_update, validator_accounts_update, incoming_receipts, @@ -1771,12 +1770,9 @@ mod tests { let (runtime, tries, mut root, mut apply_state, _, epoch_info_provider) = setup_runtime(initial_balance, initial_locked, 1); - let receipt_gas_cost = apply_state - .config - .transaction_costs - .action_receipt_creation_config - .exec_fee() - + apply_state.config.transaction_costs.action_creation_config.transfer_cost.exec_fee(); + let receipt_gas_cost = + apply_state.config.fees.fee(ActionCosts::new_action_receipt).exec_fee() + + apply_state.config.fees.fee(ActionCosts::transfer).exec_fee(); apply_state.gas_limit = Some(receipt_gas_cost * 3); let n = 40; @@ -1825,12 +1821,9 @@ mod tests { let (runtime, tries, mut root, mut apply_state, _, epoch_info_provider) = setup_runtime(initial_balance, initial_locked, 1); - let receipt_gas_cost = apply_state - .config - .transaction_costs - .action_receipt_creation_config - .exec_fee() - + apply_state.config.transaction_costs.action_creation_config.transfer_cost.exec_fee(); + let receipt_gas_cost = + apply_state.config.fees.fee(ActionCosts::new_action_receipt).exec_fee() + + apply_state.config.fees.fee(ActionCosts::transfer).exec_fee(); let n = 120; let receipts = generate_receipts(small_transfer, n); @@ -1927,7 +1920,7 @@ mod tests { let receipt_exec_gas_fee = 1000; let mut free_config = RuntimeConfig::free(); - free_config.transaction_costs.action_receipt_creation_config.execution = + free_config.fees.action_fees[ActionCosts::new_action_receipt].execution = receipt_exec_gas_fee; apply_state.config = Arc::new(free_config); // This allows us to execute 3 receipts per apply. @@ -2209,9 +2202,9 @@ mod tests { })]; let expected_gas_burnt = safe_add_gas( - apply_state.config.transaction_costs.action_receipt_creation_config.exec_fee(), + apply_state.config.fees.fee(ActionCosts::new_action_receipt).exec_fee(), total_prepaid_exec_fees( - &apply_state.config.transaction_costs, + &apply_state.config.fees, &actions, &alice_account(), PROTOCOL_VERSION, @@ -2278,9 +2271,9 @@ mod tests { })]; let expected_gas_burnt = safe_add_gas( - apply_state.config.transaction_costs.action_receipt_creation_config.exec_fee(), + apply_state.config.fees.fee(ActionCosts::new_action_receipt).exec_fee(), total_prepaid_exec_fees( - &apply_state.config.transaction_costs, + &apply_state.config.fees, &actions, &alice_account(), PROTOCOL_VERSION, diff --git a/runtime/runtime/src/verifier.rs b/runtime/runtime/src/verifier.rs index 9fa6369b21c..309bfb2fee6 100644 --- a/runtime/runtime/src/verifier.rs +++ b/runtime/runtime/src/verifier.rs @@ -60,14 +60,8 @@ pub fn validate_transaction( let sender_is_receiver = &transaction.receiver_id == signer_id; - tx_cost( - &config.transaction_costs, - transaction, - gas_price, - sender_is_receiver, - current_protocol_version, - ) - .map_err(|_| InvalidTxError::CostOverflow.into()) + tx_cost(&config.fees, transaction, gas_price, sender_is_receiver, current_protocol_version) + .map_err(|_| InvalidTxError::CostOverflow.into()) } /// Verifies the signed transaction on top of given state, charges transaction fees @@ -871,7 +865,7 @@ mod tests { #[test] fn test_validate_transaction_invalid_low_balance() { let mut config = RuntimeConfig::free(); - config.storage_amount_per_byte = 10_000_000; + config.fees.storage_usage_config.storage_amount_per_byte = 10_000_000; let initial_balance = 1_000_000_000; let transfer_amount = 950_000_000; let (signer, mut state_update, gas_price) = @@ -898,7 +892,7 @@ mod tests { RuntimeError::InvalidTxError(InvalidTxError::LackBalanceForState { signer_id: alice_account(), amount: Balance::from(std::mem::size_of::() as u64) - * config.storage_amount_per_byte + * config.storage_amount_per_byte() - (initial_balance - transfer_amount) }) ); diff --git a/runtime/runtime/tests/runtime_group_tools/mod.rs b/runtime/runtime/tests/runtime_group_tools/mod.rs index 4af1f170cff..5f97e03c304 100644 --- a/runtime/runtime/tests/runtime_group_tools/mod.rs +++ b/runtime/runtime/tests/runtime_group_tools/mod.rs @@ -12,6 +12,7 @@ use near_primitives::types::{AccountId, AccountInfo, Balance}; use near_primitives::version::PROTOCOL_VERSION; use near_store::test_utils::create_tries; use near_store::ShardTries; +use near_vm_logic::ActionCosts; use node_runtime::{ApplyState, Runtime}; use random_config::random_config; use std::collections::{HashMap, HashSet}; @@ -53,9 +54,9 @@ impl StandaloneRuntime { let mut runtime_config = random_config(); // Bumping costs to avoid inflation overflows. runtime_config.wasm_config.limit_config.max_total_prepaid_gas = 10u64.pow(15); - runtime_config.transaction_costs.action_receipt_creation_config.execution = + runtime_config.fees.action_fees[ActionCosts::new_action_receipt].execution = runtime_config.wasm_config.limit_config.max_total_prepaid_gas / 64; - runtime_config.transaction_costs.data_receipt_creation_config.base_cost.execution = + runtime_config.fees.action_fees[ActionCosts::new_data_receipt_base].execution = runtime_config.wasm_config.limit_config.max_total_prepaid_gas / 64; let runtime = Runtime::new(); diff --git a/runtime/runtime/tests/runtime_group_tools/random_config.rs b/runtime/runtime/tests/runtime_group_tools/random_config.rs index be0b3838e54..7b35087111c 100644 --- a/runtime/runtime/tests/runtime_group_tools/random_config.rs +++ b/runtime/runtime/tests/runtime_group_tools/random_config.rs @@ -1,9 +1,6 @@ use near_primitives::num_rational::Rational; use near_primitives::runtime::config::RuntimeConfig; -use near_primitives::runtime::fees::{ - AccessKeyCreationConfig, ActionCreationConfig, DataReceiptCreationConfig, Fee, - RuntimeFeesConfig, StorageUsageConfig, -}; +use near_primitives::runtime::fees::{Fee, RuntimeFeesConfig, StorageUsageConfig}; use rand::{thread_rng, RngCore}; pub fn random_config() -> RuntimeConfig { @@ -14,31 +11,14 @@ pub fn random_config() -> RuntimeConfig { execution: rng.next_u64() % 1000, }; RuntimeConfig { - transaction_costs: RuntimeFeesConfig { - action_receipt_creation_config: random_fee(), - data_receipt_creation_config: DataReceiptCreationConfig { - base_cost: random_fee(), - cost_per_byte: random_fee(), - }, - action_creation_config: ActionCreationConfig { - create_account_cost: random_fee(), - deploy_contract_cost: random_fee(), - deploy_contract_cost_per_byte: random_fee(), - function_call_cost: random_fee(), - function_call_cost_per_byte: random_fee(), - transfer_cost: random_fee(), - stake_cost: random_fee(), - add_key_cost: AccessKeyCreationConfig { - full_access_cost: random_fee(), - function_call_cost: random_fee(), - function_call_cost_per_byte: random_fee(), - }, - delete_key_cost: random_fee(), - delete_account_cost: random_fee(), + fees: RuntimeFeesConfig { + action_fees: enum_map::enum_map! { + _ => random_fee(), }, storage_usage_config: StorageUsageConfig { num_bytes_account: rng.next_u64() % 10000, num_extra_bytes_record: rng.next_u64() % 10000, + storage_amount_per_byte: rng.next_u64() as u128, }, burnt_gas_reward: Rational::new((rng.next_u32() % 100).try_into().unwrap(), 100), pessimistic_gas_price_inflation_ratio: Rational::new( @@ -52,5 +32,5 @@ pub fn random_config() -> RuntimeConfig { #[test] fn test_random_fees() { - assert_ne!(random_config().transaction_costs, random_config().transaction_costs); + assert_ne!(random_config().fees, random_config().fees); } diff --git a/test-utils/testlib/src/fees_utils.rs b/test-utils/testlib/src/fees_utils.rs index 17306662cc6..cb8bee0ad63 100644 --- a/test-utils/testlib/src/fees_utils.rs +++ b/test-utils/testlib/src/fees_utils.rs @@ -1,5 +1,6 @@ //! 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::fees::RuntimeFeesConfig; use near_primitives::types::{Balance, Gas}; @@ -30,22 +31,22 @@ impl FeeHelper { } pub fn create_account_cost(&self) -> Balance { - let exec_gas = self.cfg.action_receipt_creation_config.exec_fee() - + self.cfg.action_creation_config.create_account_cost.exec_fee(); - let send_gas = self.cfg.action_receipt_creation_config.send_fee(false) - + self.cfg.action_creation_config.create_account_cost.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.action_receipt_creation_config.exec_fee() - + self.cfg.action_creation_config.create_account_cost.exec_fee() - + self.cfg.action_creation_config.transfer_cost.exec_fee() - + self.cfg.action_creation_config.add_key_cost.full_access_cost.exec_fee(); - let send_gas = self.cfg.action_receipt_creation_config.send_fee(false) - + self.cfg.action_creation_config.create_account_cost.send_fee(false) - + self.cfg.action_creation_config.transfer_cost.send_fee(false) - + self.cfg.action_creation_config.add_key_cost.full_access_cost.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 } @@ -54,58 +55,56 @@ impl FeeHelper { } pub fn create_account_transfer_full_key_cost_no_reward(&self) -> Balance { - let exec_gas = self.cfg.action_receipt_creation_config.exec_fee() - + self.cfg.action_creation_config.create_account_cost.exec_fee() - + self.cfg.action_creation_config.transfer_cost.exec_fee() - + self.cfg.action_creation_config.add_key_cost.full_access_cost.exec_fee(); - let send_gas = self.cfg.action_receipt_creation_config.send_fee(false) - + self.cfg.action_creation_config.create_account_cost.send_fee(false) - + self.cfg.action_creation_config.transfer_cost.send_fee(false) - + self.cfg.action_creation_config.add_key_cost.full_access_cost.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.action_receipt_creation_config.exec_fee() - + self.cfg.action_creation_config.create_account_cost.exec_fee(); - let send_gas = self.cfg.action_receipt_creation_config.send_fee(false) - + self.cfg.action_creation_config.create_account_cost.send_fee(false) - + self.cfg.action_creation_config.transfer_cost.send_fee(false) - + self.cfg.action_creation_config.add_key_cost.full_access_cost.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.action_receipt_creation_config.exec_fee() - + self.cfg.action_creation_config.deploy_contract_cost.exec_fee() - + num_bytes * self.cfg.action_creation_config.deploy_contract_cost_per_byte.exec_fee(); - let send_gas = self.cfg.action_receipt_creation_config.send_fee(true) - + self.cfg.action_creation_config.deploy_contract_cost.send_fee(true) - + num_bytes - * self.cfg.action_creation_config.deploy_contract_cost_per_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.action_receipt_creation_config.exec_fee() - + self.cfg.action_creation_config.function_call_cost.exec_fee() - + num_bytes * self.cfg.action_creation_config.function_call_cost_per_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.action_receipt_creation_config.send_fee(false) - + self.cfg.action_creation_config.function_call_cost.send_fee(false) - + num_bytes - * self.cfg.action_creation_config.function_call_cost_per_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.action_receipt_creation_config.exec_fee() - + self.cfg.action_creation_config.transfer_cost.exec_fee(); - let send_gas = self.cfg.action_receipt_creation_config.send_fee(false) - + self.cfg.action_creation_config.transfer_cost.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 } @@ -118,56 +117,44 @@ impl FeeHelper { } pub fn stake_cost(&self) -> Balance { - let exec_gas = self.cfg.action_receipt_creation_config.exec_fee() - + self.cfg.action_creation_config.stake_cost.exec_fee(); - let send_gas = self.cfg.action_receipt_creation_config.send_fee(true) - + self.cfg.action_creation_config.stake_cost.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.action_receipt_creation_config.exec_fee() - + self.cfg.action_creation_config.add_key_cost.function_call_cost.exec_fee() - + num_bytes - * self - .cfg - .action_creation_config - .add_key_cost - .function_call_cost_per_byte - .exec_fee(); - let send_gas = self.cfg.action_receipt_creation_config.send_fee(true) - + self.cfg.action_creation_config.add_key_cost.function_call_cost.send_fee(true) - + num_bytes - * self - .cfg - .action_creation_config - .add_key_cost - .function_call_cost_per_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.action_receipt_creation_config.exec_fee() - + self.cfg.action_creation_config.add_key_cost.full_access_cost.exec_fee(); - let send_gas = self.cfg.action_receipt_creation_config.send_fee(true) - + self.cfg.action_creation_config.add_key_cost.full_access_cost.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.action_receipt_creation_config.exec_fee() - + self.cfg.action_creation_config.delete_key_cost.exec_fee(); - let send_gas = self.cfg.action_receipt_creation_config.send_fee(true) - + self.cfg.action_creation_config.delete_key_cost.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.action_receipt_creation_config.exec_fee() - + self.cfg.action_creation_config.delete_account_cost.exec_fee(); - let send_gas = self.cfg.action_receipt_creation_config.send_fee(false) - + self.cfg.action_creation_config.delete_account_cost.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; From dff8d6f6ad7dc9610f57098af45c3f2e706b319f Mon Sep 17 00:00:00 2001 From: pompon0 Date: Thu, 24 Nov 2022 15:30:53 +0100 Subject: [PATCH 030/188] Added an early exit to add_edges. (#8112) In case no new edges have been added to graph, we can skip recomputation of the routing table. --- .../src/peer_manager/network_state/routing.rs | 13 +++++++++---- chain/network/src/routing/graph/mod.rs | 3 +++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/chain/network/src/peer_manager/network_state/routing.rs b/chain/network/src/peer_manager/network_state/routing.rs index 8acd41fa860..9e70e5ca6a2 100644 --- a/chain/network/src/peer_manager/network_state/routing.rs +++ b/chain/network/src/peer_manager/network_state/routing.rs @@ -77,6 +77,14 @@ impl NetworkState { // instead, however that would be backward incompatible, so it can be introduced in // PROTOCOL_VERSION 60 earliest. let (edges, ok) = self.graph.verify(edges).await; + let result = match ok { + true => Ok(()), + false => Err(ReasonForBan::InvalidEdge), + }; + // Skip recomputation if no new edges have been verified. + if edges.len() == 0 { + return result; + } let this = self.clone(); let clock = clock.clone(); let _ = self @@ -107,9 +115,6 @@ impl NetworkState { // node. The node would then validate all the edges every time, then reject the whole set // because just the last edge was invalid. Instead, we accept all the edges verified so // far and return an error only afterwards. - match ok { - true => Ok(()), - false => Err(ReasonForBan::InvalidEdge), - } + result } } diff --git a/chain/network/src/routing/graph/mod.rs b/chain/network/src/routing/graph/mod.rs index 57eeb6be9f6..663af27cdad 100644 --- a/chain/network/src/routing/graph/mod.rs +++ b/chain/network/src/routing/graph/mod.rs @@ -272,6 +272,9 @@ impl Graph { self.unreliable_peers.store(Arc::new(unreliable_peers)); } + /// Verifies edge signatures on rayon runtime. + /// Since this is expensive it first deduplicates the input edges + /// and strips any edges which are already present in the graph. pub async fn verify(&self, edges: Vec) -> (Vec, bool) { let old = self.load(); let mut edges = Edge::deduplicate(edges); From 8c2bc6d714fb37a605b7f5b23665c5a163993f57 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Thu, 24 Nov 2022 15:52:19 +0100 Subject: [PATCH 031/188] fixed wait_for_routing_table (#8113) This is a testonly function awaiting for changes to the routing table. It may get updated when either: - a peer sends new edges (already covered) - a node creates new local edges (which was missing) --- chain/network/src/peer_manager/network_state/routing.rs | 1 + chain/network/src/peer_manager/peer_manager_actor.rs | 4 +--- chain/network/src/peer_manager/testonly.rs | 4 +--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/chain/network/src/peer_manager/network_state/routing.rs b/chain/network/src/peer_manager/network_state/routing.rs index 9e70e5ca6a2..5f7acb8afeb 100644 --- a/chain/network/src/peer_manager/network_state/routing.rs +++ b/chain/network/src/peer_manager/network_state/routing.rs @@ -104,6 +104,7 @@ impl NetworkState { } } // Broadcast new edges to all other peers. + this.config.event_sink.push(Event::EdgesAdded(edges.clone())); this.broadcast_routing_table_update(RoutingTableUpdate::from_edges(edges)); results }) diff --git a/chain/network/src/peer_manager/peer_manager_actor.rs b/chain/network/src/peer_manager/peer_manager_actor.rs index aff0aa0b5d3..93cf3bf255c 100644 --- a/chain/network/src/peer_manager/peer_manager_actor.rs +++ b/chain/network/src/peer_manager/peer_manager_actor.rs @@ -9,7 +9,6 @@ use crate::peer::peer_actor::PeerActor; use crate::peer_manager::connection; use crate::peer_manager::network_state::{NetworkState, WhitelistNode}; use crate::peer_manager::peer_store; -use crate::routing; use crate::stats::metrics; use crate::store; use crate::tcp; @@ -93,8 +92,7 @@ pub enum Event { ServerStarted, RoutedMessageDropped, AccountsAdded(Vec), - RoutingTableUpdate { next_hops: Arc, pruned_edges: Vec }, - EdgesVerified(Vec), + EdgesAdded(Vec), Ping(Ping), Pong(Pong), // Reported once a message has been processed. diff --git a/chain/network/src/peer_manager/testonly.rs b/chain/network/src/peer_manager/testonly.rs index 4ed08b551a1..c34821d41a5 100644 --- a/chain/network/src/peer_manager/testonly.rs +++ b/chain/network/src/peer_manager/testonly.rs @@ -347,9 +347,7 @@ impl ActorHandler { } events .recv_until(|ev| match ev { - Event::PeerManager(PME::MessageProcessed(PeerMessage::SyncRoutingTable { - .. - })) => Some(()), + Event::PeerManager(PME::EdgesAdded { .. }) => Some(()), _ => None, }) .await; From ca4b9137c570ea5a33c82367553dae28ef1fd612 Mon Sep 17 00:00:00 2001 From: Marcelo Diop-Gonzalez Date: Thu, 24 Nov 2022 11:47:30 -0500 Subject: [PATCH 032/188] feat(mirror): don't start a full node for the source chain by default (#8111) For running a mocknet test, we don't really want to start a full node for the source chain. Usually we just want to read the transactions from a mainnet or testnet DB and send them over. So this PR adds a `ChainAccess` trait used by the `struct TxMirror`, and implements it for an `online` and an `offline` version. Then the presence of the `--online-source` flag will tell which one to use. We'll keep the `online` version around because it might be useful to have in some cases. Like if you have a long running test, you can keep traffic going to it by using `--online-source` --- Cargo.lock | 4 +- .../tools/mirror/{test.py => mirror_utils.py} | 432 ++++++++++-------- pytest/tools/mirror/offline_test.py | 68 +++ pytest/tools/mirror/online_test.py | 59 +++ tools/mirror/Cargo.toml | 4 +- tools/mirror/src/chain_tracker.rs | 35 +- tools/mirror/src/cli.rs | 22 +- tools/mirror/src/lib.rs | 197 ++++---- tools/mirror/src/offline.rs | 77 ++++ tools/mirror/src/online.rs | 99 ++++ 10 files changed, 707 insertions(+), 290 deletions(-) rename pytest/tools/mirror/{test.py => mirror_utils.py} (57%) mode change 100755 => 100644 create mode 100755 pytest/tools/mirror/offline_test.py create mode 100755 pytest/tools/mirror/online_test.py create mode 100644 tools/mirror/src/offline.rs create mode 100644 tools/mirror/src/online.rs diff --git a/Cargo.lock b/Cargo.lock index b9aa23ad774..ce4a4acdb61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3233,17 +3233,19 @@ version = "0.0.0" dependencies = [ "actix", "anyhow", + "async-trait", "borsh", "bs58", "clap 3.1.18", "ed25519-dalek", "hex", "hkdf", + "near-chain", "near-chain-configs", + "near-chain-primitives", "near-client", "near-client-primitives", "near-crypto", - "near-dyn-configs", "near-indexer", "near-indexer-primitives", "near-network", diff --git a/pytest/tools/mirror/test.py b/pytest/tools/mirror/mirror_utils.py old mode 100755 new mode 100644 similarity index 57% rename from pytest/tools/mirror/test.py rename to pytest/tools/mirror/mirror_utils.py index 64ae1571300..00f7f906fb8 --- a/pytest/tools/mirror/test.py +++ b/pytest/tools/mirror/mirror_utils.py @@ -1,8 +1,7 @@ -#!/usr/bin/env python3 - -import sys, time, base58, random -import atexit +import sys +import time import base58 +import atexit import json import os import pathlib @@ -14,20 +13,12 @@ from cluster import init_cluster, spin_up_node, load_config from configured_logger import logger -from mocknet import create_genesis_file import transaction import utils import key -# This sets up an environment to test the tools/mirror process. It starts a localnet with a few validators -# and waits for some blocks to be produced. Then we fork the state and start a new chain from that, and -# start the mirror process that should mirror transactions from the source chain to the target chain. -# Halfway through we restart it to make sure that it still works properly when restarted - -TIMEOUT = 240 -NUM_VALIDATORS = 4 -TARGET_VALIDATORS = ['foo0', 'foo1', 'foo2'] MIRROR_DIR = 'test-mirror' +TIMEOUT = 240 def mkdir_clean(dirname): @@ -42,7 +33,7 @@ def dot_near(): return pathlib.Path.home() / '.near' -def ordinal_to_port(port, ordinal): +def ordinal_to_addr(port, ordinal): return f'0.0.0.0:{port + 10 + ordinal}' @@ -51,7 +42,11 @@ def copy_genesis(home): shutil.copy(dot_near() / 'test0/forked/records.json', home / 'records.json') -def init_target_dir(neard, home, ordinal, validator_account=None): +def init_target_dir(neard, + home, + ordinal, + boot_node_home, + validator_account=None): mkdir_clean(home) try: @@ -66,8 +61,11 @@ def init_target_dir(neard, home, ordinal, validator_account=None): with open(home / 'config.json', 'r') as f: config = json.load(f) config['genesis_records_file'] = 'records.json' - config['network']['addr'] = ordinal_to_port(24567, ordinal) - config['rpc']['addr'] = ordinal_to_port(3030, ordinal) + config['network']['addr'] = ordinal_to_addr(24567, ordinal) + if boot_node_home is not None: + config['network']['boot_nodes'] = read_addr_pk(boot_node_home) + config['rpc']['addr'] = ordinal_to_addr(3030, ordinal) + with open(home / 'config.json', 'w') as f: json.dump(config, f) @@ -75,20 +73,31 @@ def init_target_dir(neard, home, ordinal, validator_account=None): os.remove(home / 'validator_key.json') -def init_target_dirs(neard): - ordinal = NUM_VALIDATORS + 1 +def init_target_dirs(neard, last_ordinal, target_validators): + ordinal = last_ordinal + 1 dirs = [] - for account_id in TARGET_VALIDATORS: + for i in range(len(target_validators)): + account_id = target_validators[i] + if i > 0: + boot_node_home = dirs[0] + else: + boot_node_home = None home = dot_near() / f'test_target_{account_id}' dirs.append(home) - init_target_dir(neard, home, ordinal, validator_account=account_id) + init_target_dir(neard, + home, + ordinal, + boot_node_home, + validator_account=account_id) ordinal += 1 return dirs -def create_forked_chain(config, near_root): +def create_forked_chain(config, near_root, source_node_homes, + target_validators): + mkdir_clean(dot_near() / MIRROR_DIR) binary_name = config.get('binary_name', 'neard') neard = os.path.join(near_root, binary_name) try: @@ -115,12 +124,23 @@ def create_forked_chain(config, near_root): except subprocess.CalledProcessError as e: sys.exit(f'"mirror prepare" command failed: output: {e.stdout}') - dirs = init_target_dirs(neard) + os.rename(source_node_homes[-1], dot_near() / f'{MIRROR_DIR}/source') + ordinal = len(source_node_homes) - 1 + with open(dot_near() / f'{MIRROR_DIR}/source/config.json', 'r') as f: + config = json.load(f) + config['network']['boot_nodes'] = read_addr_pk(source_node_homes[0]) + config['network']['addr'] = ordinal_to_addr(24567, ordinal) + config['rpc']['addr'] = ordinal_to_addr(3030, ordinal) + with open(dot_near() / f'{MIRROR_DIR}/source/config.json', 'w') as f: + json.dump(config, f) + + dirs = init_target_dirs(neard, ordinal, target_validators) target_dir = dot_near() / f'{MIRROR_DIR}/target' init_target_dir(neard, target_dir, - NUM_VALIDATORS + 1 + len(dirs), + len(source_node_homes) + len(dirs), + dirs[0], validator_account=None) shutil.copy(dot_near() / 'test0/output/mirror-secret.json', target_dir / 'mirror-secret.json') @@ -166,20 +186,19 @@ def create_forked_chain(config, near_root): copy_genesis(d) copy_genesis(target_dir) - return [str(d) for d in dirs], target_dir + return [str(d) for d in dirs] -def init_mirror_dir(home, source_boot_node): - mkdir_clean(dot_near() / MIRROR_DIR) - os.rename(home, dot_near() / f'{MIRROR_DIR}/source') - ordinal = NUM_VALIDATORS - with open(dot_near() / f'{MIRROR_DIR}/source/config.json', 'r') as f: +def read_addr_pk(home): + with open(os.path.join(home, 'config.json'), 'r') as f: config = json.load(f) - config['network']['boot_nodes'] = source_boot_node.addr_with_pk() - config['network']['addr'] = ordinal_to_port(24567, ordinal) - config['rpc']['addr'] = ordinal_to_port(3030, ordinal) - with open(dot_near() / f'{MIRROR_DIR}/source/config.json', 'w') as f: - json.dump(config, f) + addr = config['network']['addr'] + + with open(os.path.join(home, 'node_key.json'), 'r') as f: + k = json.load(f) + public_key = k['public_key'] + + return f'{public_key}@{addr}' def mirror_cleanup(process): @@ -191,29 +210,66 @@ def mirror_cleanup(process): logger.error('can\'t kill mirror process') -def start_mirror(near_root, source_home, target_home, boot_node): - env = os.environ.copy() - env["RUST_LOG"] = "actix_web=warn,mio=warn,tokio_util=warn,actix_server=warn,actix_http=warn," + 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: - process = subprocess.Popen([ - os.path.join(near_root, 'neard'), 'mirror', 'run', "--source-home", - source_home, "--target-home", target_home, '--secret-file', - target_home / 'mirror-secret.json' - ], - stdin=subprocess.DEVNULL, - stdout=stdout, - stderr=stderr, - env=env) - logger.info("Started mirror process") - atexit.register(mirror_cleanup, process) - with open(target_home / 'config.json', 'r') as f: - config = json.load(f) - config['network']['boot_nodes'] = boot_node.addr_with_pk() - with open(target_home / 'config.json', 'w') as f: - json.dump(config, f) - return process +# helper class so we can pass restart_once() as a callback to send_traffic() +class MirrorProcess: + + def __init__(self, near_root, source_home, online_source): + self.online_source = online_source + self.source_home = source_home + self.neard = os.path.join(near_root, 'neard') + self.start() + self.start_time = time.time() + self.restarted = False + + 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( + "RUST_LOG", "debug") + with open(dot_near() / f'{MIRROR_DIR}/stdout', 'ab') as stdout, \ + open(dot_near() / f'{MIRROR_DIR}/stderr', 'ab') as stderr: + args = [ + self.neard, 'mirror', 'run', "--source-home", self.source_home, + "--target-home", + dot_near() / f'{MIRROR_DIR}/target/', '--secret-file', + dot_near() / f'{MIRROR_DIR}/target/mirror-secret.json' + ] + if self.online_source: + args.append('--online-source') + self.process = subprocess.Popen(args, + stdin=subprocess.DEVNULL, + stdout=stdout, + stderr=stderr, + env=env) + logger.info("Started mirror process") + atexit.register(mirror_cleanup, self.process) + + def restart(self): + logger.info('stopping mirror process') + self.process.terminate() + self.process.wait() + with open(dot_near() / f'{MIRROR_DIR}/stderr', 'ab') as stderr: + stderr.write( + b'<><><><><><><><><><><><> restarting <><><><><><><><><><><><><><><><><><><><>\n' + ) + stderr.write( + b'<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>\n' + ) + stderr.write( + b'<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>\n' + ) + self.start() + + # meant to be used in the callback to send_traffic(). restarts the process once after 30 seconds + def restart_once(self): + code = self.process.poll() + if code is not None: + assert code == 0 + return False + + if not self.restarted and time.time() - self.start_time > 30: + self.restart() + self.restarted = True + return True # we'll test out adding an access key and then sending txs signed with it @@ -259,17 +315,6 @@ def create_subaccount(node, signer_key, nonce, block_hash): return k -def send_transfers(nodes, nonces, block_hash): - for sender in range(len(nonces)): - receiver = (sender + 1) % len(nonces) - receiver_id = nodes[receiver].signer_key.account_id - - tx = transaction.sign_payment_tx(nodes[sender].signer_key, receiver_id, - 300, nonces[sender], block_hash) - nodes[sender].send_tx(tx) - nonces[sender] += 1 - - # a key that we added with an AddKey tx or implicit account transfer. # just for nonce handling convenience class AddedKey: @@ -338,46 +383,68 @@ def count_total_txs(node, min_height=0): return total -def check_num_txs(source_node, target_node, start_time, end_source_height): - with open(os.path.join(target_node.node_dir, 'genesis.json'), 'r') as f: +def allowed_run_time(target_node_dir, start_time, end_source_height): + with open(os.path.join(target_node_dir, 'genesis.json'), 'r') as f: genesis_height = json.load(f)['genesis_height'] - with open(os.path.join(target_node.node_dir, 'config.json'), 'r') as f: + with open(os.path.join(target_node_dir, 'config.json'), 'r') as f: delay = json.load(f)['consensus']['min_block_production_delay'] block_delay = 10**9 * int(delay['secs']) + int(delay['nanos']) block_delay = block_delay / 10**9 - total_source_txs = count_total_txs(source_node, min_height=genesis_height) - # start_time is the time the mirror binary was started. Give it 20 seconds to # sync and then 50% more than min_block_production_delay for each block between # the start and end points of the source chain. Not ideal to be basing a test on time # like this but there's no real strong guarantee on when the transactions should # make it on chain, so this is some kind of reasonable timeout - total_time_allowed = 20 + (end_source_height - - genesis_height) * block_delay * 1.5 - time_elapsed = time.time() - start_time - if time_elapsed < total_time_allowed: - time_left = total_time_allowed - time_elapsed - logger.info( - f'waiting for {int(time_left)} seconds to allow transactions to make it to the target chain' - ) - time.sleep(time_left) + return 20 + (end_source_height - genesis_height) * block_delay * 1.5 + +def check_num_txs(source_node, target_node): + with open(os.path.join(target_node.node_dir, 'genesis.json'), 'r') as f: + genesis_height = json.load(f)['genesis_height'] + + total_source_txs = count_total_txs(source_node, min_height=genesis_height) total_target_txs = count_total_txs(target_node) assert total_source_txs == total_target_txs, (total_source_txs, total_target_txs) logger.info(f'all {total_source_txs} transactions mirrored') -def main(): +# keeps info initialized during start_source_chain() for use in send_traffic() +class TrafficData: + + def __init__(self, num_accounts): + self.nonces = [2] * num_accounts + self.implicit_account = None + + def send_transfers(self, nodes, block_hash, skip_senders=None): + for sender in range(len(self.nonces)): + if skip_senders is not None and sender in skip_senders: + continue + receiver = (sender + 1) % len(self.nonces) + receiver_id = nodes[receiver].signer_key.account_id + + tx = transaction.sign_payment_tx(nodes[sender].signer_key, + receiver_id, 300, + self.nonces[sender], block_hash) + nodes[sender].send_tx(tx) + self.nonces[sender] += 1 + + +def start_source_chain(config, + num_source_validators=3, + target_validators=['foo0', 'foo1', 'foo2']): + # for now we need at least 2 because we're sending traffic for source_nodes[1].signer_key + # Could fix that but for now this assert is fine + assert num_source_validators >= 2 config_changes = {} - for i in range(NUM_VALIDATORS + 1): + for i in range(num_source_validators + 1): config_changes[i] = {"tracked_shards": [0, 1, 2, 3], "archive": True} config = load_config() - near_root, node_dirs = init_cluster( - num_nodes=NUM_VALIDATORS, + near_root, source_node_dirs = init_cluster( + num_nodes=num_source_validators, num_observers=1, num_shards=4, config=config, @@ -389,70 +456,63 @@ def main(): ], client_config_changes=config_changes) - nodes = [spin_up_node(config, near_root, node_dirs[0], 0)] - - init_mirror_dir(node_dirs[NUM_VALIDATORS], nodes[0]) - - for i in range(1, NUM_VALIDATORS): - nodes.append( - spin_up_node(config, near_root, node_dirs[i], i, - boot_node=nodes[0])) - nonces = [2] * len(nodes) + source_nodes = [spin_up_node(config, near_root, source_node_dirs[0], 0)] - implicit_account1 = ImplicitAccount() - for height, block_hash in utils.poll_blocks(nodes[0], timeout=TIMEOUT): - implicit_account1.transfer(nodes[0], nodes[0].signer_key, 10**24, - base58.b58decode(block_hash.encode('utf8')), - nonces[0]) - nonces[0] += 1 + for i in range(1, num_source_validators): + source_nodes.append( + spin_up_node(config, + near_root, + source_node_dirs[i], + i, + boot_node=source_nodes[0])) + traffic_data = TrafficData(len(source_nodes)) + + traffic_data.implicit_account = ImplicitAccount() + for height, block_hash in utils.poll_blocks(source_nodes[0], + timeout=TIMEOUT): + traffic_data.implicit_account.transfer( + source_nodes[0], source_nodes[0].signer_key, 10**24, + base58.b58decode(block_hash.encode('utf8')), traffic_data.nonces[0]) + traffic_data.nonces[0] += 1 break - for height, block_hash in utils.poll_blocks(nodes[0], timeout=TIMEOUT): + for height, block_hash in utils.poll_blocks(source_nodes[0], + timeout=TIMEOUT): block_hash_bytes = base58.b58decode(block_hash.encode('utf8')) - implicit_account1.send_if_inited(nodes[0], [('test2', height), - ('test3', height)], - block_hash_bytes) - send_transfers(nodes, nonces, block_hash_bytes) + traffic_data.implicit_account.send_if_inited(source_nodes[0], + [('test2', height), + ('test3', height)], + block_hash_bytes) + traffic_data.send_transfers(source_nodes, block_hash_bytes) if height > 12: break - nodes[0].kill() - target_node_dirs, target_observer_dir = create_forked_chain( - config, near_root) - nodes[0].start(boot_node=nodes[1]) + source_nodes[0].kill() + target_node_dirs = create_forked_chain(config, near_root, source_node_dirs, + target_validators) + source_nodes[0].start(boot_node=source_nodes[1]) + return near_root, source_nodes, target_node_dirs, traffic_data - ordinal = NUM_VALIDATORS + 1 - target_nodes = [ - spin_up_node(config, near_root, target_node_dirs[0], ordinal) - ] - for i in range(1, len(target_node_dirs)): - ordinal += 1 - target_nodes.append( - spin_up_node(config, - near_root, - target_node_dirs[i], - ordinal, - boot_node=target_nodes[0])) - p = start_mirror(near_root, - dot_near() / f'{MIRROR_DIR}/source/', target_observer_dir, - target_nodes[0]) - start_time = time.time() - start_source_height = nodes[0].get_latest_block().height - restarted = False +# callback will be called once for every iteration of the utils.poll_blocks() +# loop, and we break if it returns False +def send_traffic(near_root, source_nodes, traffic_data, callback): + tip = source_nodes[1].get_latest_block() + block_hash_bytes = base58.b58decode(tip.hash.encode('utf8')) + start_source_height = tip.height subaccount_key = AddedKey( - create_subaccount(nodes[0], nodes[0].signer_key, nonces[0], - block_hash_bytes)) - nonces[0] += 1 + create_subaccount(source_nodes[1], source_nodes[0].signer_key, + traffic_data.nonces[0], block_hash_bytes)) + traffic_data.nonces[0] += 1 k = key.Key.from_random('test0') new_key = AddedKey(k) - send_add_access_key(nodes[0], nodes[0].signer_key, k, nonces[0], - block_hash_bytes) - nonces[0] += 1 + send_add_access_key(source_nodes[1], source_nodes[0].signer_key, k, + traffic_data.nonces[0], block_hash_bytes) + traffic_data.nonces[0] += 1 test0_deleted_height = None test0_readded_key = None @@ -464,44 +524,45 @@ def main(): # then wait a bit before properly initializing it. This hits a corner case where the # mirror binary needs to properly look for the second tx's outcome to find the starting # nonce because the first one failed - implicit_account2.transfer(nodes[0], nodes[0].signer_key, 1, - block_hash_bytes, nonces[0]) - nonces[0] += 1 + implicit_account2.transfer(source_nodes[1], source_nodes[0].signer_key, 1, + block_hash_bytes, traffic_data.nonces[0]) + traffic_data.nonces[0] += 1 time.sleep(2) - implicit_account2.transfer(nodes[0], nodes[0].signer_key, 10**24, - block_hash_bytes, nonces[0]) - nonces[0] += 1 + implicit_account2.transfer(source_nodes[1], source_nodes[0].signer_key, + 10**24, block_hash_bytes, traffic_data.nonces[0]) + traffic_data.nonces[0] += 1 - for height, block_hash in utils.poll_blocks(nodes[0], timeout=TIMEOUT): - code = p.poll() - if code is not None: - assert code == 0 + for height, block_hash in utils.poll_blocks(source_nodes[1], + timeout=TIMEOUT): + if not callback(): break - block_hash_bytes = base58.b58decode(block_hash.encode('utf8')) if test0_deleted_height is None: - send_transfers(nodes, nonces, block_hash_bytes) + traffic_data.send_transfers(source_nodes, block_hash_bytes) else: - send_transfers(nodes[1:], nonces[1:], block_hash_bytes) + traffic_data.send_transfers(source_nodes, + block_hash_bytes, + skip_senders=set([0])) - implicit_account1.send_if_inited( - nodes[0], [('test2', height), ('test1', height), - (implicit_account2.account_id(), height)], + traffic_data.implicit_account.send_if_inited( + source_nodes[1], [('test2', height), ('test1', height), + (implicit_account2.account_id(), height)], block_hash_bytes) if not implicit_deleted: implicit_account2.send_if_inited( - nodes[1], [('test2', height), ('test0', height), - (implicit_account1.account_id(), height)], + source_nodes[1], + [('test2', height), ('test0', height), + (traffic_data.implicit_account.account_id(), height)], block_hash_bytes) - new_key.send_if_inited(nodes[2], - [('test1', height), ('test2', height), - (implicit_account1.account_id(), height), - (implicit_account2.account_id(), height)], - block_hash_bytes) + new_key.send_if_inited( + source_nodes[1], + [('test1', height), ('test2', height), + (traffic_data.implicit_account.account_id(), height), + (implicit_account2.account_id(), height)], block_hash_bytes) subaccount_key.send_if_inited( - nodes[3], [('test3', height), - (implicit_account2.account_id(), height)], + source_nodes[1], [('test3', height), + (implicit_account2.account_id(), height)], block_hash_bytes) if implicit_added is None: @@ -514,15 +575,15 @@ def main(): ) and height - start_source_height >= 15: k = key.Key.from_random(implicit_account2.account_id()) implicit_added = AddedKey(k) - send_add_access_key(nodes[0], implicit_account2.key.key, k, - implicit_account2.key.nonce, + send_add_access_key(source_nodes[1], implicit_account2.key.key, + k, implicit_account2.key.nonce, block_hash_bytes) implicit_account2.key.nonce += 1 else: - implicit_added.send_if_inited(nodes[1], [('test0', height)], + implicit_added.send_if_inited(source_nodes[1], [('test0', height)], block_hash_bytes) if implicit_added.inited() and not implicit_deleted: - send_delete_access_key(nodes[0], implicit_added.key, + send_delete_access_key(source_nodes[1], implicit_added.key, implicit_account2.key.key, implicit_added.nonce, block_hash_bytes) implicit_added.nonce += 1 @@ -530,51 +591,24 @@ def main(): if test0_deleted_height is None and new_key.inited( ) and height - start_source_height >= 15: - send_delete_access_key(nodes[0], new_key.key, nodes[0].signer_key, + send_delete_access_key(source_nodes[1], new_key.key, + source_nodes[0].signer_key, new_key.nonce + 1, block_hash_bytes) new_key.nonce += 1 test0_deleted_height = height if test0_readded_key is None and test0_deleted_height is not None and height - test0_deleted_height >= 5: - send_add_access_key(nodes[0], new_key.key, nodes[0].signer_key, - new_key.nonce + 1, block_hash_bytes) - test0_readded_key = AddedKey(nodes[0].signer_key) + send_add_access_key(source_nodes[1], new_key.key, + source_nodes[0].signer_key, new_key.nonce + 1, + block_hash_bytes) + test0_readded_key = AddedKey(source_nodes[0].signer_key) new_key.nonce += 1 if test0_readded_key is not None: test0_readded_key.send_if_inited( - nodes[1], [('test3', height), - (implicit_account2.account_id(), height)], + source_nodes[1], [('test3', height), + (implicit_account2.account_id(), height)], block_hash_bytes) - if not restarted and height - start_source_height >= 50: - logger.info('stopping mirror process') - p.terminate() - p.wait() - with open(dot_near() / f'{MIRROR_DIR}/stderr', 'ab') as stderr: - stderr.write( - b'<><><><><><><><><><><><> restarting <><><><><><><><><><><><><><><><><><><><>\n' - ) - stderr.write( - b'<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>\n' - ) - stderr.write( - b'<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>\n' - ) - p = start_mirror(near_root, - dot_near() / f'{MIRROR_DIR}/source/', - target_observer_dir, target_nodes[0]) - restarted = True - if height - start_source_height >= 100: break - - time.sleep(5) - # we don't need these anymore - for node in nodes[1:]: - node.kill() - check_num_txs(nodes[0], target_nodes[0], start_time, height) - - -if __name__ == '__main__': - main() diff --git a/pytest/tools/mirror/offline_test.py b/pytest/tools/mirror/offline_test.py new file mode 100755 index 00000000000..969a55d3034 --- /dev/null +++ b/pytest/tools/mirror/offline_test.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +import sys +import time +import pathlib + +sys.path.append(str(pathlib.Path(__file__).resolve().parents[2] / 'lib')) + +from cluster import spin_up_node, load_config +from configured_logger import logger + +import mirror_utils + +# This sets up an environment to test the tools/mirror process. It starts a localnet with a few validators +# and waits for some blocks to be produced. Then we fork the state and start a new chain from that, and +# send some traffic. After a while we stop the source chain nodes and start the target chain nodes, +# and run the mirror binary that should send the source chain traffic to the target chain + + +def main(): + config = load_config() + + near_root, source_nodes, target_node_dirs, traffic_data = mirror_utils.start_source_chain( + config) + + # sleep for a bit to allow test0 to catch up after restarting before we send traffic + time.sleep(5) + mirror_utils.send_traffic(near_root, source_nodes, traffic_data, + lambda: True) + + target_nodes = [ + spin_up_node(config, near_root, target_node_dirs[i], + len(source_nodes) + 1 + i) + for i in range(len(target_node_dirs)) + ] + + end_source_height = source_nodes[0].get_latest_block().height + time.sleep(5) + # we don't need these anymore + for node in source_nodes[1:]: + node.kill() + + mirror = mirror_utils.MirrorProcess(near_root, + source_nodes[1].node_dir, + online_source=False) + + total_time_allowed = mirror_utils.allowed_run_time(target_node_dirs[0], + mirror.start_time, + end_source_height) + while True: + time.sleep(5) + # this will restart the binary one time during this test, and it will return false + # when it exits on its own, which should happen once it finishes sending all the + # transactions in its source chain (~/.near/test1/) + if not mirror.restart_once(): + break + elapsed = time.time() - mirror.start_time + if elapsed > total_time_allowed: + logger.warn( + f'mirror process has not exited after {int(elapsed)} seconds. stopping the test now' + ) + break + + mirror_utils.check_num_txs(source_nodes[0], target_nodes[0]) + + +if __name__ == '__main__': + main() diff --git a/pytest/tools/mirror/online_test.py b/pytest/tools/mirror/online_test.py new file mode 100755 index 00000000000..dd8541893fa --- /dev/null +++ b/pytest/tools/mirror/online_test.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +import sys +import time +import pathlib + +sys.path.append(str(pathlib.Path(__file__).resolve().parents[2] / 'lib')) + +from cluster import spin_up_node, load_config +from configured_logger import logger + +import mirror_utils + +# This sets up an environment to test the tools/mirror process. It starts a localnet with a few validators +# and waits for some blocks to be produced. Then we fork the state and start a new chain from that, and +# start the mirror process while the sending more traffic to the source chain + + +def main(): + config = load_config() + + near_root, source_nodes, target_node_dirs, traffic_data = mirror_utils.start_source_chain( + config) + + target_nodes = [ + spin_up_node(config, near_root, target_node_dirs[i], + len(source_nodes) + 1 + i) + for i in range(len(target_node_dirs)) + ] + + mirror = mirror_utils.MirrorProcess(near_root, + mirror_utils.dot_near() / + f'{mirror_utils.MIRROR_DIR}/source', + online_source=True) + + mirror_utils.send_traffic(near_root, source_nodes, traffic_data, + mirror.restart_once) + + end_source_height = source_nodes[0].get_latest_block().height + time.sleep(5) + # we don't need these anymore + for node in source_nodes[1:]: + node.kill() + + total_time_allowed = mirror_utils.allowed_run_time(target_node_dirs[0], + mirror.start_time, + end_source_height) + time_elapsed = time.time() - mirror.start_time + if time_elapsed < total_time_allowed: + time_left = total_time_allowed - time_elapsed + logger.info( + 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]) + + +if __name__ == '__main__': + main() diff --git a/tools/mirror/Cargo.toml b/tools/mirror/Cargo.toml index ed1bcac54ee..97d72680791 100644 --- a/tools/mirror/Cargo.toml +++ b/tools/mirror/Cargo.toml @@ -8,6 +8,7 @@ edition.workspace = true [dependencies] actix.workspace = true anyhow.workspace = true +async-trait.workspace = true borsh.workspace = true bs58.workspace = true clap.workspace = true @@ -29,7 +30,8 @@ tracing.workspace = true nearcore = { path = "../../nearcore" } near-chain-configs = { path = "../../core/chain-configs" } -near-dyn-configs = { path = "../../core/dyn-configs" } +near-chain = { path = "../../chain/chain" } +near-chain-primitives = { path = "../../chain/chain-primitives" } near-client = { path = "../../chain/client" } near-client-primitives = { path = "../../chain/client-primitives" } near-indexer-primitives = { path = "../../chain/indexer-primitives" } diff --git a/tools/mirror/src/chain_tracker.rs b/tools/mirror/src/chain_tracker.rs index e93b36d153d..551bf2679db 100644 --- a/tools/mirror/src/chain_tracker.rs +++ b/tools/mirror/src/chain_tracker.rs @@ -131,22 +131,44 @@ pub(crate) struct TxTracker { updater_to_keys: HashMap>, nonces: HashMap<(AccountId, PublicKey), NonceInfo>, height_queued: Option, + // the reason we have these (nonempty_height_queued, height_seen, etc) is so that we can + // exit after we receive the target block containing the txs we sent for the last source block. + // It's a minor thing, but otherwise if we just exit after sending the last source block's txs, + // we won't get to see the resulting txs on chain in the debug logs from log_target_block() + nonempty_height_queued: Option, + height_popped: Option, + height_seen: Option, send_time: Option>>, // Config value in the target chain, used to judge how long to wait before sending a new batch of txs min_block_production_delay: Duration, // timestamps in the target chain, used to judge how long to wait before sending a new batch of txs recent_block_timestamps: VecDeque, + // last source block we'll be sending transactions for + stop_height: Option, } impl TxTracker { - pub(crate) fn new(min_block_production_delay: Duration) -> Self { - Self { min_block_production_delay, ..Default::default() } + pub(crate) fn new( + min_block_production_delay: Duration, + stop_height: Option, + ) -> Self { + Self { min_block_production_delay, stop_height, ..Default::default() } } pub(crate) fn height_queued(&self) -> Option { self.height_queued } + pub(crate) fn finished(&self) -> bool { + match self.stop_height { + Some(_) => { + self.height_popped >= self.stop_height + && self.height_seen >= self.nonempty_height_queued + } + None => false, + } + } + pub(crate) fn num_blocks_queued(&self) -> usize { self.queued_blocks.len() } @@ -290,6 +312,9 @@ impl TxTracker { ) -> anyhow::Result<()> { self.height_queued = Some(block.source_height); for c in block.chunks.iter() { + if !c.txs.is_empty() { + self.nonempty_height_queued = Some(block.source_height); + } for (tx_idx, tx) in c.txs.iter().enumerate() { let tx_ref = TxRef { source_height: block.source_height, shard_id: c.shard_id, tx_idx }; @@ -360,6 +385,7 @@ impl TxTracker { self.try_set_nonces(target_view_client, db, access_key, None).await?; } let block = &mut self.queued_blocks[0]; + self.height_popped = Some(block.source_height); for c in block.chunks.iter_mut() { for (tx_idx, tx) in c.txs.iter_mut().enumerate() { match tx { @@ -671,9 +697,12 @@ impl TxTracker { for s in msg.shards { if let Some(c) = s.chunk { for tx in c.transactions { - if self.sent_txs.remove(&tx.transaction.hash).is_some() { + if let Some(info) = self.sent_txs.remove(&tx.transaction.hash) { crate::metrics::TRANSACTIONS_INCLUDED.inc(); self.remove_tx(&tx); + if Some(info.source_height) > self.height_seen { + self.height_seen = Some(info.source_height); + } } let id = ChainObjectId::Tx(TxInfo { hash: tx.transaction.hash, diff --git a/tools/mirror/src/cli.rs b/tools/mirror/src/cli.rs index 4f6ea0a606c..f7de18054ff 100644 --- a/tools/mirror/src/cli.rs +++ b/tools/mirror/src/cli.rs @@ -1,5 +1,6 @@ use anyhow::Context; use clap::Parser; +use near_primitives::types::BlockHeight; use std::cell::Cell; use std::path::PathBuf; @@ -15,8 +16,8 @@ enum SubCommand { Run(RunCmd), } -/// Start two NEAR nodes, one for each chain, and try to mirror -/// transactions from the source chain to the target chain. +/// initialize a target chain with genesis records from the source chain, and +/// then try to mirror transactions from the source chain to the target chain. #[derive(Parser)] struct RunCmd { /// source chain home dir @@ -35,6 +36,14 @@ struct RunCmd { /// that does contain a secret, the mirror will refuse to start #[clap(long)] no_secret: bool, + /// Start a NEAR node for the source chain, instead of only using + /// whatever's currently stored in --source-home + #[clap(long)] + online_source: bool, + /// If provided, we will stop after sending transactions coming from + /// this height in the source chain + #[clap(long)] + stop_height: Option, } impl RunCmd { @@ -61,7 +70,14 @@ impl RunCmd { let system = new_actix_system(runtime); system .block_on(async move { - actix::spawn(crate::run(self.source_home, self.target_home, secret)).await + actix::spawn(crate::run( + self.source_home, + self.target_home, + secret, + self.stop_height, + self.online_source, + )) + .await }) .unwrap() } diff --git a/tools/mirror/src/lib.rs b/tools/mirror/src/lib.rs index 018be33f65c..aa14806bcaf 100644 --- a/tools/mirror/src/lib.rs +++ b/tools/mirror/src/lib.rs @@ -1,11 +1,12 @@ use actix::Addr; use anyhow::Context; +use async_trait::async_trait; use borsh::{BorshDeserialize, BorshSerialize}; use near_chain_configs::GenesisValidationMode; use near_client::{ClientActor, ViewClientActor}; use near_client::{ProcessTxRequest, ProcessTxResponse}; use near_client_primitives::types::{ - GetChunk, GetChunkError, GetExecutionOutcome, GetExecutionOutcomeError, + GetBlockError, GetChunkError, GetExecutionOutcome, GetExecutionOutcomeError, GetExecutionOutcomeResponse, Query, QueryError, }; use near_crypto::{PublicKey, SecretKey}; @@ -35,6 +36,8 @@ pub mod cli; mod genesis; mod key_mapping; mod metrics; +mod offline; +mod online; mod secret; pub use cli::MirrorCommand; @@ -221,10 +224,75 @@ fn set_next_source_height(db: &DB, height: BlockHeight) -> anyhow::Result<()> { Ok(()) } -struct TxMirror { +struct ChunkTxs { + shard_id: ShardId, + transactions: Vec, +} + +#[derive(thiserror::Error, Debug)] +enum ChainError { + #[error("block unknown")] + UnknownBlock, + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +impl ChainError { + fn other(error: E) -> Self { + Self::Other(anyhow::Error::from(error)) + } +} + +impl From for ChainError { + fn from(err: GetBlockError) -> Self { + match err { + GetBlockError::UnknownBlock { .. } => Self::UnknownBlock, + _ => Self::other(err), + } + } +} + +impl From for ChainError { + fn from(err: GetChunkError) -> Self { + match err { + GetChunkError::UnknownBlock { .. } => Self::UnknownBlock, + _ => Self::other(err), + } + } +} + +impl From for ChainError { + fn from(err: near_chain_primitives::Error) -> Self { + match err { + near_chain_primitives::Error::DBNotFoundErr(_) => Self::UnknownBlock, + _ => Self::other(err), + } + } +} + +#[async_trait(?Send)] +trait ChainAccess { + async fn init(&self) -> anyhow::Result<()> { + Ok(()) + } + + async fn head_height(&self) -> anyhow::Result; + + async fn get_txs( + &self, + height: BlockHeight, + shards: &[ShardId], + ) -> Result, ChainError>; +} + +struct TxMirror { target_stream: mpsc::Receiver, - source_view_client: Addr, - source_client: Addr, + source_chain_access: T, + // TODO: separate out the code that uses the target chain clients, and + // make it an option to send the transactions to some RPC node. + // that way it would be possible to run this code and send transactions with an + // old binary not caught up to the current protocol version, since the + // transactions we're constructing should stay valid. target_view_client: Addr, target_client: Addr, db: DB, @@ -605,9 +673,9 @@ async fn fetch_receipt_outcome( } } -impl TxMirror { +impl TxMirror { fn new>( - source_home: P, + source_chain_access: T, target_home: P, secret: Option<[u8; crate::secret::SECRET_LEN]>, ) -> anyhow::Result { @@ -618,15 +686,6 @@ impl TxMirror { })?; let db = open_db(target_home.as_ref(), &target_config).context("failed to open mirror DB")?; - let source_config = - nearcore::config::load_config(source_home.as_ref(), GenesisValidationMode::UnsafeFast) - .with_context(|| { - format!("Error loading source config from {:?}", source_home.as_ref()) - })?; - - let source_node = nearcore::start_with_config(source_home.as_ref(), source_config.clone()) - .context("failed to start source chain NEAR node")?; - let target_indexer = Indexer::new(near_indexer::IndexerConfig { home_dir: target_home.as_ref().to_path_buf(), sync_mode: near_indexer::SyncModeEnum::LatestSynced, @@ -637,8 +696,7 @@ impl TxMirror { let target_stream = target_indexer.streamer(); Ok(Self { - source_view_client: source_node.view_client, - source_client: source_node.client, + source_chain_access, target_client, target_view_client, target_stream, @@ -784,35 +842,21 @@ impl TxMirror { ref_hash: CryptoHash, tracker: &mut crate::chain_tracker::TxTracker, ) -> anyhow::Result> { - let mut chunks = Vec::new(); - for shard_id in self.tracked_shards.iter() { - let mut txs = Vec::new(); - - let chunk = match self - .source_view_client - .send(GetChunk::Height(source_height, *shard_id).with_span_context()) - .await? - { - Ok(c) => c, + let source_chunks = + match self.source_chain_access.get_txs(source_height, &self.tracked_shards).await { + Ok(x) => x, Err(e) => match e { - GetChunkError::UnknownBlock { .. } => return Ok(None), - GetChunkError::UnknownChunk { .. } => { - tracing::error!( - "Can't fetch source chain shard {} chunk at height {}. Are we tracking all shards?", - shard_id, source_height - ); - continue; - } - _ => return Err(e.into()), + ChainError::UnknownBlock => return Ok(None), + ChainError::Other(e) => return Err(e), }, }; - if chunk.header.height_included != source_height { - continue; - } - let mut num_not_ready = 0; + let mut num_not_ready = 0; + let mut chunks = Vec::new(); + for ch in source_chunks { + let mut txs = Vec::new(); - for (idx, source_tx) in chunk.transactions.into_iter().enumerate() { + for (idx, source_tx) in ch.transactions.into_iter().enumerate() { let (actions, tx_nonce_updates) = self.map_actions(&source_tx).await?; if actions.is_empty() { // If this is a tx containing only stake actions, skip it. @@ -890,16 +934,16 @@ impl TxMirror { if num_not_ready == 0 { tracing::debug!( target: "mirror", "prepared {} transacations for source chain #{} shard {}", - txs.len(), source_height, shard_id + txs.len(), source_height, ch.shard_id ); } else { tracing::debug!( target: "mirror", "prepared {} transacations for source chain #{} shard {} with {} \ still waiting for the corresponding access keys to make it on chain", - txs.len(), source_height, shard_id, num_not_ready, + txs.len(), source_height, ch.shard_id, num_not_ready, ); } - chunks.push(MappedChunk { txs, shard_id: *shard_id }); + chunks.push(MappedChunk { txs, shard_id: ch.shard_id }); } Ok(Some(MappedBlock { source_height, chunks })) } @@ -917,8 +961,11 @@ impl TxMirror { } let next_batch_time = tracker.next_batch_time(); - let source_head = - self.get_source_height().await.context("can't fetch source chain HEAD")?; + let source_head = self + .source_chain_access + .head_height() + .await + .context("can't fetch source chain HEAD")?; let start_height = match tracker.height_queued() { Some(h) => h + 1, None => self.get_next_source_height()?, @@ -976,38 +1023,10 @@ impl TxMirror { self.queue_txs(&mut tracker, target_head, true).await?; } }; - } - } - - async fn get_source_height(&self) -> Option { - self.source_client - .send( - near_client::Status { is_health_check: false, detailed: false }.with_span_context(), - ) - .await - .unwrap() - .ok() - .map(|s| s.sync_info.latest_block_height) - } - - // wait until HEAD moves. We don't really need it to be fully synced. - async fn wait_source_ready(&self) { - let mut first_height = None; - loop { - if let Some(head) = self.get_source_height().await { - match first_height { - Some(h) => { - if h != head { - return; - } - } - None => { - first_height = Some(head); - } - } + if tracker.finished() { + tracing::info!(target: "mirror", "finished sending all transactions"); + return Ok(()); } - - tokio::time::sleep(Duration::from_millis(500)).await; } } @@ -1016,10 +1035,13 @@ impl TxMirror { (msg.block.header.height, msg.block.header.hash) } - async fn run(mut self) -> anyhow::Result<()> { - let mut tracker = - crate::chain_tracker::TxTracker::new(self.target_min_block_production_delay); - self.wait_source_ready().await; + async fn run(mut self, stop_height: Option) -> anyhow::Result<()> { + let mut tracker = crate::chain_tracker::TxTracker::new( + self.target_min_block_production_delay, + stop_height, + ); + self.source_chain_access.init().await?; + let (target_height, target_head) = self.wait_target_synced().await; self.queue_txs(&mut tracker, target_head, false).await?; @@ -1032,7 +1054,16 @@ async fn run>( source_home: P, target_home: P, secret: Option<[u8; crate::secret::SECRET_LEN]>, + stop_height: Option, + online_source: bool, ) -> anyhow::Result<()> { - let m = TxMirror::new(source_home, target_home, secret)?; - m.run().await + if !online_source { + let source_chain_access = crate::offline::ChainAccess::new(source_home)?; + let stop_height = stop_height.unwrap_or(source_chain_access.head_height().await?); + TxMirror::new(source_chain_access, target_home, secret)?.run(Some(stop_height)).await + } else { + TxMirror::new(crate::online::ChainAccess::new(source_home)?, target_home, secret)? + .run(stop_height) + .await + } } diff --git a/tools/mirror/src/offline.rs b/tools/mirror/src/offline.rs new file mode 100644 index 00000000000..dba74e5692e --- /dev/null +++ b/tools/mirror/src/offline.rs @@ -0,0 +1,77 @@ +use crate::{ChainError, ChunkTxs}; +use anyhow::Context; +use async_trait::async_trait; +use near_chain::{ChainStore, ChainStoreAccess}; +use near_chain_configs::GenesisValidationMode; +use near_primitives::types::BlockHeight; +use near_primitives_core::types::ShardId; +use std::path::Path; + +pub(crate) struct ChainAccess { + chain: ChainStore, +} + +impl ChainAccess { + pub(crate) fn new>(home: P) -> anyhow::Result { + let config = + nearcore::config::load_config(home.as_ref(), GenesisValidationMode::UnsafeFast) + .with_context(|| format!("Error loading config from {:?}", home.as_ref()))?; + // leave it ReadWrite since otherwise there are problems with the compiled contract cache + let store_opener = + near_store::NodeStorage::opener(home.as_ref(), &config.config.store, None); + let store = store_opener + .open() + .with_context(|| format!("Error opening store in {:?}", home.as_ref()))? + .get_store(near_store::Temperature::Hot); + let chain = ChainStore::new( + store, + config.genesis.config.genesis_height, + !config.client_config.archive, + ); + Ok(Self { chain }) + } +} + +#[async_trait(?Send)] +impl crate::ChainAccess for ChainAccess { + async fn head_height(&self) -> anyhow::Result { + Ok(self.chain.head().context("Could not fetch chain head")?.height) + } + + async fn get_txs( + &self, + height: BlockHeight, + shards: &[ShardId], + ) -> Result, ChainError> { + let block_hash = self.chain.get_block_hash_by_height(height)?; + let block = self + .chain + .get_block(&block_hash) + .with_context(|| format!("Can't get block {} at height {}", &block_hash, height))?; + + // of course simpler/faster to just have an array of bools but this is a one liner and who cares :) + let shards = shards.iter().collect::>(); + + let mut chunks = Vec::new(); + for chunk in block.chunks().iter() { + if !shards.contains(&chunk.shard_id()) { + continue; + } + let chunk = match self.chain.get_chunk(&chunk.chunk_hash()) { + Ok(c) => c, + Err(e) => { + tracing::error!( + "Can't fetch source chain shard {} chunk at height {}. Are we tracking all shards?: {:?}", + chunk.shard_id(), height, e + ); + continue; + } + }; + chunks.push(ChunkTxs { + shard_id: chunk.shard_id(), + transactions: chunk.transactions().iter().map(|t| t.clone().into()).collect(), + }) + } + Ok(chunks) + } +} diff --git a/tools/mirror/src/online.rs b/tools/mirror/src/online.rs new file mode 100644 index 00000000000..748c5f3e2c6 --- /dev/null +++ b/tools/mirror/src/online.rs @@ -0,0 +1,99 @@ +use crate::{ChainError, ChunkTxs}; +use actix::Addr; +use anyhow::Context; +use async_trait::async_trait; +use near_chain_configs::GenesisValidationMode; +use near_client::{ClientActor, ViewClientActor}; +use near_client_primitives::types::{GetChunk, GetChunkError}; +use near_o11y::WithSpanContextExt; +use near_primitives::types::BlockHeight; +use near_primitives_core::types::ShardId; +use std::path::Path; +use std::time::Duration; + +pub(crate) struct ChainAccess { + view_client: Addr, + client: Addr, +} + +impl ChainAccess { + pub(crate) fn new>(home: P) -> anyhow::Result { + let config = + nearcore::config::load_config(home.as_ref(), GenesisValidationMode::UnsafeFast) + .with_context(|| format!("Error loading config from {:?}", home.as_ref()))?; + + let node = nearcore::start_with_config(home.as_ref(), config.clone()) + .context("failed to start NEAR node")?; + Ok(Self { view_client: node.view_client, client: node.client }) + } +} + +#[async_trait(?Send)] +impl crate::ChainAccess for ChainAccess { + // wait until HEAD moves. We don't really need it to be fully synced. + async fn init(&self) -> anyhow::Result<()> { + let mut first_height = None; + loop { + let head = self.head_height().await?; + match first_height { + Some(h) => { + if h != head { + return Ok(()); + } + } + None => { + first_height = Some(head); + } + } + + tokio::time::sleep(Duration::from_millis(500)).await; + } + } + + async fn head_height(&self) -> anyhow::Result { + self.client + .send( + near_client::Status { is_health_check: false, detailed: false }.with_span_context(), + ) + .await + .unwrap() + .map(|s| s.sync_info.latest_block_height) + .map_err(Into::into) + } + + async fn get_txs( + &self, + height: BlockHeight, + shards: &[ShardId], + ) -> Result, ChainError> { + let mut chunks = Vec::new(); + for shard_id in shards.iter() { + let chunk = match self + .view_client + .send(GetChunk::Height(height, *shard_id).with_span_context()) + .await + .unwrap() + { + Ok(c) => c, + Err(e) => match e { + GetChunkError::UnknownChunk { .. } => { + tracing::error!( + "Can't fetch source chain shard {} chunk at height {}. Are we tracking all shards?", + shard_id, height + ); + continue; + } + _ => return Err(e.into()), + }, + }; + if chunk.header.height_included == height { + chunks.push(ChunkTxs { + shard_id: *shard_id, + transactions: chunk.transactions.clone(), + }) + } + } + + Ok(chunks) + } +} From 9e576fe4614db22ee681c39ad8cfa88814f12378 Mon Sep 17 00:00:00 2001 From: mm-near <91919554+mm-near@users.noreply.github.com> Date: Fri, 25 Nov 2022 15:05:23 +0100 Subject: [PATCH 033/188] Start creating connections (edges) with large nonces (#7966) * preparing for sending large nonces * also show nonce on debug page. * add time since nonce was created to html * set proper nonces on re-initialzing of the connection * fixed broken test * added more tests * reviews feedback part1 * comments * logs fix * comments and cleanup unnecessary clock advance * added missing updates * compile fixes * test fixed * fixed compilation issue * review comment * fixed after merge --- chain/client/src/debug.rs | 1 + chain/client/src/test_utils.rs | 1 + chain/jsonrpc/res/network_info.html | 5 +- chain/network/src/network_protocol/edge.rs | 10 +++ chain/network/src/peer/peer_actor.rs | 67 +++++++++++++-- .../src/peer_manager/connection/mod.rs | 18 +++- .../src/peer_manager/network_state/mod.rs | 6 +- .../src/peer_manager/network_state/routing.rs | 25 +++++- .../src/peer_manager/peer_manager_actor.rs | 1 + chain/network/src/peer_manager/tests/nonce.rs | 83 ++++++++++++++++++- chain/network/src/types.rs | 16 +--- core/primitives/src/views.rs | 2 + .../src/tests/client/process_blocks.rs | 27 ++++-- tools/mock-node/src/lib.rs | 16 +++- 14 files changed, 234 insertions(+), 44 deletions(-) diff --git a/chain/client/src/debug.rs b/chain/client/src/debug.rs index 3281ba5cf35..cbf6aa47ed5 100644 --- a/chain/client/src/debug.rs +++ b/chain/client/src/debug.rs @@ -662,6 +662,7 @@ fn new_peer_info_view(chain: &Chain, connected_peer_info: &ConnectedPeerInfo) -> .elapsed() .whole_milliseconds() as u64, is_outbound_peer: connected_peer_info.peer_type == PeerType::Outbound, + nonce: connected_peer_info.nonce, } } diff --git a/chain/client/src/test_utils.rs b/chain/client/src/test_utils.rs index f4c2a07b319..974072a10a6 100644 --- a/chain/client/src/test_utils.rs +++ b/chain/client/src/test_utils.rs @@ -681,6 +681,7 @@ pub fn setup_mock_all_validators( last_time_received_message: near_network::time::Instant::now(), connection_established_time: near_network::time::Instant::now(), peer_type: PeerType::Outbound, + nonce: 3, }) .collect(); let peers2 = peers diff --git a/chain/jsonrpc/res/network_info.html b/chain/jsonrpc/res/network_info.html index 80e5de40ef7..b65c1aa4dde 100644 --- a/chain/jsonrpc/res/network_info.html +++ b/chain/jsonrpc/res/network_info.html @@ -260,13 +260,15 @@ let row = $('.js-tbody-peers').append($('
').append(add_debug_port_link(peer.addr))) .append($('').append(validator.join(","))) - .append($('').append(peer.peer_id.substr(9, 5) + "...")) + .append($('').append(peer.peer_id.substr(8, 5) + "...")) .append($('').append(convertTime(peer.last_time_received_message_millis)).addClass(last_ping_class)) .append($('').append(JSON.stringify(peer.height)).addClass(peer_class)) .append($('').append(displayHash(peer))) .append($('').append(JSON.stringify(peer.tracked_shards))) .append($('').append(JSON.stringify(peer.archival))) .append($('').append(((peer.is_outbound_peer) ? 'OUT' : 'IN'))) + // If this is a new style nonce - show the approx time since it was created. + .append($('').append(peer.nonce + "
" + ((peer.nonce > 1660000000) ? convertTime(Date.now() - peer.nonce * 1000) : "old style nonce"))) .append($('
').append(convertTime(peer.connection_established_time_millis))) .append($('').append(computeTraffic(peer.received_bytes_per_sec, peer.sent_bytes_per_sec))) .append($('').append(routedValidator.join(","))) @@ -412,6 +414,7 @@

Tracked Shards Archival Connection typeNonce First connection Traffic (last minute) Route to validators
+ + + + + + + + + + + + + + + + +
AddressValidatorPeerIdProxiesLast pingTracked ShardsArchivalConnection typeFirst connectionTraffic (last minute)
+ + + diff --git a/chain/jsonrpc/src/lib.rs b/chain/jsonrpc/src/lib.rs index 8ec2f5b933e..41f46e0afc8 100644 --- a/chain/jsonrpc/src/lib.rs +++ b/chain/jsonrpc/src/lib.rs @@ -300,6 +300,9 @@ 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; @@ -1008,6 +1011,16 @@ 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, @@ -1371,6 +1384,18 @@ 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(); @@ -1423,6 +1448,9 @@ async fn display_debug_html( "last_blocks" => Some(debug_page_string!("last_blocks.html", handler)), "last_blocks.js" => Some(debug_page_string!("last_blocks.js", handler)), "network_info" => Some(debug_page_string!("network_info.html", handler)), + "network_info.css" => Some(debug_page_string!("network_info.css", handler)), + "network_info.js" => Some(debug_page_string!("network_info.js", handler)), + "tier1_network_info" => Some(debug_page_string!("tier1_network_info.html", handler)), "epoch_info" => Some(debug_page_string!("epoch_info.html", handler)), "chain_n_chunk_info" => Some(debug_page_string!("chain_n_chunk_info.html", handler)), "sync" => Some(debug_page_string!("sync.html", handler)), @@ -1495,6 +1523,10 @@ 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/{api}").route(web::get().to(debug_handler))) .service( diff --git a/chain/network/src/peer_manager/peer_manager_actor.rs b/chain/network/src/peer_manager/peer_manager_actor.rs index 78fc929dacf..a56866c2130 100644 --- a/chain/network/src/peer_manager/peer_manager_actor.rs +++ b/chain/network/src/peer_manager/peer_manager_actor.rs @@ -202,7 +202,7 @@ impl PeerManagerActor { let state = state.clone(); let clock = clock.clone(); async move { - // Start server if address provided. + // Start server if address provided. if let Some(server_addr) = state.config.node_addr { tracing::debug!(target: "network", at = ?server_addr, "starting public server"); let mut listener = match tcp::Listener::bind(server_addr).await { @@ -636,6 +636,7 @@ impl PeerManagerActor { next_hops: self.state.graph.routing_table.view_route(&announce_account.peer_id), }) .collect(), + tier1_accounts_keys: self.state.accounts_data.load().keys.iter().cloned().collect(), tier1_accounts_data: self.state.accounts_data.load().data.values().cloned().collect(), } } diff --git a/chain/network/src/types.rs b/chain/network/src/types.rs index 9a10152a077..898ce4b5eb8 100644 --- a/chain/network/src/types.rs +++ b/chain/network/src/types.rs @@ -365,6 +365,7 @@ pub struct NetworkInfo { /// Accounts of known block and chunk producers from routing table. pub known_producers: Vec, /// Collected data about the current TIER1 accounts. + pub tier1_accounts_keys: Vec, pub tier1_accounts_data: Vec>, /// TIER1 connections. pub tier1_connections: Vec, diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index 8870f6a4859..6b10b268729 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -383,6 +383,7 @@ pub struct NetworkInfoView { pub num_connected_peers: usize, pub connected_peers: Vec, pub known_producers: Vec, + pub tier1_accounts_keys: Vec, pub tier1_accounts_data: Vec, pub tier1_connections: Vec, } diff --git a/integration-tests/src/tests/client/process_blocks.rs b/integration-tests/src/tests/client/process_blocks.rs index 4178f4cbc5e..85574d81a8b 100644 --- a/integration-tests/src/tests/client/process_blocks.rs +++ b/integration-tests/src/tests/client/process_blocks.rs @@ -1041,6 +1041,7 @@ fn client_sync_headers() { received_bytes_per_sec: 0, known_producers: vec![], tier1_connections: vec![], + tier1_accounts_keys: vec![], tier1_accounts_data: vec![], }) .with_span_context(), diff --git a/tools/chainsync-loadtest/src/network.rs b/tools/chainsync-loadtest/src/network.rs index 7a765489dcd..f83fd13e70c 100644 --- a/tools/chainsync-loadtest/src/network.rs +++ b/tools/chainsync-loadtest/src/network.rs @@ -87,6 +87,7 @@ impl Network { received_bytes_per_sec: 0, known_producers: vec![], tier1_connections: vec![], + tier1_accounts_keys: vec![], tier1_accounts_data: vec![], }), info_futures: Default::default(), diff --git a/tools/mock-node/src/lib.rs b/tools/mock-node/src/lib.rs index 6be34eba38f..693e2c6f9d1 100644 --- a/tools/mock-node/src/lib.rs +++ b/tools/mock-node/src/lib.rs @@ -256,6 +256,7 @@ impl MockPeerManagerActor { received_bytes_per_sec: 0, known_producers: vec![], tier1_connections: vec![], + tier1_accounts_keys: vec![], tier1_accounts_data: vec![], }; let incoming_requests = IncomingRequests::new( From 1f0c57e39cd8962064f90bd57762841497797945 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Mon, 12 Dec 2022 12:23:19 +0100 Subject: [PATCH 087/188] deprecating AnnounceAccount (#8182) Deprecating AnnounceAccount in favor of AccountData, when finding routing target. AnnounceAccount will be still used as a fallback. In the next release, once we confirm that AnnounceAccount is not used any more, we will remove the fallback. We will stop broadcasting AnnounceAccount only once MIN_SUPPORTED_VERSION > 57. --- .../src/peer_manager/network_state/mod.rs | 43 +++++-- .../network/src/peer_manager/tests/routing.rs | 6 +- chain/network/src/peer_manager/tests/tier1.rs | 121 ++++++++++++------ chain/network/src/stats/metrics.rs | 13 ++ 4 files changed, 126 insertions(+), 57 deletions(-) diff --git a/chain/network/src/peer_manager/network_state/mod.rs b/chain/network/src/peer_manager/network_state/mod.rs index 0617ead91b9..ad68ecc5bb7 100644 --- a/chain/network/src/peer_manager/network_state/mod.rs +++ b/chain/network/src/peer_manager/network_state/mod.rs @@ -474,11 +474,11 @@ impl NetworkState { msg: RoutedMessageBody, ) -> bool { let mut success = false; + let accounts_data = self.accounts_data.load(); // All TIER1 messages are being sent over both TIER1 and TIER2 connections for now, // so that we can actually observe the latency/reliability improvements in practice: // for each message we track over which network tier it arrived faster? if tcp::Tier::T1.is_allowed_routed(&msg) { - let accounts_data = self.accounts_data.load(); for key in accounts_data.keys_by_id.get(account_id).iter().flat_map(|keys| keys.iter()) { let data = match accounts_data.data.get(key) { @@ -503,19 +503,34 @@ impl NetworkState { } } - let target = match self.graph.routing_table.account_owner(account_id) { - Some(peer_id) => peer_id, - None => { - // TODO(MarX, #1369): Message is dropped here. Define policy for this case. - metrics::MessageDropped::UnknownAccount.inc(&msg); - tracing::debug!(target: "network", - account_id = ?self.config.validator.as_ref().map(|v|v.account_id()), - to = ?account_id, - ?msg,"Drop message: unknown account", - ); - tracing::trace!(target: "network", known_peers = ?self.graph.routing_table.get_accounts_keys(), "Known peers"); - return false; - } + let peer_id_from_account_data = accounts_data + .keys_by_id + .get(account_id) + .iter() + .flat_map(|keys| keys.iter()) + .flat_map(|key| accounts_data.data.get(key)) + .next() + .map(|data| data.peer_id.clone()); + // Find the target peer_id: + // - first look it up in self.accounts_data + // - if missing, fall back to lookup in self.graph.routing_table + // We want to deprecate self.graph.routing_table.account_owner in the next release. + let target = if let Some(peer_id) = peer_id_from_account_data { + metrics::ACCOUNT_TO_PEER_LOOKUPS.with_label_values(&["AccountData"]).inc(); + peer_id + } else if let Some(peer_id) = self.graph.routing_table.account_owner(account_id) { + metrics::ACCOUNT_TO_PEER_LOOKUPS.with_label_values(&["AnnounceAccount"]).inc(); + peer_id + } else { + // TODO(MarX, #1369): Message is dropped here. Define policy for this case. + metrics::MessageDropped::UnknownAccount.inc(&msg); + tracing::debug!(target: "network", + account_id = ?self.config.validator.as_ref().map(|v|v.account_id()), + to = ?account_id, + ?msg,"Drop message: unknown account", + ); + tracing::trace!(target: "network", known_peers = ?self.graph.routing_table.get_accounts_keys(), "Known peers"); + return false; }; let msg = RawRoutedMessage { target: PeerIdOrHash::PeerId(target), body: msg }; diff --git a/chain/network/src/peer_manager/tests/routing.rs b/chain/network/src/peer_manager/tests/routing.rs index bac7f86bbdc..fa87b27c9f8 100644 --- a/chain/network/src/peer_manager/tests/routing.rs +++ b/chain/network/src/peer_manager/tests/routing.rs @@ -862,7 +862,7 @@ async fn max_num_peers_limit() { drop(pm3); } -// test that TTL is handled property. +/// Test that TTL is handled properly. #[tokio::test] async fn ttl() { init_test_logger(); @@ -916,8 +916,8 @@ async fn ttl() { } } -// After the initial exchange, all subsequent SyncRoutingTable messages are -// expected to contain only the diff of the known data. +/// After the initial exchange, all subsequent SyncRoutingTable messages are +/// expected to contain only the diff of the known data. #[tokio::test] async fn repeated_data_in_sync_routing_table() { init_test_logger(); diff --git a/chain/network/src/peer_manager/tests/tier1.rs b/chain/network/src/peer_manager/tests/tier1.rs index 8e0ac5260e6..ffae873b378 100644 --- a/chain/network/src/peer_manager/tests/tier1.rs +++ b/chain/network/src/peer_manager/tests/tier1.rs @@ -8,10 +8,8 @@ use crate::peer_manager::testonly::Event; use crate::tcp; use crate::testonly::{make_rng, Rng}; use crate::time; -use crate::types::{NetworkRequests, NetworkResponses, PeerManagerMessageRequest}; use near_o11y::testonly::init_test_logger; -use near_o11y::WithSpanContextExt; -use near_primitives::block_header::{Approval, ApprovalInner, ApprovalMessage}; +use near_primitives::block_header::{Approval, ApprovalInner}; use near_primitives::validator_signer::ValidatorSigner; use near_store::db::TestDB; use rand::Rng as _; @@ -48,46 +46,66 @@ async fn establish_connections(clock: &time::Clock, pms: &[&peer_manager::teston } } +// Sends a routed TIER1 message from `from` to `to`. +// Returns the message body that was sent, or None if the routing information was missing. async fn send_tier1_message( rng: &mut Rng, + clock: &time::Clock, from: &peer_manager::testonly::ActorHandler, to: &peer_manager::testonly::ActorHandler, -) { +) -> Option { let from_signer = from.cfg.validator.as_ref().unwrap().signer.clone(); let to_signer = to.cfg.validator.as_ref().unwrap().signer.clone(); let target = to_signer.validator_id().clone(); - let want = make_block_approval(rng, from_signer.as_ref()); - let req = NetworkRequests::Approval { - approval_message: ApprovalMessage { approval: want.clone(), target }, - }; + let want = RoutedMessageBody::BlockApproval(make_block_approval(rng, from_signer.as_ref())); + let clock = clock.clone(); + from.with_state(move |s| async move { + if s.send_message_to_account(&clock, &target, want.clone()) { + Some(want) + } else { + None + } + }) + .await +} + +// Sends a routed TIER1 message from `from` to `to`, then waits until `to` receives it. +// `recv_tier` specifies over which network the message is expected to be actually delivered. +async fn send_and_recv_tier1_message( + rng: &mut Rng, + clock: &time::Clock, + from: &peer_manager::testonly::ActorHandler, + to: &peer_manager::testonly::ActorHandler, + recv_tier: tcp::Tier, +) { let mut events = to.events.from_now(); - let resp = from - .actix - .addr - .send(PeerManagerMessageRequest::NetworkRequests(req).with_span_context()) - .await - .unwrap(); - assert_eq!(NetworkResponses::NoResponse, resp.as_network_response()); + let want = send_tier1_message(rng, clock, from, to).await.expect("routing info not available"); let got = events .recv_until(|ev| match ev { - Event::PeerManager(PME::MessageProcessed(tcp::Tier::T1, PeerMessage::Routed(got))) => { + Event::PeerManager(PME::MessageProcessed(tier, PeerMessage::Routed(got))) + if tier == recv_tier => + { Some(got) } _ => None, }) .await; assert_eq!(from.cfg.node_id(), got.author); - assert_eq!(RoutedMessageBody::BlockApproval(want), got.body); + assert_eq!(want, got.body); } /// Send a message over each connection. -async fn test_clique(rng: &mut Rng, pms: &[&peer_manager::testonly::ActorHandler]) { +async fn test_clique( + rng: &mut Rng, + clock: &time::Clock, + pms: &[&peer_manager::testonly::ActorHandler], +) { for from in pms { for to in pms { if from.cfg.node_id() == to.cfg.node_id() { continue; } - send_tier1_message(rng, from, to).await; + send_and_recv_tier1_message(rng, clock, from, to, tcp::Tier::T1).await; } } } @@ -101,7 +119,7 @@ async fn first_proxy_advertisement() { let rng = &mut rng; let mut clock = time::FakeClock::default(); let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); - let pm = peer_manager::testonly::start( + let pm = start_pm( clock.clock(), near_store::db::TestDB::new(), chain.make_config(rng), @@ -133,7 +151,7 @@ async fn direct_connections() { let mut pms = vec![]; for _ in 0..5 { pms.push( - peer_manager::testonly::start( + start_pm( clock.clock(), near_store::db::TestDB::new(), chain.make_config(rng), @@ -160,7 +178,7 @@ async fn direct_connections() { tracing::info!(target:"test", "Establish connections."); establish_connections(&clock.clock(), &pms[..]).await; tracing::info!(target:"test", "Test clique."); - test_clique(rng, &pms[..]).await; + test_clique(rng, &clock.clock(), &pms[..]).await; } /// Test which spawns N validators, each with 1 proxy. @@ -179,7 +197,7 @@ async fn proxy_connections() { let mut proxies = vec![]; for _ in 0..N { proxies.push( - peer_manager::testonly::start( + start_pm( clock.clock(), near_store::db::TestDB::new(), chain.make_config(rng), @@ -198,20 +216,13 @@ async fn proxy_connections() { peer_id: proxies[i].cfg.node_id(), addr: proxies[i].cfg.node_addr.unwrap(), }]); - validators.push( - peer_manager::testonly::start( - clock.clock(), - near_store::db::TestDB::new(), - cfg, - chain.clone(), - ) - .await, - ); + validators + .push(start_pm(clock.clock(), near_store::db::TestDB::new(), cfg, chain.clone()).await); } let validators: Vec<_> = validators.iter().collect(); // Connect validators and proxies in a star topology. Any connected graph would do. - let hub = peer_manager::testonly::start( + let hub = start_pm( clock.clock(), near_store::db::TestDB::new(), chain.make_config(rng), @@ -238,7 +249,7 @@ async fn proxy_connections() { pm.set_chain_info(chain_info.clone()).await; } establish_connections(&clock.clock(), &all[..]).await; - test_clique(rng, &validators[..]).await; + test_clique(rng, &clock.clock(), &validators[..]).await; } #[tokio::test] @@ -263,7 +274,7 @@ async fn account_keys_change() { pm.set_chain_info(chain_info.clone()).await; } establish_connections(&clock.clock(), &[&v0, &v1, &v2, &hub]).await; - test_clique(rng, &[&v0, &v1]).await; + test_clique(rng, &clock.clock(), &[&v0, &v1]).await; // TIER1 nodes in 2nd epoch are {v0,v2}. let chain_info = peer_manager::testonly::make_chain_info(&chain, &[&v0.cfg, &v2.cfg]); @@ -271,7 +282,7 @@ async fn account_keys_change() { pm.set_chain_info(chain_info.clone()).await; } establish_connections(&clock.clock(), &[&v0, &v1, &v2, &hub]).await; - test_clique(rng, &[&v0, &v2]).await; + test_clique(rng, &clock.clock(), &[&v0, &v2]).await; drop(v0); drop(v1); @@ -313,8 +324,6 @@ async fn proxy_change() { hub.connect_to(&p1.peer_info(), tcp::Tier::T2).await; hub.connect_to(&v0.peer_info(), tcp::Tier::T2).await; hub.connect_to(&v1.peer_info(), tcp::Tier::T2).await; - tracing::info!(target:"dupa","p0 = {}",p0cfg.node_id()); - tracing::info!(target:"dupa","hub = {}",hub.cfg.node_id()); tracing::info!(target:"test", "p0 goes down"); drop(p0); @@ -326,7 +335,7 @@ async fn proxy_change() { tracing::info!(target:"test", "TIER1 connections get established: v0 -> p1 <- v1."); establish_connections(&clock.clock(), &[&v0, &v1, &p1, &hub]).await; tracing::info!(target:"test", "Send message v1 -> v0 over TIER1."); - send_tier1_message(rng, &v1, &v0).await; + send_and_recv_tier1_message(rng, &clock.clock(), &v1, &v0, tcp::Tier::T1).await; // Advance time, so that the new AccountsData has newer timestamp. clock.advance(time::Duration::hours(1)); @@ -340,10 +349,42 @@ async fn proxy_change() { tracing::info!(target:"test", "TIER1 connections get established: v0 -> p0 <- v1."); establish_connections(&clock.clock(), &[&v0, &v1, &p0, &hub]).await; tracing::info!(target:"test", "Send message v1 -> v0 over TIER1."); - send_tier1_message(rng, &v1, &v0).await; + send_and_recv_tier1_message(rng, &clock.clock(), &v1, &v0, tcp::Tier::T1).await; drop(hub); drop(v0); drop(v1); drop(p0); } + +#[tokio::test] +async fn tier2_routing_using_accounts_data() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + tracing::info!(target:"test", "start 2 nodes and connect them"); + let pm0 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let pm1 = start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + pm0.connect_to(&pm1.peer_info(), tcp::Tier::T2).await; + + tracing::info!(target:"test", "Try to send a routed message pm0 -> pm1 over TIER2"); + // It should fail due to missing routing information: neither AccountData or AnnounceAccount is + // broadcasted by default in tests. + // TODO(gprusak): send_tier1_message sends an Approval message, which is not a valid message to + // be sent from a non-TIER1 node. Make it more realistic by sending a Transaction message. + assert!(send_tier1_message(rng, &clock.clock(), &pm0, &pm1).await.is_none()); + + tracing::info!(target:"test", "propagate AccountsData"); + let chain_info = peer_manager::testonly::make_chain_info(&chain, &[&pm1.cfg]); + for pm in [&pm0, &pm1] { + pm.set_chain_info(chain_info.clone()).await; + } + let data: HashSet<_> = pm1.tier1_advertise_proxies(&clock.clock()).await.into_iter().collect(); + pm0.wait_for_accounts_data(&data).await; + + tracing::info!(target:"test", "Send a routed message pm0 -> pm1 over TIER2."); + send_and_recv_tier1_message(rng, &clock.clock(), &pm0, &pm1, tcp::Tier::T2).await; +} diff --git a/chain/network/src/stats/metrics.rs b/chain/network/src/stats/metrics.rs index c54842b3764..c325ee0dd95 100644 --- a/chain/network/src/stats/metrics.rs +++ b/chain/network/src/stats/metrics.rs @@ -349,6 +349,19 @@ pub(crate) static ALREADY_CONNECTED_ACCOUNT: Lazy = Lazy::new(|| { .unwrap() }); +pub(crate) static ACCOUNT_TO_PEER_LOOKUPS: Lazy = Lazy::new(|| { + try_create_int_counter_vec( + "near_account_to_peer_lookups", + "number of lookups of peer_id by account_id (for routed messages)", + // Source is either "AnnounceAccount" or "AccountData". + // We want to deprecate AnnounceAccount, so eventually we want all + // lookups to be done via AccountData. For now AnnounceAccount is + // used as a fallback. + &["source"], + ) + .unwrap() +}); + /// Updated the prometheus metrics about the received routed message `msg`. /// `tier` indicates the network over which the message was transmitted. /// `fastest` indicates whether this message is the first copy of `msg` received - From 783658f5efb4fe6bb3eede34120f211a7387c3f3 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Mon, 12 Dec 2022 12:50:23 +0100 Subject: [PATCH 088/188] migrated connect_to_unbanned_peer test (#8196) Migrated connect_to_unbanned_peer test from integration-tests to near-network. It was flaky when executed in presubmit. I've made it more synchronized and faked out time, so now it shouldn't cause problems any more. I've tested it locally for flakiness (multiple concurrent runs in a loop) and observed no problems. To accomodate the test logic, I reorganized a bit the implementation of some PeerStore methods, but no semantic change was introduced there. --- chain/network/src/peer/peer_actor.rs | 4 +- .../src/peer_manager/peer_manager_actor.rs | 9 +- .../src/peer_manager/peer_store/mod.rs | 165 +++++++++--------- chain/network/src/peer_manager/testonly.rs | 34 +++- .../network/src/peer_manager/tests/routing.rs | 73 +++++++- .../src/tests/network/ban_peers.rs | 27 --- 6 files changed, 181 insertions(+), 131 deletions(-) diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index 989eb78f868..5d4fd3eade2 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -100,7 +100,7 @@ pub(crate) enum ClosingReason { #[error("Received a message of type not allowed on this connection.")] DisallowedMessage, #[error("PeerManager requested to close the connection")] - PeerManager, + PeerManagerRequest, #[error("Received DisconnectMessage from peer")] DisconnectMessage, #[error("Peer clock skew exceeded {MAX_CLOCK_SKEW}")] @@ -1551,7 +1551,7 @@ impl actix::Handler> for PeerActor { ctx, match msg.ban_reason { Some(reason) => ClosingReason::Ban(reason), - None => ClosingReason::PeerManager, + None => ClosingReason::PeerManagerRequest, }, ); } diff --git a/chain/network/src/peer_manager/peer_manager_actor.rs b/chain/network/src/peer_manager/peer_manager_actor.rs index a56866c2130..dffe001a964 100644 --- a/chain/network/src/peer_manager/peer_manager_actor.rs +++ b/chain/network/src/peer_manager/peer_manager_actor.rs @@ -498,10 +498,7 @@ impl PeerManagerActor { let _timer = metrics::PEER_MANAGER_TRIGGER_TIME.with_label_values(&["monitor_peers"]).start_timer(); - self.state.peer_store.unban(&self.clock); - if let Err(err) = self.state.peer_store.update_connected_peers_last_seen(&self.clock) { - tracing::error!(target: "network", ?err, "Failed to update peers last seen time."); - } + self.state.peer_store.update(&self.clock); if self.is_outbound_bootstrap_needed() { let tier2 = self.state.tier2.load(); @@ -547,10 +544,6 @@ impl PeerManagerActor { // If there are too many active connections try to remove some connections self.maybe_stop_active_connection(); - if let Err(err) = self.state.peer_store.remove_expired(&self.clock) { - tracing::error!(target: "network", ?err, "Failed to remove expired peers"); - }; - // Find peers that are not reliable (too much behind) - and make sure that we're not routing messages through them. let unreliable_peers = self.unreliable_peers(); metrics::PEER_UNRELIABLE.set(unreliable_peers.len() as i64); diff --git a/chain/network/src/peer_manager/peer_store/mod.rs b/chain/network/src/peer_manager/peer_store/mod.rs index ea536310b6a..206d61c1bb8 100644 --- a/chain/network/src/peer_manager/peer_store/mod.rs +++ b/chain/network/src/peer_manager/peer_store/mod.rs @@ -18,6 +18,9 @@ mod testonly; #[cfg(test)] mod tests; +/// How often to update the KnownPeerState.last_seen in storage. +const UPDATE_LAST_SEEN_INTERVAL: time::Duration = time::Duration::minutes(1); + /// Level of trust we have about a new (PeerId, Addr) pair. #[derive(Eq, PartialEq, Debug, Clone, Copy)] enum TrustLevel { @@ -224,16 +227,73 @@ impl Inner { } Ok(()) } + + /// Removes peers that are not responding for expiration period. + fn remove_expired(&mut self, now: time::Utc) { + let mut to_remove = vec![]; + for (peer_id, peer_status) in self.peer_states.iter() { + if peer_status.status != KnownPeerStatus::Connected + && now > peer_status.last_seen + self.config.peer_expiration_duration + { + tracing::debug!(target: "network", "Removing peer: last seen {:?} ago", now-peer_status.last_seen); + to_remove.push(peer_id.clone()); + } + } + if let Err(err) = self.delete_peers(&to_remove) { + tracing::error!(target: "network", ?err, "Failed to remove expired peers"); + } + } + + fn unban(&mut self, now: time::Utc) { + let mut to_unban = vec![]; + for (peer_id, peer_state) in &self.peer_states { + if let KnownPeerStatus::Banned(_, ban_time) = peer_state.status { + if now < ban_time + self.config.ban_window { + continue; + } + tracing::info!(target: "network", unbanned = ?peer_id, ?ban_time, "unbanning a peer"); + to_unban.push(peer_id.clone()); + } + } + for peer_id in &to_unban { + if let Err(err) = self.peer_unban(&peer_id) { + tracing::error!(target: "network", ?peer_id, ?err, "Failed to unban a peer"); + } + } + } + + /// Update the 'last_seen' time for all the peers that we're currently connected to. + fn update_last_seen(&mut self, now: time::Utc) { + for (peer_id, peer_state) in self.peer_states.iter_mut() { + if peer_state.status == KnownPeerStatus::Connected + && now > peer_state.last_seen + UPDATE_LAST_SEEN_INTERVAL + { + peer_state.last_seen = now; + if let Err(err) = self.store.set_peer_state(peer_id, peer_state) { + tracing::error!(target: "network", ?peer_id, ?err, "Failed to update peers last seen time."); + } + } + } + } + + /// Cleans up the state of the PeerStore, due to passing time. + /// * it unbans a peer if config.ban_window has passed + /// * it updates KnownPeerStatus.last_seen of the connected peers + /// * it removes peers which were not seen for config.peer_expiration_duration + /// This function should be called periodically. + pub fn update(&mut self, clock: &time::Clock) { + let now = clock.now_utc(); + // TODO(gprusak): these operations could be put into a single DB write transaction. + self.unban(now); + self.update_last_seen(now); + self.remove_expired(now); + } } pub(crate) struct PeerStore(Mutex); impl PeerStore { - pub(crate) fn new( - clock: &time::Clock, - config: Config, - store: store::Store, - ) -> anyhow::Result { + pub fn new(clock: &time::Clock, config: Config, store: store::Store) -> anyhow::Result { let boot_nodes: HashSet<_> = config.boot_nodes.iter().map(|p| p.id.clone()).collect(); // A mapping from `PeerId` to `KnownPeerState`. let mut peerid_2_state = HashMap::default(); @@ -345,29 +405,29 @@ impl PeerStore { self.0.lock().config.blacklist.contains(*addr) } - pub(crate) fn len(&self) -> usize { + pub fn len(&self) -> usize { self.0.lock().peer_states.len() } - pub(crate) fn is_banned(&self, peer_id: &PeerId) -> bool { + pub fn is_banned(&self, peer_id: &PeerId) -> bool { self.0.lock().peer_states.get(peer_id).map_or(false, |s| s.status.is_banned()) } - pub(crate) fn count_banned(&self) -> usize { + pub fn count_banned(&self) -> usize { self.0.lock().peer_states.values().filter(|st| st.status.is_banned()).count() } + pub fn update(&self, clock: &time::Clock) { + self.0.lock().update(clock) + } + #[allow(dead_code)] /// Returns the state of the current peer in memory. - pub(crate) fn get_peer_state(&self, peer_id: &PeerId) -> Option { + pub fn get_peer_state(&self, peer_id: &PeerId) -> Option { self.0.lock().peer_states.get(peer_id).cloned() } - pub(crate) fn peer_connected( - &self, - clock: &time::Clock, - peer_info: &PeerInfo, - ) -> anyhow::Result<()> { + pub fn peer_connected(&self, clock: &time::Clock, peer_info: &PeerInfo) -> anyhow::Result<()> { let mut inner = self.0.lock(); inner.add_signed_peer(clock, peer_info.clone())?; let mut store = inner.store.clone(); @@ -377,29 +437,7 @@ impl PeerStore { Ok(store.set_peer_state(&peer_info.id, entry)?) } - /// Update the 'last_seen' time for all the peers that we're currently connected to. - pub(crate) fn update_connected_peers_last_seen( - &self, - clock: &time::Clock, - ) -> anyhow::Result<()> { - let mut inner = self.0.lock(); - let mut store = inner.store.clone(); - for (peer_id, peer_state) in inner.peer_states.iter_mut() { - if peer_state.status == KnownPeerStatus::Connected - && clock.now_utc() > peer_state.last_seen.saturating_add(time::Duration::minutes(1)) - { - peer_state.last_seen = clock.now_utc(); - store.set_peer_state(peer_id, peer_state)? - } - } - Ok(()) - } - - pub(crate) fn peer_disconnected( - &self, - clock: &time::Clock, - peer_id: &PeerId, - ) -> anyhow::Result<()> { + pub fn peer_disconnected(&self, clock: &time::Clock, peer_id: &PeerId) -> anyhow::Result<()> { let mut inner = self.0.lock(); let mut store = inner.store.clone(); if let Some(peer_state) = inner.peer_states.get_mut(peer_id) { @@ -414,7 +452,7 @@ impl PeerStore { /// Records the last attempt to connect to peer. /// Marks the peer as Unknown (as we failed to connect to it). - pub(crate) fn peer_connection_attempt( + pub fn peer_connection_attempt( &self, clock: &time::Clock, peer_id: &PeerId, @@ -437,7 +475,7 @@ impl PeerStore { Ok(()) } - pub(crate) fn peer_ban( + pub fn peer_ban( &self, clock: &time::Clock, peer_id: &PeerId, @@ -459,7 +497,7 @@ impl PeerStore { /// Return unconnected or peers with unknown status that we can try to connect to. /// Peers with unknown addresses are filtered out. - pub(crate) fn unconnected_peer( + pub fn unconnected_peer( &self, ignore_fn: impl Fn(&KnownPeerState) -> bool, prefer_previously_connected_peer: bool, @@ -499,29 +537,12 @@ impl PeerStore { } /// Return healthy known peers up to given amount. - pub(crate) fn healthy_peers(&self, max_count: usize) -> Vec { + pub fn healthy_peers(&self, max_count: usize) -> Vec { self.0 .lock() .find_peers(|p| matches!(p.status, KnownPeerStatus::Banned(_, _)).not(), max_count) } - /// Removes peers that are not responding for expiration period. - pub(crate) fn remove_expired(&self, clock: &time::Clock) -> anyhow::Result<()> { - let mut inner = self.0.lock(); - let now = clock.now_utc(); - let mut to_remove = vec![]; - for (peer_id, peer_status) in inner.peer_states.iter() { - let diff = now - peer_status.last_seen; - if peer_status.status != KnownPeerStatus::Connected - && diff > inner.config.peer_expiration_duration - { - tracing::debug!(target: "network", "Removing peer: last seen {:?} ago", diff); - to_remove.push(peer_id.clone()); - } - } - inner.delete_peers(&to_remove) - } - /// Adds peers we’ve learned about from other peers. /// /// Identities of the nodes hasn’t been verified in any way. We don’t even @@ -529,7 +550,7 @@ impl PeerStore { /// are nodes there we haven’t received signatures of their peer ID. /// /// See also [`Self::add_direct_peer`] and [`Self::add_signed_peer`]. - pub(crate) fn add_indirect_peers( + pub fn add_indirect_peers( &self, clock: &time::Clock, peers: impl Iterator, @@ -561,34 +582,10 @@ impl PeerStore { /// confirming that identity yet. /// /// See also [`Self::add_indirect_peers`] and [`Self::add_signed_peer`]. - pub(crate) fn add_direct_peer( - &self, - clock: &time::Clock, - peer_info: PeerInfo, - ) -> anyhow::Result<()> { + pub fn add_direct_peer(&self, clock: &time::Clock, peer_info: PeerInfo) -> anyhow::Result<()> { self.0.lock().add_peer(clock, peer_info, TrustLevel::Direct) } - pub fn unban(&self, clock: &time::Clock) { - let mut inner = self.0.lock(); - let now = clock.now_utc(); - let mut to_unban = vec![]; - for (peer_id, peer_state) in &inner.peer_states { - if let KnownPeerStatus::Banned(_, ban_time) = peer_state.status { - if now < ban_time + inner.config.ban_window { - continue; - } - tracing::info!(target: "network", unbanned = ?peer_id, ?ban_time, "unbanning a peer"); - to_unban.push(peer_id.clone()); - } - } - for peer_id in &to_unban { - if let Err(err) = inner.peer_unban(&peer_id) { - tracing::error!(target: "network", ?err, "Failed to unban a peer"); - } - } - } - pub fn load(&self) -> HashMap { self.0.lock().peer_states.clone() } diff --git a/chain/network/src/peer_manager/testonly.rs b/chain/network/src/peer_manager/testonly.rs index 801b0e2e48f..2e686660170 100644 --- a/chain/network/src/peer_manager/testonly.rs +++ b/chain/network/src/peer_manager/testonly.rs @@ -15,6 +15,7 @@ use crate::testonly::fake_client; use crate::time; use crate::types::{ AccountKeys, ChainInfo, KnownPeerStatus, NetworkRequests, PeerManagerMessageRequest, + ReasonForBan, }; use crate::PeerManagerActor; use near_o11y::WithSpanContextExt; @@ -151,7 +152,7 @@ impl ActorHandler { &self, peer_info: &PeerInfo, tier: tcp::Tier, - ) -> impl 'static + Send + Future { + ) -> impl 'static + Send + Future { let addr = self.actix.addr.clone(); let events = self.events.clone(); let peer_info = peer_info.clone(); @@ -165,7 +166,7 @@ impl ActorHandler { Event::PeerManager(PME::HandshakeCompleted(ev)) if ev.stream_id == stream_id => { - Some(()) + Some(stream_id) } Event::PeerManager(PME::ConnectionClosed(ev)) if ev.stream_id == stream_id => { panic!("PeerManager rejected the handshake") @@ -324,6 +325,25 @@ impl ActorHandler { self.with_state(move |s| async move { s.tier1_advertise_proxies(&clock).await }).await } + pub async fn disconnect_and_ban( + &self, + clock: &time::Clock, + peer_id: &PeerId, + reason: ReasonForBan, + ) { + // TODO(gprusak): make it wait asynchronously for the connection to get closed. + // TODO(gprusak): figure out how to await for both ends to disconnect. + let clock = clock.clone(); + let peer_id = peer_id.clone(); + self.with_state(move |s| async move { s.disconnect_and_ban(&clock, &peer_id, reason) }) + .await + } + + pub async fn peer_store_update(&self, clock: &time::Clock) { + let clock = clock.clone(); + self.with_state(move |s| async move { s.peer_store.update(&clock) }).await; + } + pub async fn send_ping(&self, nonce: u64, target: PeerId) { self.actix .addr @@ -452,7 +472,7 @@ pub(crate) async fn start( cfg: config::NetworkConfig, chain: Arc, ) -> ActorHandler { - let (send, recv) = broadcast::unbounded_channel(); + let (send, mut recv) = broadcast::unbounded_channel(); let actix = ActixSystem::spawn({ let mut cfg = cfg.clone(); let chain = chain.clone(); @@ -464,9 +484,13 @@ pub(crate) async fn start( } }) .await; - let mut h = ActorHandler { cfg, actix, events: recv }; + let h = ActorHandler { cfg, actix, events: recv.clone() }; // Wait for the server to start. - assert_eq!(Event::PeerManager(PME::ServerStarted), h.events.recv().await); + recv.recv_until(|ev| match ev { + Event::PeerManager(PME::ServerStarted) => Some(()), + _ => None, + }) + .await; h.set_chain_info(chain.get_chain_info()).await; h } diff --git a/chain/network/src/peer_manager/tests/routing.rs b/chain/network/src/peer_manager/tests/routing.rs index fa87b27c9f8..2c16145dd68 100644 --- a/chain/network/src/peer_manager/tests/routing.rs +++ b/chain/network/src/peer_manager/tests/routing.rs @@ -14,8 +14,8 @@ use crate::store; use crate::tcp; use crate::testonly::{make_rng, Rng}; use crate::time; -use crate::types::PeerInfo; use crate::types::PeerMessage; +use crate::types::{PeerInfo, ReasonForBan}; use near_o11y::testonly::init_test_logger; use near_primitives::network::PeerId; use near_store::db::TestDB; @@ -600,7 +600,10 @@ async fn test_dropping_duplicate_messages() { wait_for_pong(&mut pm0_ev, Pong { nonce: 1, source: id2.clone() }).await; } -// Awaits until a ConnectionClosed event with the expected reason is seen in the event stream. +/// Awaits until a ConnectionClosed event with the expected reason is seen in the event stream. +/// This helper function should be used in tests with peer manager instances with +/// `config.outbound_enabled = true`, because it makes the order of spawning connections +/// non-deterministic, so we cannot just wait for the first ConnectionClosed event. pub(crate) async fn wait_for_connection_closed( events: &mut broadcast::Receiver, want_reason: ClosingReason, @@ -1268,11 +1271,11 @@ async fn archival_node() { tracing::info!(target:"test", "connect node 4 to node 0 and wait for pm0 to close a connection"); pm4.send_outbound_connect(&pm0.peer_info(), tcp::Tier::T2).await; - wait_for_connection_closed(&mut pm0_ev, ClosingReason::PeerManager).await; + wait_for_connection_closed(&mut pm0_ev, ClosingReason::PeerManagerRequest).await; tracing::info!(target:"test", "connect node 1 to node 0 and wait for pm0 to close a connection"); pm1.send_outbound_connect(&pm0.peer_info(), tcp::Tier::T2).await; - wait_for_connection_closed(&mut pm0_ev, ClosingReason::PeerManager).await; + wait_for_connection_closed(&mut pm0_ev, ClosingReason::PeerManagerRequest).await; tracing::info!(target:"test", "check that node 0 and node 1 are still connected"); pm0.wait_for_direct_connection(id1.clone()).await; @@ -1294,9 +1297,69 @@ async fn archival_node() { tracing::info!(target:"test", "[{_step}] connect the chosen node to node 0 and wait for pm0 to close a connection"); chosen.send_outbound_connect(&pm0.peer_info(), tcp::Tier::T2).await; - wait_for_connection_closed(&mut pm0_ev, ClosingReason::PeerManager).await; + wait_for_connection_closed(&mut pm0_ev, ClosingReason::PeerManagerRequest).await; tracing::info!(target:"test", "[{_step}] check that node 0 and node 1 are still connected"); pm0.wait_for_direct_connection(id1.clone()).await; } } + +/// Awaits for ConnectionClosed event for a given `stream_id`. +async fn wait_for_stream_closed( + events: &mut broadcast::Receiver, + stream_id: tcp::StreamId, +) -> ClosingReason { + events + .recv_until(|ev| match ev { + Event::PeerManager(PME::ConnectionClosed(ev)) if ev.stream_id == stream_id => { + Some(ev.reason) + } + _ => None, + }) + .await +} + +/// Check two peers are able to connect again after one peers is banned and unbanned. +#[tokio::test] +async fn connect_to_unbanned_peer() { + init_test_logger(); + let mut rng = make_rng(921853233); + let rng = &mut rng; + let mut clock = time::FakeClock::default(); + let chain = Arc::new(data::Chain::make(&mut clock, rng, 10)); + + let mut pm0 = + start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + let mut pm1 = + start_pm(clock.clock(), TestDB::new(), chain.make_config(rng), chain.clone()).await; + + tracing::info!(target:"test", "pm0 connects to pm1"); + let stream_id = pm0.connect_to(&pm1.peer_info(), tcp::Tier::T2).await; + + tracing::info!(target:"test", "pm1 bans pm0"); + let ban_reason = ReasonForBan::BadBlock; + pm1.disconnect_and_ban(&clock.clock(), &pm0.cfg.node_id(), ban_reason).await; + wait_for_stream_closed(&mut pm0.events, stream_id).await; + assert_eq!( + ClosingReason::Ban(ban_reason), + wait_for_stream_closed(&mut pm1.events, stream_id).await + ); + + tracing::info!(target:"test", "pm0 fails to reconnect to pm1"); + let got_reason = pm1 + .start_inbound(chain.clone(), pm0.cfg.clone()) + .await + .manager_fail_handshake(&clock.clock()) + .await; + assert_eq!(ClosingReason::RejectedByPeerManager(RegisterPeerError::Banned), got_reason); + + tracing::info!(target:"test", "pm1 unbans pm0"); + clock.advance(pm1.cfg.peer_store.ban_window); + pm1.peer_store_update(&clock.clock()).await; + + tracing::info!(target:"test", "pm0 reconnects to pm1"); + pm0.connect_to(&pm1.peer_info(), tcp::Tier::T2).await; + + drop(pm0); + drop(pm1); +} diff --git a/integration-tests/src/tests/network/ban_peers.rs b/integration-tests/src/tests/network/ban_peers.rs index de9f3cfe773..ebe371bb0c2 100644 --- a/integration-tests/src/tests/network/ban_peers.rs +++ b/integration-tests/src/tests/network/ban_peers.rs @@ -22,30 +22,3 @@ fn dont_connect_to_banned_peer() -> anyhow::Result<()> { start_test(runner) } - -/// Check two peers are able to connect again after one peers is banned and unbanned. -#[test] -fn connect_to_unbanned_peer() -> anyhow::Result<()> { - let mut runner = Runner::new(2, 2) - .enable_outbound() - .use_boot_nodes(vec![0, 1]) - .ban_window(time::Duration::seconds(2)); - - // Check both peers are connected - runner.push(Action::CheckRoutingTable(0, vec![(1, vec![1])])); - runner.push(Action::CheckRoutingTable(1, vec![(0, vec![0])])); - - // Ban peer 1 - runner.push_action(ban_peer(0, 1)); - - runner.push(Action::Wait(time::Duration::milliseconds(1000))); - // During two seconds peer is banned so no connection is possible. - runner.push(Action::CheckRoutingTable(0, vec![])); - runner.push(Action::CheckRoutingTable(1, vec![])); - - // After two seconds peer is unbanned and they should be able to connect again. - runner.push(Action::CheckRoutingTable(0, vec![(1, vec![1])])); - runner.push(Action::CheckRoutingTable(1, vec![(0, vec![0])])); - - start_test(runner) -} From d95a5f58d998c69cb8d4e965ad6b0a440cf3f233 Mon Sep 17 00:00:00 2001 From: wacban Date: Mon, 12 Dec 2022 12:41:34 +0000 Subject: [PATCH 089/188] [chore] cleanup after shardnet (#8171) --- Makefile | 3 --- chain/chain/Cargo.toml | 2 -- chain/client/Cargo.toml | 2 -- chain/indexer/Cargo.toml | 3 --- chain/network/Cargo.toml | 2 -- chain/network/src/config_json.rs | 8 +------- core/primitives/Cargo.toml | 3 --- core/primitives/src/epoch_manager.rs | 3 --- core/primitives/src/shard_layout.rs | 12 ----------- core/primitives/src/version.rs | 30 +++++----------------------- nearcore/Cargo.toml | 2 -- nearcore/src/config.rs | 4 ++-- neard/Cargo.toml | 6 ------ tools/ping/src/cli.rs | 11 ++-------- tools/state-parts/src/cli.rs | 4 ++-- 15 files changed, 12 insertions(+), 83 deletions(-) diff --git a/Makefile b/Makefile index 90e9d92188c..243fe66ef99 100644 --- a/Makefile +++ b/Makefile @@ -91,9 +91,6 @@ sandbox-release: neard-sandbox-release neard-sandbox-release: cargo build -p neard --features sandbox --release -shardnet-release: - cargo build -p neard --release --features shardnet - .PHONY: docker-nearcore docker-nearcore-nightly release neard debug .PHONY: perf-release perf-debug nightly-release nightly-debug assertions-release sandbox diff --git a/chain/chain/Cargo.toml b/chain/chain/Cargo.toml index ab0115886f3..81873d0ba75 100644 --- a/chain/chain/Cargo.toml +++ b/chain/chain/Cargo.toml @@ -50,8 +50,6 @@ no_cache = ["near-store/no_cache"] protocol_feature_flat_state = ["near-store/protocol_feature_flat_state"] protocol_feature_reject_blocks_with_outdated_protocol_version = ["near-primitives/protocol_feature_reject_blocks_with_outdated_protocol_version"] -shardnet = ["protocol_feature_reject_blocks_with_outdated_protocol_version"] - nightly = [ "nightly_protocol", ] diff --git a/chain/client/Cargo.toml b/chain/client/Cargo.toml index 408a927b0a2..1debd8665ad 100644 --- a/chain/client/Cargo.toml +++ b/chain/client/Cargo.toml @@ -71,5 +71,3 @@ sandbox = [ "near-client-primitives/sandbox", "near-chain/sandbox", ] -# Shardnet is the experimental network that we deploy for chunk-only producer testing. -shardnet = ["near-network/shardnet"] diff --git a/chain/indexer/Cargo.toml b/chain/indexer/Cargo.toml index 91d4b927208..2b33e18d01e 100644 --- a/chain/indexer/Cargo.toml +++ b/chain/indexer/Cargo.toml @@ -27,6 +27,3 @@ near-o11y = { path = "../../core/o11y" } near-primitives = { path = "../../core/primitives" } near-store = { path = "../../core/store" } node-runtime = { path = "../../runtime/runtime" } - -[features] -shardnet = ["nearcore/shardnet", "near-client/shardnet", "near-primitives/shardnet"] diff --git a/chain/network/Cargo.toml b/chain/network/Cargo.toml index 4632dfa1703..82136551947 100644 --- a/chain/network/Cargo.toml +++ b/chain/network/Cargo.toml @@ -65,5 +65,3 @@ performance_stats = [ "near-rust-allocator-proxy", ] test_features = [] - -shardnet = [] diff --git a/chain/network/src/config_json.rs b/chain/network/src/config_json.rs index 696d03fda6b..e1e5d5c1b6b 100644 --- a/chain/network/src/config_json.rs +++ b/chain/network/src/config_json.rs @@ -55,13 +55,7 @@ fn default_peer_expiration_duration() -> Duration { // If non-zero - we'll skip sending tombstones during initial sync and for that many seconds after start. fn default_skip_tombstones() -> i64 { - // Enable by default in shardnet only. - if cfg!(feature = "shardnet") { - // Skip sending tombstones during sync and 240 seconds after start. - 240 - } else { - 0 - } + 0 } #[derive(Serialize, Deserialize, Clone, Debug)] diff --git a/core/primitives/Cargo.toml b/core/primitives/Cargo.toml index 34d37428d87..c8f7df32aca 100644 --- a/core/primitives/Cargo.toml +++ b/core/primitives/Cargo.toml @@ -61,9 +61,6 @@ nightly = [ nightly_protocol = [] -# Shardnet is the experimental network that we deploy for chunk-only producer testing. -shardnet = ["protocol_feature_reject_blocks_with_outdated_protocol_version"] - [dev-dependencies] assert_matches.workspace = true bencher.workspace = true diff --git a/core/primitives/src/epoch_manager.rs b/core/primitives/src/epoch_manager.rs index 8d4155dcddd..90af47c855f 100644 --- a/core/primitives/src/epoch_manager.rs +++ b/core/primitives/src/epoch_manager.rs @@ -126,9 +126,6 @@ impl AllEpochConfig { config.validator_max_kickout_stake_perc = 30; } } - if checked_feature!("shardnet", ShardnetShardLayoutUpgrade, protocol_version) { - config.shard_layout = ShardLayout::shardnet_upgrade_shard_layout(); - } config } } diff --git a/core/primitives/src/shard_layout.rs b/core/primitives/src/shard_layout.rs index ad460595df9..a8f99f5731d 100644 --- a/core/primitives/src/shard_layout.rs +++ b/core/primitives/src/shard_layout.rs @@ -166,18 +166,6 @@ impl ShardLayout { ) } - pub fn shardnet_upgrade_shard_layout() -> ShardLayout { - ShardLayout::v1( - vec!["v3sweat.shardnet.near".parse().unwrap()], - vec!["fffffffffffff", "mmmmmmmmmmmmm", "uuuuuuuuuuuuu"] - .into_iter() - .map(|s| s.parse().unwrap()) - .collect(), - Some(vec![vec![1], vec![2], vec![3], vec![0, 4]]), - 2, - ) - } - /// Given a parent shard id, return the shard uids for the shards in the current shard layout that /// are split from this parent shard. If this shard layout has no parent shard layout, return None pub fn get_split_shard_uids(&self, parent_shard_id: ShardId) -> Option> { diff --git a/core/primitives/src/version.rs b/core/primitives/src/version.rs index 345c8cc349c..70f60d87aa5 100644 --- a/core/primitives/src/version.rs +++ b/core/primitives/src/version.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -149,14 +147,11 @@ pub enum ProtocolFeature { Ed25519Verify, #[cfg(feature = "protocol_feature_reject_blocks_with_outdated_protocol_version")] RejectBlocksWithOutdatedProtocolVersions, - #[cfg(feature = "shardnet")] - ShardnetShardLayoutUpgrade, } /// Both, outgoing and incoming tcp connections to peers, will be rejected if `peer's` /// protocol version is lower than this. -pub const PEER_MIN_ALLOWED_PROTOCOL_VERSION: ProtocolVersion = - if cfg!(feature = "shardnet") { PROTOCOL_VERSION - 1 } else { STABLE_PROTOCOL_VERSION - 2 }; +pub const PEER_MIN_ALLOWED_PROTOCOL_VERSION: ProtocolVersion = STABLE_PROTOCOL_VERSION - 2; /// Current protocol version used on the mainnet. /// Some features (e. g. FixStorageUsage) require that there is at least one epoch with exactly @@ -167,8 +162,6 @@ const STABLE_PROTOCOL_VERSION: ProtocolVersion = 57; pub const PROTOCOL_VERSION: ProtocolVersion = if cfg!(feature = "nightly_protocol") { // On nightly, pick big enough version to support all features. 132 -} else if cfg!(feature = "shardnet") { - 102 } else { // Enable all stable features. STABLE_PROTOCOL_VERSION @@ -182,12 +175,8 @@ pub const PROTOCOL_VERSION: ProtocolVersion = if cfg!(feature = "nightly_protoco /// it’s set according to the schedule for that protocol upgrade. Release /// candidates usually have separate schedule to final releases. pub const PROTOCOL_UPGRADE_SCHEDULE: Lazy = Lazy::new(|| { - if cfg!(feature = "shardnet") { - ProtocolUpgradeVotingSchedule::from_str("2022-09-05 15:00:00").unwrap() - } else { - // Update to according to schedule when making a release. - ProtocolUpgradeVotingSchedule::default() - } + // Update to according to schedule when making a release. + ProtocolUpgradeVotingSchedule::default() }); /// Gives new clients an option to upgrade without announcing that they support @@ -236,8 +225,7 @@ impl ProtocolFeature { ProtocolFeature::ChunkOnlyProducers | ProtocolFeature::MaxKickoutStake => 56, ProtocolFeature::AccountIdInFunctionCallPermission => 57, - // Nightly & shardnet features, this is to make feature MaxKickoutStake not enabled on - // shardnet + // Nightly features #[cfg(feature = "protocol_feature_fix_staking_threshold")] ProtocolFeature::FixStakingThreshold => 126, #[cfg(feature = "protocol_feature_fix_contract_loading_cost")] @@ -245,15 +233,7 @@ impl ProtocolFeature { #[cfg(feature = "protocol_feature_ed25519_verify")] ProtocolFeature::Ed25519Verify => 131, #[cfg(feature = "protocol_feature_reject_blocks_with_outdated_protocol_version")] - ProtocolFeature::RejectBlocksWithOutdatedProtocolVersions => { - if cfg!(feature = "shardnet") { - 102 - } else { - 132 - } - } - #[cfg(feature = "shardnet")] - ProtocolFeature::ShardnetShardLayoutUpgrade => 102, + ProtocolFeature::RejectBlocksWithOutdatedProtocolVersions => 132, } } } diff --git a/nearcore/Cargo.toml b/nearcore/Cargo.toml index 47f9d168714..2599c7b73c4 100644 --- a/nearcore/Cargo.toml +++ b/nearcore/Cargo.toml @@ -136,6 +136,4 @@ sandbox = [ ] io_trace = ["near-vm-runner/io_trace"] -shardnet = ["near-network/shardnet"] - cold_store = ["near-store/cold_store"] diff --git a/nearcore/src/config.rs b/nearcore/src/config.rs index 905bfad1f5a..6bc24db8438 100644 --- a/nearcore/src/config.rs +++ b/nearcore/src/config.rs @@ -884,7 +884,7 @@ pub fn init_configs( genesis.to_file(&dir.join(config.genesis_file)); info!(target: "near", "Generated mainnet genesis file in {}", dir.display()); } - "testnet" | "betanet" | "shardnet" => { + "testnet" | "betanet" => { if test_seed.is_some() { bail!("Test seed is not supported for {chain_id}"); } @@ -1304,7 +1304,7 @@ pub fn load_config( None => Genesis::from_file(&genesis_file, genesis_validation), }; - if matches!(genesis.config.chain_id.as_ref(), "mainnet" | "testnet" | "betanet" | "shardnet") { + if matches!(genesis.config.chain_id.as_ref(), "mainnet" | "testnet" | "betanet") { // Make sure validators tracks all shards, see // https://github.com/near/nearcore/issues/7388 anyhow::ensure!(!config.tracked_shards.is_empty(), diff --git a/neard/Cargo.toml b/neard/Cargo.toml index f2abf708a3b..be84e3315a5 100644 --- a/neard/Cargo.toml +++ b/neard/Cargo.toml @@ -72,12 +72,6 @@ nightly = [ ] nightly_protocol = ["nearcore/nightly_protocol"] -# Shardnet is the experimental network that we deploy for chunk-only producer testing. -shardnet = [ - "near-primitives/shardnet", - "nearcore/shardnet", -] - # Compile with option to emit a detailed trace of IO operations and their # results that can be replayed on the estimator. To produce the output, compile # with this flag and then enable it at runtime with `--record-io-trace=path` option. diff --git a/tools/ping/src/cli.rs b/tools/ping/src/cli.rs index 601dec0ba48..ad243fbb9ce 100644 --- a/tools/ping/src/cli.rs +++ b/tools/ping/src/cli.rs @@ -17,11 +17,11 @@ pub struct PingCommand { chain_id: String, #[clap(long)] /// genesis hash to use in the Handshake we send. This must be provided if --chain-id - /// is not one of "mainnet", "testnet" or "shardnet" + /// is not "mainnet" or "testnet" genesis_hash: Option, #[clap(long)] /// head height to use in the Handshake we send. This must be provided if --chain-id - /// is not one of "mainnet", "testnet" or "shardnet" + /// is not "mainnet" or "testnet" head_height: Option, /// Protocol version to advertise in our handshake #[clap(long)] @@ -111,13 +111,6 @@ pub static CHAIN_INFO: &[ChainInfo] = &[ 34, 162, 137, 113, 220, 51, 15, 0, 153, 223, 148, 55, 148, 16, ]), }, - ChainInfo { - chain_id: "shardnet", - genesis_hash: CryptoHash([ - 23, 22, 21, 53, 29, 32, 253, 218, 219, 182, 221, 220, 200, 18, 11, 102, 161, 16, 96, - 127, 219, 141, 160, 109, 150, 121, 215, 174, 108, 67, 47, 110, - ]), - }, ]; fn parse_account_filter>(filename: P) -> std::io::Result> { diff --git a/tools/state-parts/src/cli.rs b/tools/state-parts/src/cli.rs index eb6656309d3..03da2624691 100644 --- a/tools/state-parts/src/cli.rs +++ b/tools/state-parts/src/cli.rs @@ -21,12 +21,12 @@ pub struct StatePartsCommand { #[clap(long)] /// genesis hash to use in the Handshake we send. This must be provided if --chain-id - /// is not one of "mainnet", "testnet" or "shardnet" + /// is not "mainnet" or "testnet" genesis_hash: Option, #[clap(long)] /// head height to use in the Handshake we send. This must be provided if --chain-id - /// is not one of "mainnet", "testnet" or "shardnet" + /// is not "mainnet" or "testnet" head_height: Option, /// Protocol version to advertise in our handshake From 56cc704a8c7d9a311ddb0974f151793e7ec676f3 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Mon, 12 Dec 2022 15:24:36 +0100 Subject: [PATCH 090/188] doc: add text about re-request reviews (#8208) This text will probably not be read very often but still documenting github UI in case it helps someone. --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4e28aa0f06b..29a7e0c9ae8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -117,6 +117,10 @@ who belongs either to the super owners or the normal owners group. well. They will review your tests, and make sure that they can convince themselves the test coverage is adequate before they even look into the change, so make sure you tested all the corner cases. +- it is normal to sometimes require multiple rounds of reviews to get a PR + merged. If your PR received some feedback from a reviewer, use the [github + UI](https://stackoverflow.com/questions/40893008/how-to-resume-review-process-after-updating-pull-request-at-github) + to re-request a review. The author is also free to directly request reviews from specific persons [through the github From a2bf920e16b3659fde319ac098f2b7b12a856ab0 Mon Sep 17 00:00:00 2001 From: wacban Date: Mon, 12 Dec 2022 20:29:30 +0000 Subject: [PATCH 091/188] [feat] Add --tracked-shards cmd line arg to the LocalnetCmd (#8187) --- integration-tests/src/node/mod.rs | 4 +- nearcore/src/config.rs | 70 +++++++++++++++++++++++++++++++ neard/src/cli.rs | 19 +++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/integration-tests/src/node/mod.rs b/integration-tests/src/node/mod.rs index d3111963476..13431f79feb 100644 --- a/integration-tests/src/node/mod.rs +++ b/integration-tests/src/node/mod.rs @@ -145,14 +145,14 @@ fn near_configs_to_node_configs( pub fn create_nodes(num_nodes: usize, prefix: &str) -> Vec { let (configs, validator_signers, network_signers, genesis, _) = - create_testnet_configs(1, num_nodes as NumSeats, 0, prefix, true, false, false); + create_testnet_configs(1, num_nodes as NumSeats, 0, prefix, true, false, false, vec![]); near_configs_to_node_configs(configs, validator_signers, network_signers, genesis) } pub fn create_nodes_from_seeds(seeds: Vec) -> Vec { let code = near_test_contracts::rs_contract(); let (configs, validator_signers, network_signers, mut genesis) = - create_testnet_configs_from_seeds(seeds.clone(), 1, 0, true, false, None); + create_testnet_configs_from_seeds(seeds.clone(), 1, 0, true, false, None, vec![]); genesis.config.gas_price_adjustment_rate = Ratio::from_integer(0); for seed in seeds { let mut is_account_record_found = false; diff --git a/nearcore/src/config.rs b/nearcore/src/config.rs index 6bc24db8438..1dc88a5ccd3 100644 --- a/nearcore/src/config.rs +++ b/nearcore/src/config.rs @@ -1055,6 +1055,7 @@ pub fn create_testnet_configs_from_seeds( local_ports: bool, archive: bool, fixed_shards: Option>, + tracked_shards: Vec, ) -> (Vec, Vec, Vec, Genesis) { let num_validator_seats = (seeds.len() - num_non_validator_seats as usize) as NumSeats; let validator_signers = @@ -1110,6 +1111,7 @@ pub fn create_testnet_configs_from_seeds( config.network.skip_sync_wait = num_validator_seats == 1; } config.archive = archive; + config.tracked_shards = tracked_shards.clone(); config.consensus.min_num_peers = std::cmp::min(num_validator_seats as usize - 1, config.consensus.min_num_peers); configs.push(config); @@ -1127,6 +1129,7 @@ pub fn create_testnet_configs( local_ports: bool, archive: bool, fixed_shards: bool, + tracked_shards: Vec, ) -> (Vec, Vec, Vec, Genesis, Vec) { let fixed_shards = if fixed_shards { @@ -1152,6 +1155,7 @@ pub fn create_testnet_configs( local_ports, archive, fixed_shards, + tracked_shards, ); (configs, validator_signers, network_signers, genesis, shard_keys) @@ -1166,6 +1170,7 @@ pub fn init_testnet_configs( local_ports: bool, archive: bool, fixed_shards: bool, + tracked_shards: Vec, ) { let (configs, validator_signers, network_signers, genesis, shard_keys) = create_testnet_configs( num_shards, @@ -1175,6 +1180,7 @@ pub fn init_testnet_configs( local_ports, archive, fixed_shards, + tracked_shards, ); for i in 0..(num_validator_seats + num_non_validator_seats) as usize { let node_dir = dir.join(format!("{}{}", prefix, i)); @@ -1415,3 +1421,67 @@ fn test_config_from_file() { ); } } + +#[test] +fn test_create_testnet_configs() { + let num_shards = 4; + let num_validator_seats = 4; + let num_non_validator_seats = 8; + let prefix = "node"; + let local_ports = true; + + // Set all supported options to true and verify config and genesis. + + let archive = true; + let fixed_shards = true; + let tracked_shards: Vec = vec![0, 1, 3]; + + let (configs, _validator_signers, _network_signers, genesis, _shard_keys) = + create_testnet_configs( + num_shards, + num_validator_seats, + num_non_validator_seats, + prefix, + local_ports, + archive, + fixed_shards, + tracked_shards.clone(), + ); + + assert_eq!(configs.len() as u64, num_validator_seats + num_non_validator_seats); + + for config in configs { + assert_eq!(config.archive, true); + assert_eq!(config.tracked_shards, tracked_shards); + } + + assert_eq!(genesis.config.validators.len(), num_shards as usize); + assert_eq!(genesis.config.shard_layout.num_shards(), num_shards); + + // Set all supported options to false and verify config and genesis. + + let archive = false; + let fixed_shards = false; + let tracked_shards: Vec = vec![]; + + let (configs, _validator_signers, _network_signers, genesis, _shard_keys) = + create_testnet_configs( + num_shards, + num_validator_seats, + num_non_validator_seats, + prefix, + local_ports, + archive, + fixed_shards, + tracked_shards.clone(), + ); + assert_eq!(configs.len() as u64, num_validator_seats + num_non_validator_seats); + + for config in configs { + assert_eq!(config.archive, false); + assert_eq!(config.tracked_shards, tracked_shards); + } + + assert_eq!(genesis.config.validators.len() as u64, num_shards); + assert_eq!(genesis.config.shard_layout.num_shards(), num_shards); +} diff --git a/neard/src/cli.rs b/neard/src/cli.rs index 14e071adc5f..5de2b803350 100644 --- a/neard/src/cli.rs +++ b/neard/src/cli.rs @@ -554,10 +554,28 @@ pub(super) struct LocalnetCmd { /// Whether to configure nodes as archival. #[clap(long)] archival_nodes: bool, + /// Comma separated list of shards to track, the word 'all' to track all shards or the word 'none' to track no shards. + #[clap(long, default_value = "all")] + tracked_shards: String, } impl LocalnetCmd { + fn parse_tracked_shards(tracked_shards: &String, num_shards: NumShards) -> Vec { + if tracked_shards.to_lowercase() == "all" { + return (0..num_shards).collect(); + } + if tracked_shards.to_lowercase() == "none" { + return vec![]; + } + tracked_shards + .split(',') + .map(|shard_id| shard_id.parse::().expect("Shard id must be an integer")) + .collect() + } + pub(super) fn run(self, home_dir: &Path) { + let tracked_shards = Self::parse_tracked_shards(&self.tracked_shards, self.shards); + nearcore::config::init_testnet_configs( home_dir, self.shards, @@ -567,6 +585,7 @@ impl LocalnetCmd { true, self.archival_nodes, self.fixed_shards, + tracked_shards, ); } } From 6ed336d62585e6cf1f7e63bb4c207f0537fa6f44 Mon Sep 17 00:00:00 2001 From: robin-near <111538878+robin-near@users.noreply.github.com> Date: Mon, 12 Dec 2022 14:02:43 -0800 Subject: [PATCH 092/188] [header sync] Use a new algorithm for deriving locators (#8178) --- chain/chain/src/test_utils.rs | 33 +++ chain/client/src/sync/header.rs | 464 ++++++++++++++++++++++---------- 2 files changed, 358 insertions(+), 139 deletions(-) diff --git a/chain/chain/src/test_utils.rs b/chain/chain/src/test_utils.rs index 6406bbb14ae..36507724f52 100644 --- a/chain/chain/src/test_utils.rs +++ b/chain/chain/src/test_utils.rs @@ -4,6 +4,7 @@ mod validator_schedule; use std::cmp::Ordering; use std::sync::Arc; +use chrono::{DateTime, Utc}; use near_primitives::test_utils::create_test_signer; use num_rational::Ratio; use tracing::debug; @@ -77,6 +78,7 @@ pub fn process_block_sync( Ok(accepted_blocks) } +// TODO(#8190) Improve this testing API. pub fn setup() -> (Chain, Arc, Arc) { setup_with_tx_validity_period(100) } @@ -140,6 +142,37 @@ pub fn setup_with_validators( (chain, runtime, signers) } +pub fn setup_with_validators_and_start_time( + vs: ValidatorSchedule, + epoch_length: u64, + tx_validity_period: NumBlocks, + start_time: DateTime, +) -> (Chain, Arc, Vec>) { + let store = create_test_store(); + let signers = + vs.all_block_producers().map(|x| Arc::new(create_test_signer(x.as_str()))).collect(); + let runtime = Arc::new(KeyValueRuntime::new_with_validators(store, vs, epoch_length)); + let chain = Chain::new( + runtime.clone(), + &ChainGenesis { + time: start_time, + height: 0, + gas_limit: 1_000_000, + min_gas_price: 100, + max_gas_price: 1_000_000_000, + total_supply: 1_000_000_000, + gas_price_adjustment_rate: Ratio::from_integer(0), + transaction_validity_period: tx_validity_period, + epoch_length, + protocol_version: PROTOCOL_VERSION, + }, + DoomslugThresholdMode::NoApprovals, + ChainConfig::test(), + ) + .unwrap(); + (chain, runtime, signers) +} + pub fn format_hash(hash: CryptoHash) -> String { let mut hash = hash.to_string(); hash.truncate(6); diff --git a/chain/client/src/sync/header.rs b/chain/client/src/sync/header.rs index 4f75f1d775e..07b9050eeb8 100644 --- a/chain/client/src/sync/header.rs +++ b/chain/client/src/sync/header.rs @@ -7,7 +7,7 @@ use rand::seq::SliceRandom; use rand::thread_rng; use tracing::{debug, warn}; -use near_chain::Chain; +use near_chain::{Chain, ChainStoreAccess}; use near_network::types::{HighestHeightPeerInfo, NetworkRequests, PeerManagerAdapter}; use near_primitives::block::Tip; use near_primitives::hash::CryptoHash; @@ -31,7 +31,6 @@ pub const NS_PER_SECOND: u128 = 1_000_000_000; /// Handles major re-orgs by finding closest header that matches and re-downloading headers from that point. pub struct HeaderSync { network_adapter: Arc, - history_locator: Vec<(BlockHeight, CryptoHash)>, prev_header_sync: (DateTime, BlockHeight, BlockHeight, BlockHeight), syncing_peer: Option, stalling_ts: Option>, @@ -52,7 +51,6 @@ impl HeaderSync { ) -> Self { HeaderSync { network_adapter, - history_locator: vec![], prev_header_sync: (Clock::utc(), 0, 0, 0), syncing_peer: None, stalling_ts: None, @@ -84,7 +82,6 @@ impl HeaderSync { debug!(target: "sync", "Sync: initial transition to Header sync. Header head {} at {}", header_head.last_block_hash, header_head.height, ); - self.history_locator.retain(|&x| x.0 == 0); true } SyncStatus::StateSync { .. } => false, @@ -237,75 +234,52 @@ impl HeaderSync { None } + // The remote side will return MAX_BLOCK_HEADERS headers, starting from the first hash in + // the returned "locator" list that is on their canonical chain. + // + // The locator allows us to start syncing from a reasonably recent common ancestor. Since + // we don't know which fork the remote side is on, we include a few hashes. The first one + // we include is the tip of our chain, and the next one is 2 blocks back (on the same chain, + // by number of blocks (or in other words, by ordinals), not by height), then 4 blocks + // back, then 8 blocks back, etc, until we reach the most recent final block. The reason + // why we stop at the final block is because the consensus guarantees us that the final + // blocks observed by all nodes are on the same fork. fn get_locator(&mut self, chain: &Chain) -> Result, near_chain::Error> { - let tip = chain.header_head()?; - let genesis_height = chain.genesis().height(); - let heights = get_locator_heights(tip.height - genesis_height) - .into_iter() - .map(|h| h + genesis_height) - .collect::>(); - - // For each height we need, we either check if something is close enough from last locator, or go to the db. - let mut locator: Vec<(u64, CryptoHash)> = vec![(tip.height, tip.last_block_hash)]; - for h in heights { - if let Some(x) = close_enough(&self.history_locator, h) { - locator.push(x); - } else { - // Walk backwards to find last known hash. - let last_loc = *locator.last().unwrap(); - if let Ok(header) = chain.get_block_header_by_height(h) { - if header.height() != last_loc.0 { - locator.push((header.height(), *header.hash())); - } - } - } - } - locator.dedup_by(|a, b| a.0 == b.0); - debug!(target: "sync", "Sync: locator: {:?}", locator); - self.history_locator = locator.clone(); - Ok(locator.iter().map(|x| x.1).collect()) - } -} - -/// Check if there is a close enough value to provided height in the locator. -fn close_enough(locator: &[(u64, CryptoHash)], height: u64) -> Option<(u64, CryptoHash)> { - if locator.len() == 0 { - return None; - } - // Check boundaries, if lower than the last. - if locator.last().unwrap().0 >= height { - return locator.last().map(|x| *x); - } - // Higher than first and first is within acceptable gap. - if locator[0].0 < height && height.saturating_sub(127) < locator[0].0 { - return Some(locator[0]); - } - for h in locator.windows(2) { - if height <= h[0].0 && height > h[1].0 { - if h[0].0 - height < height - h[1].0 { - return Some(h[0]); - } else { - return Some(h[1]); - } + let store = chain.store(); + let tip = store.header_head()?; + // We could just get the ordinal from the header, but it's off by one: #8177. + let tip_ordinal = store.get_block_merkle_tree(&tip.last_block_hash)?.size(); + let final_head = store.final_head()?; + let final_head_ordinal = store.get_block_merkle_tree(&final_head.last_block_hash)?.size(); + let ordinals = get_locator_ordinals(final_head_ordinal, tip_ordinal); + let mut locator: Vec = vec![]; + for ordinal in &ordinals { + let block_hash = store.get_block_hash_from_ordinal(*ordinal)?; + locator.push(block_hash); } + debug!(target: "sync", "Sync: locator: {:?} ordinals: {:?}", locator, ordinals); + Ok(locator) } - None } -/// Given height stepping back to 0 in powers of 2 steps. -fn get_locator_heights(height: u64) -> Vec { - let mut current = height; - let mut heights = vec![]; - while current > 0 { - heights.push(current); - if heights.len() >= MAX_BLOCK_HEADER_HASHES as usize - 1 { +/// Step back from highest to lowest ordinal, in powers of 2 steps, limited by MAX_BLOCK_HEADERS +/// heights per step, and limited by MAX_BLOCK_HEADER_HASHES steps in total. +fn get_locator_ordinals(lowest_ordinal: u64, highest_ordinal: u64) -> Vec { + let mut current = highest_ordinal; + let mut ordinals = vec![]; + let mut step = 2; + while current > lowest_ordinal && ordinals.len() < MAX_BLOCK_HEADER_HASHES as usize - 1 { + ordinals.push(current); + if current <= lowest_ordinal + step { break; } - let next = 2u64.pow(heights.len() as u32); - current = if current > next { current - next } else { 0 }; + current -= step; + // Do not step back more than MAX_BLOCK_HEADERS, as the gap in between would not + // allow us to sync to a more recent block. + step = min(step * 2, MAX_BLOCK_HEADERS); } - heights.push(0); - heights + ordinals.push(lowest_ordinal); + ordinals } #[cfg(test)] @@ -314,14 +288,13 @@ mod test { use std::thread; use near_chain::test_utils::{ - process_block_sync, setup, setup_with_validators, ValidatorSchedule, + process_block_sync, setup, setup_with_validators_and_start_time, ValidatorSchedule, }; use near_chain::{BlockProcessingArtifact, Provenance}; use near_crypto::{KeyType, PublicKey}; use near_network::test_utils::MockPeerManagerAdapter; use near_primitives::block::{Approval, Block, GenesisId}; use near_primitives::network::PeerId; - use near_primitives::test_utils::create_test_signer; use super::*; use near_network::types::{BlockInfo, FullPeerInfo, PeerInfo}; @@ -331,25 +304,45 @@ mod test { use num_rational::Ratio; #[test] - fn test_get_locator_heights() { - assert_eq!(get_locator_heights(0), vec![0]); - assert_eq!(get_locator_heights(1), vec![1, 0]); - assert_eq!(get_locator_heights(2), vec![2, 0]); - assert_eq!(get_locator_heights(3), vec![3, 1, 0]); - assert_eq!(get_locator_heights(10), vec![10, 8, 4, 0]); - assert_eq!(get_locator_heights(100), vec![100, 98, 94, 86, 70, 38, 0]); + fn test_get_locator_ordinals() { + assert_eq!(get_locator_ordinals(0, 0), vec![0]); + assert_eq!(get_locator_ordinals(0, 1), vec![1, 0]); + assert_eq!(get_locator_ordinals(0, 2), vec![2, 0]); + assert_eq!(get_locator_ordinals(0, 3), vec![3, 1, 0]); + assert_eq!(get_locator_ordinals(0, 10), vec![10, 8, 4, 0]); + assert_eq!(get_locator_ordinals(0, 100), vec![100, 98, 94, 86, 70, 38, 0]); assert_eq!( - get_locator_heights(1000), + get_locator_ordinals(0, 1000), vec![1000, 998, 994, 986, 970, 938, 874, 746, 490, 0] ); // Locator is still reasonable size even given large height. assert_eq!( - get_locator_heights(10000), - vec![10000, 9998, 9994, 9986, 9970, 9938, 9874, 9746, 9490, 8978, 7954, 5906, 1810, 0,] + get_locator_ordinals(0, 10000), + vec![ + 10000, 9998, 9994, 9986, 9970, 9938, 9874, 9746, 9490, 8978, 8466, 7954, 7442, + 6930, 6418, 5906, 5394, 4882, 4370, 0 + ] + ); + assert_eq!(get_locator_ordinals(100, 100), vec![100]); + assert_eq!(get_locator_ordinals(100, 101), vec![101, 100]); + assert_eq!(get_locator_ordinals(100, 102), vec![102, 100]); + assert_eq!(get_locator_ordinals(100, 103), vec![103, 101, 100]); + assert_eq!(get_locator_ordinals(100, 110), vec![110, 108, 104, 100]); + assert_eq!(get_locator_ordinals(100, 200), vec![200, 198, 194, 186, 170, 138, 100]); + assert_eq!( + get_locator_ordinals(20000, 21000), + vec![21000, 20998, 20994, 20986, 20970, 20938, 20874, 20746, 20490, 20000] + ); + assert_eq!( + get_locator_ordinals(20000, 30000), + vec![ + 30000, 29998, 29994, 29986, 29970, 29938, 29874, 29746, 29490, 28978, 28466, 27954, + 27442, 26930, 26418, 25906, 25394, 24882, 24370, 20000 + ] ); } - /// Starts two chains that fork of genesis and checks that they can sync heaaders to the longest. + /// Starts two chains that fork of genesis and checks that they can sync headers to the longest. #[test] fn test_sync_headers_fork() { let mock_adapter = Arc::new(MockPeerManagerAdapter::default()); @@ -363,7 +356,9 @@ mod test { let (mut chain, _, signer) = setup(); for _ in 0..3 { let prev = chain.get_block(&chain.head().unwrap().last_block_hash).unwrap(); - let block = Block::empty(&prev, &*signer); + // Have gaps in the chain, so we don't have final blocks (i.e. last final block is + // genesis). Otherwise we violate consensus invariants. + let block = Block::empty_with_height(&prev, prev.header().height() + 2, &*signer); process_block_sync( &mut chain, &None, @@ -376,7 +371,9 @@ mod test { let (mut chain2, _, signer2) = setup(); for _ in 0..5 { let prev = chain2.get_block(&chain2.head().unwrap().last_block_hash).unwrap(); - let block = Block::empty(&prev, &*signer2); + // Have gaps in the chain, so we don't have final blocks (i.e. last final block is + // genesis). Otherwise we violate consensus invariants. + let block = Block::empty_with_height(&prev, prev.header().height() + 2, &*signer2); process_block_sync( &mut chain2, &None, @@ -418,7 +415,105 @@ mod test { assert_eq!( item, NetworkRequests::BlockHeadersRequest { - hashes: [3, 1, 0] + // chain is 6 -> 4 -> 2 -> 0. + hashes: [6, 2, 0] + .iter() + .map(|i| *chain.get_block_by_height(*i).unwrap().hash()) + .collect(), + peer_id: peer1.peer_info.id + } + ); + } + + #[test] + fn test_sync_headers_fork_from_final_block() { + let mock_adapter = Arc::new(MockPeerManagerAdapter::default()); + let mut header_sync = HeaderSync::new( + mock_adapter.clone(), + TimeDuration::from_secs(10), + TimeDuration::from_secs(2), + TimeDuration::from_secs(120), + 1_000_000_000, + ); + let (mut chain, _, signer) = setup(); + let (mut chain2, _, signer2) = setup(); + for chain in [&mut chain, &mut chain2] { + // Both chains share a common final block at height 3. + for _ in 0..5 { + let prev = chain.get_block(&chain.head().unwrap().last_block_hash).unwrap(); + let block = Block::empty(&prev, &*signer); + process_block_sync( + chain, + &None, + block.into(), + Provenance::PRODUCED, + &mut BlockProcessingArtifact::default(), + ) + .unwrap(); + } + } + for _ in 0..7 { + let prev = chain.get_block(&chain.head().unwrap().last_block_hash).unwrap(); + // Test with huge gaps to make sure we are still able to find locators. + let block = Block::empty_with_height(&prev, prev.header().height() + 1000, &*signer); + process_block_sync( + &mut chain, + &None, + block.into(), + Provenance::PRODUCED, + &mut BlockProcessingArtifact::default(), + ) + .unwrap(); + } + for _ in 0..3 { + let prev = chain2.get_block(&chain2.head().unwrap().last_block_hash).unwrap(); + // Test with huge gaps, but 3 blocks here produce a higher height than the 7 blocks + // above. + let block = Block::empty_with_height(&prev, prev.header().height() + 3100, &*signer2); + process_block_sync( + &mut chain2, + &None, + block.into(), + Provenance::PRODUCED, + &mut BlockProcessingArtifact::default(), + ) + .unwrap(); + } + let mut sync_status = SyncStatus::NoSync; + let peer1 = FullPeerInfo { + peer_info: PeerInfo::random(), + chain_info: near_network::types::PeerChainInfo { + genesis_id: GenesisId { + chain_id: "unittest".to_string(), + hash: *chain.genesis().hash(), + }, + tracked_shards: vec![], + archival: false, + last_block: Some(BlockInfo { + height: chain2.head().unwrap().height, + hash: chain2.head().unwrap().last_block_hash, + }), + }, + }; + let head = chain.head().unwrap(); + assert!(header_sync + .run( + &mut sync_status, + &mut chain, + head.height, + &[>>::into(peer1.clone()).unwrap()] + ) + .is_ok()); + assert!(sync_status.is_syncing()); + // Check that it queried last block, and then stepped down to genesis block to find common block with the peer. + + let item = mock_adapter.pop().unwrap().as_network_requests(); + assert_eq!( + item, + NetworkRequests::BlockHeadersRequest { + // chain is 7005 -> 6005 -> 5005 -> 4005 -> 3005 -> 2005 -> 1005 -> 5 -> 4 -> 3 -> 2 -> 1 -> 0 + // where 3 is final. + hashes: [7005, 5005, 1005, 3] .iter() .map(|i| *chain.get_block_by_height(*i).unwrap().hash()) .collect(), @@ -464,71 +559,15 @@ mod test { }; set_syncing_peer(&mut header_sync); - let vs = ValidatorSchedule::new().block_producers_per_epoch(vec![vec![ - "test0", "test1", "test2", "test3", "test4", - ] - .iter() - .map(|x| x.parse().unwrap()) - .collect()]); - let (chain, _, signers) = setup_with_validators(vs, 1000, 100); + let (chain, _, signer) = setup(); let genesis = chain.get_block(&chain.genesis().hash().clone()).unwrap(); let mut last_block = &genesis; let mut all_blocks = vec![]; - let mut block_merkle_tree = PartialMerkleTree::default(); for i in 0..61 { let current_height = 3 + i * 5; - - let approvals = [None, None, Some("test3"), Some("test4")] - .iter() - .map(|account_id| { - account_id.map(|account_id| { - let signer = create_test_signer(account_id); - Approval::new( - *last_block.hash(), - last_block.header().height(), - current_height, - &signer, - ) - .signature - }) - }) - .collect(); - let (epoch_id, next_epoch_id) = - if last_block.header().prev_hash() == &CryptoHash::default() { - (last_block.header().next_epoch_id().clone(), EpochId(*last_block.hash())) - } else { - ( - last_block.header().epoch_id().clone(), - last_block.header().next_epoch_id().clone(), - ) - }; - let block = Block::produce( - PROTOCOL_VERSION, - PROTOCOL_VERSION, - last_block.header(), - current_height, - last_block.header().block_ordinal() + 1, - last_block.chunks().iter().cloned().collect(), - epoch_id, - next_epoch_id, - None, - approvals, - Ratio::new(0, 1), - 0, - 100, - Some(0), - vec![], - vec![], - &*signers[3], - *last_block.header().next_bp_hash(), - block_merkle_tree.root(), - None, - ); - block_merkle_tree.insert(*block.hash()); - + let block = Block::empty_with_height(last_block, current_height, &*signer); all_blocks.push(block); - last_block = &all_blocks[all_blocks.len() - 1]; } @@ -584,4 +623,151 @@ mod test { assert!(false); } } + + #[test] + fn test_sync_from_very_behind() { + let mock_adapter = Arc::new(MockPeerManagerAdapter::default()); + let mut header_sync = HeaderSync::new( + mock_adapter.clone(), + TimeDuration::from_secs(10), + TimeDuration::from_secs(2), + TimeDuration::from_secs(120), + 1_000_000_000, + ); + + let vs = ValidatorSchedule::new() + .block_producers_per_epoch(vec![vec!["test0".parse().unwrap()]]); + let genesis_time = Clock::utc(); + // Don't bother with epoch switches. It's not relevant. + let (mut chain, _, _) = + setup_with_validators_and_start_time(vs.clone(), 10000, 100, genesis_time); + let (mut chain2, _, signers2) = + setup_with_validators_and_start_time(vs, 10000, 100, genesis_time); + // Set up the second chain with 2000+ blocks. + let mut block_merkle_tree = PartialMerkleTree::default(); + block_merkle_tree.insert(*chain.genesis().hash()); // for genesis block + for _ in 0..(4 * MAX_BLOCK_HEADERS + 10) { + let last_block = chain2.get_block(&chain2.head().unwrap().last_block_hash).unwrap(); + let this_height = last_block.header().height() + 1; + let (epoch_id, next_epoch_id) = + if last_block.header().prev_hash() == &CryptoHash::default() { + (last_block.header().next_epoch_id().clone(), EpochId(*last_block.hash())) + } else { + ( + last_block.header().epoch_id().clone(), + last_block.header().next_epoch_id().clone(), + ) + }; + let block = Block::produce( + PROTOCOL_VERSION, + PROTOCOL_VERSION, + last_block.header(), + this_height, + last_block.header().block_ordinal() + 1, + last_block.chunks().iter().cloned().collect(), + epoch_id, + next_epoch_id, + None, + signers2 + .iter() + .map(|signer| { + Some( + Approval::new( + *last_block.hash(), + last_block.header().height(), + this_height, + signer.as_ref(), + ) + .signature, + ) + }) + .collect(), + Ratio::new(0, 1), + 0, + 100, + Some(0), + vec![], + vec![], + &*signers2[0], + *last_block.header().next_bp_hash(), + block_merkle_tree.root(), + None, + ); + block_merkle_tree.insert(*block.hash()); + chain2.process_block_header(block.header(), &mut Vec::new()).unwrap(); // just to validate + process_block_sync( + &mut chain2, + &None, + block.into(), + Provenance::PRODUCED, + &mut BlockProcessingArtifact::default(), + ) + .unwrap(); + } + let mut sync_status = SyncStatus::NoSync; + let peer1 = FullPeerInfo { + peer_info: PeerInfo::random(), + chain_info: near_network::types::PeerChainInfo { + genesis_id: GenesisId { + chain_id: "unittest".to_string(), + hash: *chain.genesis().hash(), + }, + tracked_shards: vec![], + archival: false, + last_block: Some(BlockInfo { + height: chain2.head().unwrap().height, + hash: chain2.head().unwrap().last_block_hash, + }), + }, + }; + // It should be done in 5 iterations, but give it 10 iterations just in case it would + // get into an infinite loop because of some bug and cause the test to hang. + for _ in 0..10 { + let header_head = chain.header_head().unwrap(); + if header_head.last_block_hash == chain2.header_head().unwrap().last_block_hash { + // sync is done. + break; + } + assert!(header_sync + .run( + &mut sync_status, + &mut chain, + header_head.height, + &[>>::into(peer1.clone()).unwrap()] + ) + .is_ok()); + match sync_status { + SyncStatus::HeaderSync { .. } => {} + _ => panic!("Unexpected sync status: {:?}", sync_status), + } + let message = match mock_adapter.pop() { + Some(message) => message.as_network_requests(), + None => { + panic!("No message was sent; current height: {}", header_head.height); + } + }; + match message { + NetworkRequests::BlockHeadersRequest { hashes, peer_id } => { + assert_eq!(peer_id, peer1.peer_info.id); + let headers = chain2.retrieve_headers(hashes, MAX_BLOCK_HEADERS, None).unwrap(); + assert!(headers.len() > 0, "No headers were returned"); + match chain.sync_block_headers(headers, &mut Vec::new()) { + Ok(_) => {} + Err(e) => { + panic!("Error inserting headers: {:?}", e); + } + } + } + _ => panic!("Unexpected network message: {:?}", message), + } + if chain.header_head().unwrap().height <= header_head.height { + panic!( + "Syncing is not making progress. Head was not updated from {}", + header_head.height + ); + } + } + let new_tip = chain.header_head().unwrap(); + assert_eq!(new_tip.last_block_hash, chain2.head().unwrap().last_block_hash); + } } From 87f0c73125cfb1e07db26c7d8dc0b986bac28a85 Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Tue, 13 Dec 2022 08:42:51 +0100 Subject: [PATCH 093/188] refactor: improve trie prefetching (#8205) --- core/store/src/lib.rs | 4 +- core/store/src/trie/mod.rs | 2 +- .../src/trie/prefetching_trie_storage.rs | 85 +++++++++++------- runtime/runtime/src/lib.rs | 12 ++- runtime/runtime/src/prefetch.rs | 90 +++++++++++-------- 5 files changed, 118 insertions(+), 75 deletions(-) diff --git a/core/store/src/lib.rs b/core/store/src/lib.rs index e90cdc46c42..d9ba0837f7d 100644 --- a/core/store/src/lib.rs +++ b/core/store/src/lib.rs @@ -33,8 +33,8 @@ pub use crate::trie::iterator::{TrieIterator, TrieTraversalItem}; pub use crate::trie::update::{TrieUpdate, TrieUpdateIterator, TrieUpdateValuePtr}; pub use crate::trie::{ estimator, split_state, ApplyStatePartResult, KeyForStateChanges, KeyLookupMode, NibbleSlice, - PartialStorage, PrefetchApi, RawTrieNode, RawTrieNodeWithSize, ShardTries, Trie, TrieAccess, - TrieCache, TrieCachingStorage, TrieChanges, TrieConfig, TrieDBStorage, TrieStorage, + PartialStorage, PrefetchApi, PrefetchError, RawTrieNode, RawTrieNodeWithSize, ShardTries, Trie, + TrieAccess, TrieCache, TrieCachingStorage, TrieChanges, TrieConfig, TrieDBStorage, TrieStorage, WrappedTrieChanges, }; pub use flat_state::FlatStateDelta; diff --git a/core/store/src/trie/mod.rs b/core/store/src/trie/mod.rs index ba3aeeeb3fa..4134ea22f57 100644 --- a/core/store/src/trie/mod.rs +++ b/core/store/src/trie/mod.rs @@ -21,7 +21,7 @@ pub(crate) use crate::trie::config::DEFAULT_SHARD_CACHE_TOTAL_SIZE_LIMIT; use crate::trie::insert_delete::NodesStorage; use crate::trie::iterator::TrieIterator; pub use crate::trie::nibble_slice::NibbleSlice; -pub use crate::trie::prefetching_trie_storage::PrefetchApi; +pub use crate::trie::prefetching_trie_storage::{PrefetchApi, PrefetchError}; pub use crate::trie::shard_tries::{KeyForStateChanges, ShardTries, WrappedTrieChanges}; pub use crate::trie::trie_storage::{TrieCache, TrieCachingStorage, TrieDBStorage, TrieStorage}; use crate::trie::trie_storage::{TrieMemoryPartialStorage, TrieRecordingStorage}; diff --git a/core/store/src/trie/prefetching_trie_storage.rs b/core/store/src/trie/prefetching_trie_storage.rs index f993f4d0ce7..efe0fa8311a 100644 --- a/core/store/src/trie/prefetching_trie_storage.rs +++ b/core/store/src/trie/prefetching_trie_storage.rs @@ -39,6 +39,10 @@ const NUM_IO_THREADS: usize = 8; /// /// Each I/O threads will have its own copy of `TriePrefetchingStorage`, so /// this should remain a cheap object. +/// +/// Please note that this should only be used from the background threads +/// performing prefetching. The main thread uses `PrefetchStagingArea` +/// directly to look up prefetched values before going to the database. #[derive(Clone)] struct TriePrefetchingStorage { /// Store is shared with parent `TrieCachingStorage`. @@ -81,6 +85,14 @@ pub struct PrefetchApi { pub shard_uid: ShardUId, } +#[derive(thiserror::Error, Debug)] +pub enum PrefetchError { + #[error("I/O scheduler input queue is full")] + QueueFull, + #[error("I/O scheduler input queue is disconnected")] + QueueDisconnected, +} + /// Staging area for in-flight prefetch requests and a buffer for prefetched data. /// /// Before starting a pre-fetch, a slot is reserved for it. Once the data is @@ -133,6 +145,18 @@ enum PrefetchSlot { Done(Arc<[u8]>), } +impl PrefetchSlot { + /// Returns amount of memory reserved for a value in the prefetching area. + fn reserved_memory(&self) -> usize { + match self { + PrefetchSlot::Done(value) => value.len(), + PrefetchSlot::PendingFetch | PrefetchSlot::PendingPrefetch => { + PREFETCH_RESERVED_BYTES_PER_SLOT + } + } + } +} + struct SizeTrackedHashMap { map: HashMap, size_bytes: usize, @@ -140,11 +164,18 @@ struct SizeTrackedHashMap { } impl SizeTrackedHashMap { + fn new(shard_id: ShardId) -> Self { + let instance = + Self { map: Default::default(), size_bytes: 0, metrics: StagedMetrics::new(shard_id) }; + instance.update_metrics(); + instance + } + fn insert(&mut self, k: CryptoHash, v: PrefetchSlot) -> Option { - self.size_bytes += Self::reserved_memory(&v); + self.size_bytes += v.reserved_memory(); let dropped = self.map.insert(k, v); if let Some(dropped) = &dropped { - self.size_bytes -= Self::reserved_memory(dropped); + self.size_bytes -= dropped.reserved_memory(); } self.update_metrics(); dropped @@ -153,7 +184,7 @@ impl SizeTrackedHashMap { fn remove(&mut self, k: &CryptoHash) -> Option { let dropped = self.map.remove(k); if let Some(dropped) = &dropped { - self.size_bytes -= Self::reserved_memory(dropped); + self.size_bytes -= dropped.reserved_memory(); } self.update_metrics(); dropped @@ -170,16 +201,6 @@ impl SizeTrackedHashMap { self.metrics.prefetch_staged_items.set(self.map.len() as i64); } - /// Reserved memory capacity for a value from the prefetching area. - fn reserved_memory(slot: &PrefetchSlot) -> usize { - match slot { - PrefetchSlot::Done(value) => value.len(), - PrefetchSlot::PendingFetch | PrefetchSlot::PendingPrefetch => { - PREFETCH_RESERVED_BYTES_PER_SLOT - } - } - } - fn get(&self, key: &CryptoHash) -> Option<&PrefetchSlot> { self.map.get(key) } @@ -289,14 +310,7 @@ impl TriePrefetchingStorage { impl PrefetchStagingArea { fn new(shard_id: ShardId) -> Self { - let inner = InnerPrefetchStagingArea { - slots: SizeTrackedHashMap { - map: Default::default(), - size_bytes: 0, - metrics: StagedMetrics::new(shard_id), - }, - }; - inner.slots.update_metrics(); + let inner = InnerPrefetchStagingArea { slots: SizeTrackedHashMap::new(shard_id) }; Self(Arc::new(Mutex::new(inner))) } @@ -308,7 +322,7 @@ impl PrefetchStagingArea { /// 2: IO thread misses in the shard cache on the same key and starts fetching it again. /// 3: Main thread value is inserted in shard cache. pub(crate) fn release(&self, key: &CryptoHash) { - let mut guard = self.0.lock().expect(POISONED_LOCK_ERR); + let mut guard = self.lock(); let dropped = guard.slots.remove(key); // `Done` is the result after a successful prefetch. // `PendingFetch` means the value has been read without a prefetch. @@ -332,7 +346,7 @@ impl PrefetchStagingArea { /// Of course, that would require prefetching to be moved into an async environment, pub(crate) fn blocking_get(&self, key: CryptoHash) -> Option> { loop { - match self.0.lock().expect(POISONED_LOCK_ERR).slots.get(&key) { + match self.lock().slots.get(&key) { Some(PrefetchSlot::Done(value)) => return Some(value.clone()), Some(_) => (), None => return None, @@ -348,7 +362,7 @@ impl PrefetchStagingArea { } fn insert_fetched(&self, key: CryptoHash, value: Arc<[u8]>) { - self.0.lock().expect(POISONED_LOCK_ERR).slots.insert(key, PrefetchSlot::Done(value)); + self.lock().slots.insert(key, PrefetchSlot::Done(value)); } /// Get prefetched value if available and otherwise atomically insert the @@ -358,7 +372,7 @@ impl PrefetchStagingArea { key: CryptoHash, set_if_empty: PrefetchSlot, ) -> PrefetcherResult { - let mut guard = self.0.lock().expect(POISONED_LOCK_ERR); + let mut guard = self.lock(); let full = guard.slots.size_bytes > MAX_PREFETCH_STAGING_MEMORY - PREFETCH_RESERVED_BYTES_PER_SLOT; match guard.slots.map.get(&key) { @@ -377,6 +391,15 @@ impl PrefetchStagingArea { } } } + + fn clear(&self) { + self.lock().slots.clear(); + } + + #[track_caller] + fn lock(&self) -> std::sync::MutexGuard { + self.0.lock().expect(POISONED_LOCK_ERR) + } } impl PrefetchApi { @@ -415,13 +438,15 @@ impl PrefetchApi { (this, handle) } - /// Returns the argument back if queue is full. pub fn prefetch_trie_key( &self, root: StateRoot, trie_key: TrieKey, - ) -> Result<(), (StateRoot, TrieKey)> { - self.work_queue_tx.try_send((root, trie_key)).map_err(|e| e.into_inner()) + ) -> Result<(), PrefetchError> { + self.work_queue_tx.try_send((root, trie_key)).map_err(|e| match e { + crossbeam::channel::TrySendError::Full(_) => PrefetchError::QueueFull, + crossbeam::channel::TrySendError::Disconnected(_) => PrefetchError::QueueDisconnected, + }) } pub fn start_io_thread( @@ -482,7 +507,7 @@ impl PrefetchApi { /// Clear prefetched staging area from data that has not been picked up by the main thread. pub fn clear_data(&self) { - self.prefetching.0.lock().expect(POISONED_LOCK_ERR).slots.clear(); + self.prefetching.clear(); } } @@ -534,9 +559,7 @@ mod tests { /// Returns the number of prefetched values currently staged. pub fn num_prefetched_and_staged(&self) -> usize { self.prefetching - .0 .lock() - .unwrap() .slots .map .iter() diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index 46411939900..baea4da1b63 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -1169,7 +1169,8 @@ impl Runtime { let mut prefetcher = TriePrefetcher::new_if_enabled(trie.clone()); if let Some(prefetcher) = &mut prefetcher { - let _queue_full = prefetcher.input_transactions(transactions); + // Prefetcher is allowed to fail + _ = prefetcher.prefetch_transactions_data(transactions); } let mut stats = ApplyStats::default(); @@ -1286,7 +1287,8 @@ impl Runtime { // We first process local receipts. They contain staking, local contract calls, etc. if let Some(prefetcher) = &mut prefetcher { prefetcher.clear(); - let _queue_full = prefetcher.input_receipts(&local_receipts); + // Prefetcher is allowed to fail + _ = prefetcher.prefetch_receipts_data(&local_receipts); } for receipt in local_receipts.iter() { if total_gas_burnt < gas_limit { @@ -1313,7 +1315,8 @@ impl Runtime { if let Some(prefetcher) = &mut prefetcher { prefetcher.clear(); - let _queue_full = prefetcher.input_receipts(std::slice::from_ref(&receipt)); + // Prefetcher is allowed to fail + _ = prefetcher.prefetch_receipts_data(std::slice::from_ref(&receipt)); } // Validating the delayed receipt. If it fails, it's likely the state is inconsistent. @@ -1336,7 +1339,8 @@ impl Runtime { // And then we process the new incoming receipts. These are receipts from other shards. if let Some(prefetcher) = &mut prefetcher { prefetcher.clear(); - let _queue_full = prefetcher.input_receipts(&incoming_receipts); + // Prefetcher is allowed to fail + _ = prefetcher.prefetch_receipts_data(&incoming_receipts); } for receipt in incoming_receipts.iter() { // Validating new incoming no matter whether we have available gas or not. We don't diff --git a/runtime/runtime/src/prefetch.rs b/runtime/runtime/src/prefetch.rs index f3deea6f4c1..66378b72a1c 100644 --- a/runtime/runtime/src/prefetch.rs +++ b/runtime/runtime/src/prefetch.rs @@ -48,10 +48,10 @@ use near_primitives::transaction::{Action, SignedTransaction}; use near_primitives::trie_key::TrieKey; use near_primitives::types::AccountId; use near_primitives::types::StateRoot; -use near_store::{PrefetchApi, Trie}; +use near_store::{PrefetchApi, PrefetchError, Trie}; use sha2::Digest; use std::rc::Rc; -use tracing::debug; +use tracing::{debug, warn}; use crate::metrics; /// Transaction runtime view of the prefetching subsystem. @@ -82,10 +82,15 @@ impl TriePrefetcher { None } - /// Start prefetching data for processing the receipts. + /// Starts prefetching data for processing the receipts. /// - /// Returns an error if the prefetching queue is full. - pub(crate) fn input_receipts(&mut self, receipts: &[Receipt]) -> Result<(), ()> { + /// Returns an error if prefetching for any receipt fails. + /// The function is not idempotent; in case of failure, prefetching + /// for some receipts may have been initiated. + pub(crate) fn prefetch_receipts_data( + &mut self, + receipts: &[Receipt], + ) -> Result<(), PrefetchError> { for receipt in receipts.iter() { if let ReceiptEnum::Action(action_receipt) = &receipt.receipt { let account_id = receipt.receiver_id.clone(); @@ -116,13 +121,15 @@ impl TriePrefetcher { Ok(()) } - /// Start prefetching data for processing the transactions. + /// Starts prefetching data for processing the transactions. /// - /// Returns an error if the prefetching queue is full. - pub(crate) fn input_transactions( + /// Returns an error if prefetching for any transaction fails. + /// The function is not idempotent; in case of failure, prefetching + /// for some transactions may have been initiated. + pub(crate) fn prefetch_transactions_data( &mut self, transactions: &[SignedTransaction], - ) -> Result<(), ()> { + ) -> Result<(), PrefetchError> { if self.prefetch_api.enable_receipt_prefetching { for t in transactions { let account_id = t.transaction.signer_id.clone(); @@ -157,23 +164,31 @@ impl TriePrefetcher { self.prefetch_api.clear_data(); } - fn prefetch_trie_key(&self, trie_key: TrieKey) -> Result<(), ()> { - let queue_full = self.prefetch_api.prefetch_trie_key(self.trie_root, trie_key).is_err(); - if queue_full { - self.prefetch_queue_full.inc(); - debug!(target: "prefetcher", "I/O scheduler input queue full, dropping prefetch request"); - Err(()) - } else { - self.prefetch_enqueued.inc(); - Ok(()) - } + fn prefetch_trie_key(&self, trie_key: TrieKey) -> Result<(), PrefetchError> { + let res = self.prefetch_api.prefetch_trie_key(self.trie_root, trie_key); + match res { + Err(PrefetchError::QueueFull) => { + self.prefetch_queue_full.inc(); + debug!(target: "prefetcher", "I/O scheduler input queue is full, dropping prefetch request"); + } + Err(PrefetchError::QueueDisconnected) => { + // This shouldn't have happened, hence logging warning here + warn!(target: "prefetcher", "I/O scheduler input queue is disconnected, dropping prefetch request"); + } + Ok(()) => self.prefetch_enqueued.inc(), + }; + res } /// Prefetcher specifically tuned for SWEAT record batch /// /// Temporary hack, consider removing after merging flat storage, see /// . - fn prefetch_sweat_record_batch(&self, account_id: AccountId, arg: &[u8]) -> Result<(), ()> { + fn prefetch_sweat_record_batch( + &self, + account_id: AccountId, + arg: &[u8], + ) -> Result<(), PrefetchError> { if let Ok(json) = serde_json::de::from_slice::(arg) { if json.is_object() { if let Some(list) = json.get("steps_batch") { @@ -206,11 +221,11 @@ impl TriePrefetcher { mod tests { use super::TriePrefetcher; use near_primitives::{trie_key::TrieKey, types::AccountId}; - use near_store::{ - test_utils::{create_test_store, test_populate_trie}, - ShardTries, ShardUId, Trie, TrieConfig, - }; - use std::{rc::Rc, str::FromStr, time::Duration}; + use near_store::test_utils::{create_test_store, test_populate_trie}; + use near_store::{ShardTries, ShardUId, Trie, TrieConfig}; + use std::rc::Rc; + use std::str::FromStr; + use std::time::{Duration, Instant}; #[test] fn test_basic_prefetch_account() { @@ -293,8 +308,7 @@ mod tests { let prefetch_keys = accounts_to_trie_keys(prefetch); let shard_uids = vec![ShardUId::single_shard()]; - let mut trie_config = TrieConfig::default(); - trie_config.enable_receipt_prefetching = true; + let trie_config = TrieConfig { enable_receipt_prefetching: true, ..TrieConfig::default() }; let store = create_test_store(); let flat_storage_factory = near_store::flat_state::FlatStateFactory::new(store.clone()); let tries = ShardTries::new(store, trie_config, &shard_uids, flat_storage_factory); @@ -313,28 +327,30 @@ mod tests { let prefetcher = TriePrefetcher::new_if_enabled(trie.clone()) .expect("caching storage should have prefetcher"); - let p = &prefetcher.prefetch_api; + let prefetch_api = &prefetcher.prefetch_api; - assert_eq!(p.num_prefetched_and_staged(), 0); + assert_eq!(prefetch_api.num_prefetched_and_staged(), 0); for trie_key in &prefetch_keys { _ = prefetcher.prefetch_trie_key(trie_key.clone()); } std::thread::yield_now(); - // don't use infinite loop to avoid test from ever hanging - for _ in 0..1000 { - if !p.work_queued() { - break; - } - std::thread::sleep(Duration::from_micros(100)); + let wait_work_queue_empty_start = Instant::now(); + while prefetch_api.work_queued() { + std::thread::yield_now(); + // Use timeout to avoid hanging the test + assert!( + wait_work_queue_empty_start.elapsed() < Duration::from_millis(100), + "timeout while waiting for prefetch queue to become empty" + ); } // Request can still be pending. Stop threads and wait for them to finish. std::thread::sleep(Duration::from_micros(1000)); assert_eq!( - p.num_prefetched_and_staged(), + prefetch_api.num_prefetched_and_staged(), expected_prefetched, "unexpected number of prefetched values" ); @@ -345,7 +361,7 @@ mod tests { let _value = trie.get(&storage_key).unwrap(); } assert_eq!( - p.num_prefetched_and_staged(), + prefetch_api.num_prefetched_and_staged(), 0, "prefetching staging area not clear after reading all values from main thread" ); From 506a32867a901c4497bc3b3b943fe8a271568283 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Tue, 13 Dec 2022 10:38:39 +0100 Subject: [PATCH 094/188] attempt to deflake test_dropping_duplicate_messages (#8204) I didn't manage to reproduce it locally, so this is a blind shot, but it won't hurt. --- chain/network/src/peer/peer_actor.rs | 2 +- .../src/peer_manager/tests/accounts_data.rs | 2 ++ .../network/src/peer_manager/tests/routing.rs | 19 +++++++++++++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index 5d4fd3eade2..dd90e18bd1d 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -56,7 +56,7 @@ const MAX_TRANSACTIONS_PER_BLOCK_MESSAGE: usize = 1000; /// Limit cache size of 1000 messages const ROUTED_MESSAGE_CACHE_SIZE: usize = 1000; /// Duplicated messages will be dropped if routed through the same peer multiple times. -const DROP_DUPLICATED_MESSAGES_PERIOD: time::Duration = time::Duration::milliseconds(50); +pub(crate) const DROP_DUPLICATED_MESSAGES_PERIOD: time::Duration = time::Duration::milliseconds(50); /// How often to send the latest block to peers. const SYNC_LATEST_BLOCK_INTERVAL: time::Duration = time::Duration::seconds(60); /// How often to perform a full sync of AccountsData with the peer. diff --git a/chain/network/src/peer_manager/tests/accounts_data.rs b/chain/network/src/peer_manager/tests/accounts_data.rs index faa8a477e3f..d6e04275fe7 100644 --- a/chain/network/src/peer_manager/tests/accounts_data.rs +++ b/chain/network/src/peer_manager/tests/accounts_data.rs @@ -260,4 +260,6 @@ async fn rate_limiting() { let want_max = connections * 12; println!("got {msgs}, want <= {want_max}"); assert!(msgs <= want_max, "got {msgs} messages, want at most {want_max}"); + + drop(pms); } diff --git a/chain/network/src/peer_manager/tests/routing.rs b/chain/network/src/peer_manager/tests/routing.rs index 2c16145dd68..5026f0893c3 100644 --- a/chain/network/src/peer_manager/tests/routing.rs +++ b/chain/network/src/peer_manager/tests/routing.rs @@ -4,7 +4,9 @@ use crate::config::NetworkConfig; use crate::network_protocol::testonly as data; use crate::network_protocol::{Edge, Encoding, Ping, Pong, RoutedMessageBody, RoutingTableUpdate}; use crate::peer; -use crate::peer::peer_actor::{ClosingReason, ConnectionClosedEvent}; +use crate::peer::peer_actor::{ + ClosingReason, ConnectionClosedEvent, DROP_DUPLICATED_MESSAGES_PERIOD, +}; use crate::peer_manager; use crate::peer_manager::peer_manager_actor::Event as PME; use crate::peer_manager::testonly::start as start_pm; @@ -356,6 +358,9 @@ async fn ping_simple() { tracing::info!(target:"test", "await pong at {id0}"); wait_for_pong(&mut pm0_ev, Pong { nonce: 0, source: id1.clone() }).await; + + drop(pm0); + drop(pm1); } // test ping without a direct connection @@ -590,7 +595,7 @@ async fn test_dropping_duplicate_messages() { tracing::info!(target:"test", "await pong at {id0}"); wait_for_pong(&mut pm0_ev, Pong { nonce: 1, source: id2.clone() }).await; - clock.advance(time::Duration::milliseconds(300)); + clock.advance(DROP_DUPLICATED_MESSAGES_PERIOD + time::Duration::milliseconds(1)); tracing::info!(target:"test", "send ping from {id0} to {id2}"); pm0.send_ping(1, id2.clone()).await; @@ -598,6 +603,10 @@ async fn test_dropping_duplicate_messages() { wait_for_ping(&mut pm2_ev, Ping { nonce: 1, source: id0.clone() }).await; tracing::info!(target:"test", "await pong at {id0}"); wait_for_pong(&mut pm0_ev, Pong { nonce: 1, source: id2.clone() }).await; + + drop(pm0); + drop(pm1); + drop(pm2); } /// Awaits until a ConnectionClosed event with the expected reason is seen in the event stream. @@ -1302,6 +1311,12 @@ async fn archival_node() { tracing::info!(target:"test", "[{_step}] check that node 0 and node 1 are still connected"); pm0.wait_for_direct_connection(id1.clone()).await; } + + drop(pm0); + drop(pm1); + drop(pm2); + drop(pm3); + drop(pm4); } /// Awaits for ConnectionClosed event for a given `stream_id`. From b38d8a77d7c419c0dc7b5b3653ba0575de5cbe90 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Tue, 13 Dec 2022 12:05:58 +0100 Subject: [PATCH 095/188] added info about TIER1 to the CHANGELOG (#8209) * TIER1 enabled * applied comments * fixed typos Co-authored-by: near-bulldozer[bot] <73298989+near-bulldozer[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++--- docs/SUMMARY.md | 4 ++++ docs/advanced_configuration/networking.md | 28 +++++++++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 docs/advanced_configuration/networking.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ddbcc2dcf1..0a49bf11b48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,9 @@ * Stabilize `account_id_in_function_call_permission` feature: enforcing validity of account ids in function call permission. -* Enable TIER1 peer discovery. Validator nodes are now exchanging the [public addresses - set in config](https://github.com/near/nearcore/blob/301fb493ea4f6d9b75d7dac7f2b52d00a1b2b709/chain/network/src/config_json.rs#L162). - The TIER1 connections support (direct connections between validators) based on - this discovery mechanism will be added soon. +* Enable TIER1 network. Participants of the BFT consensus (block & chunk producers) now can establish direct TIER1 connections + between each other, which will optimize the communication latency and minimize the number of dropped chunks. + To configure this feature, see [advanced\_configuration/networking](./docs/advanced_configuration/networking.md). ### Non-protocol Changes diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index d309f5380ee..8ba5c2962ae 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -42,6 +42,10 @@ - [Testing Utils](./practices/testing/test_utils.md) - [Protocol Upgrade](./practices/protocol_upgrade.md) +# Advanced configuration + +- [Networking](./advanced_configuration/networking.md) + # Misc - [Misc](./misc/README.md) diff --git a/docs/advanced_configuration/networking.md b/docs/advanced_configuration/networking.md new file mode 100644 index 00000000000..00c8e9d50ac --- /dev/null +++ b/docs/advanced_configuration/networking.md @@ -0,0 +1,28 @@ +This document describes the advanced network options that you can configure by modifying +the "network" section of your "config.json" file. + +### TIER1 network + +Participants of the BFT consensus (block & chunk producers) now can establish direct (aka TIER1) connections +between each other, which will optimize the communication latency and minimize the number of dropped chunks. +If you are a validator, you can enable TIER1 connections by setting the following fields in the config: + +* [public_addrs](https://github.com/near/nearcore/blob/d95a5f58d998c69cb8d4e965ad6b0a440cf3f233/chain/network/src/config_json.rs#L154) + * this is a list of the public addresses (in the format `"@:"`) of trusted nodes, + which are willing to route messages to your node + * this list will be broadcasted to the network, so that other validator nodes can connect to your node. + * if your node has a static public IP, set `public_addrs` to a list with a single entry with public key and address of your node, for example: + `"public_addrs": ["ed25519:86EtEy7epneKyrcJwSWP7zsisTkfDRH5CFVszt4qiQYw@31.192.22.209:24567"]`. + * if your node doesn't have a public IP (for example, it is hidden behind a NAT), set `public_addrs` to + a list (<=10 entries) of proxy nodes that you trust (arbitrary nodes with static public IPs). + * support for nodes with dynamic public IPs is not implemented yet. +* [experimental.tier1_enable_outbound](https://github.com/near/nearcore/blob/d95a5f58d998c69cb8d4e965ad6b0a440cf3f233/chain/network/src/config_json.rs#L213) + * makes your node actively try to establish outbound TIER1 connections (recommended) once it learns about + public addresses of other validator nodes. If disabled, your node won't try to establish outbound TIER1 connections, + but it still may accept incoming TIER1 connections from other nodes. + * currently `false` by default, but will be changed to `true` by default in the future +* [experimental.tier1_enable_inbound](https://github.com/near/nearcore/blob/d95a5f58d998c69cb8d4e965ad6b0a440cf3f233/chain/network/src/config_json.rs#L209) + * makes your node accept inbound TIER1 connections from other validator nodes. + * disable both `tier1_enable_inbound` and `tier1_enable_outbound` if you want to opt out from the TIER1 communication entirely + * disable `tier1_enable_inbound` if you are not a validator AND you don't want your node to act as a proxy for validators. + * `true` by default From 12cb91364dfb976ed0f87eaed324a93e3f44ca42 Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Wed, 14 Dec 2022 10:04:23 +0100 Subject: [PATCH 096/188] refactor: fix clippy erasing_op warning (#8219) --- core/primitives-core/src/config.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/core/primitives-core/src/config.rs b/core/primitives-core/src/config.rs index 25cba9c1299..c9b939d5c5f 100644 --- a/core/primitives-core/src/config.rs +++ b/core/primitives-core/src/config.rs @@ -318,14 +318,16 @@ impl ExtCostsConfig { ExtCosts::storage_remove_ret_value_byte => SAFETY_MULTIPLIER * 3843852, ExtCosts::storage_has_key_base => SAFETY_MULTIPLIER * 18013298875, ExtCosts::storage_has_key_byte => SAFETY_MULTIPLIER * 10263615, - ExtCosts::storage_iter_create_prefix_base => SAFETY_MULTIPLIER * 0, - ExtCosts::storage_iter_create_prefix_byte => SAFETY_MULTIPLIER * 0, - ExtCosts::storage_iter_create_range_base => SAFETY_MULTIPLIER * 0, - ExtCosts::storage_iter_create_from_byte => SAFETY_MULTIPLIER * 0, - ExtCosts::storage_iter_create_to_byte => SAFETY_MULTIPLIER * 0, - ExtCosts::storage_iter_next_base => SAFETY_MULTIPLIER * 0, - ExtCosts::storage_iter_next_key_byte => SAFETY_MULTIPLIER * 0, - ExtCosts::storage_iter_next_value_byte => SAFETY_MULTIPLIER * 0, + // Here it should be `SAFETY_MULTIPLIER * 0` for consistency, but then + // clippy complains with "this operation will always return zero" warning + ExtCosts::storage_iter_create_prefix_base => 0, + ExtCosts::storage_iter_create_prefix_byte => 0, + ExtCosts::storage_iter_create_range_base => 0, + ExtCosts::storage_iter_create_from_byte => 0, + ExtCosts::storage_iter_create_to_byte => 0, + ExtCosts::storage_iter_next_base => 0, + ExtCosts::storage_iter_next_key_byte => 0, + ExtCosts::storage_iter_next_value_byte => 0, ExtCosts::touching_trie_node => SAFETY_MULTIPLIER * 5367318642, ExtCosts::read_cached_trie_node => SAFETY_MULTIPLIER * 760_000_000, ExtCosts::promise_and_base => SAFETY_MULTIPLIER * 488337800, From 01c6f6f8ed6f4bbd15ece896d728145f6f993be5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Dec 2022 09:17:09 +0000 Subject: [PATCH 097/188] build(deps): bump secp256k1 from 0.24.0 to 0.24.2 (#8189) Bumps [secp256k1](https://github.com/rust-bitcoin/rust-secp256k1) from 0.24.0 to 0.24.2.
Changelog

Sourced from secp256k1's changelog.

0.24.2 - 2022-12-05

0.24.1 - 2022-10-25

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=secp256k1&package-manager=cargo&previous-version=0.24.0&new-version=0.24.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) - `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language - `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language - `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language - `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/near/nearcore/network/alerts).
--- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d8b07fd1b6..881c64cbe52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5118,9 +5118,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "secp256k1" -version = "0.24.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7649a0b3ffb32636e60c7ce0d70511eda9c52c658cd0634e194d5a19943aeff" +checksum = "d9512ffd81e3a3503ed401f79c33168b9148c75038956039166cd750eaa037c3" dependencies = [ "rand 0.8.5", "secp256k1-sys", From 1d04d08b38dc1dc788a3f8e64684320c3acf8542 Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Wed, 14 Dec 2022 12:13:02 +0100 Subject: [PATCH 098/188] refactor: disable clippy for near-test-contracts (#8213) This disables clippy for test code under `near-test-contracts`. As far as I understand it is explicitly designed to be "suboptimal" in order to test our runtime, so clippy is not helpful in that case. Can be verified with `cargo clippy -p near-test-contracts` Part of #8145 --- runtime/near-test-contracts/contract-for-fuzzing-rs/src/lib.rs | 2 ++ runtime/near-test-contracts/estimator-contract/src/lib.rs | 1 + runtime/near-test-contracts/test-contract-rs/src/lib.rs | 2 ++ 3 files changed, 5 insertions(+) diff --git a/runtime/near-test-contracts/contract-for-fuzzing-rs/src/lib.rs b/runtime/near-test-contracts/contract-for-fuzzing-rs/src/lib.rs index 5d7301a834a..32ea3e7b949 100644 --- a/runtime/near-test-contracts/contract-for-fuzzing-rs/src/lib.rs +++ b/runtime/near-test-contracts/contract-for-fuzzing-rs/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::all)] + use std::mem::size_of; #[allow(unused)] diff --git a/runtime/near-test-contracts/estimator-contract/src/lib.rs b/runtime/near-test-contracts/estimator-contract/src/lib.rs index a88ecbf1909..e61309390f0 100644 --- a/runtime/near-test-contracts/estimator-contract/src/lib.rs +++ b/runtime/near-test-contracts/estimator-contract/src/lib.rs @@ -1,5 +1,6 @@ #![no_std] #![allow(non_snake_case)] +#![allow(clippy::all)] #[panic_handler] #[no_mangle] diff --git a/runtime/near-test-contracts/test-contract-rs/src/lib.rs b/runtime/near-test-contracts/test-contract-rs/src/lib.rs index c0975d3b879..2638be47657 100644 --- a/runtime/near-test-contracts/test-contract-rs/src/lib.rs +++ b/runtime/near-test-contracts/test-contract-rs/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::all)] + use std::mem::size_of; #[allow(unused)] From 7e767d31073dad66e032fe5904f9cdc80cd8c773 Mon Sep 17 00:00:00 2001 From: Ekleog-NEAR <96595974+Ekleog-NEAR@users.noreply.github.com> Date: Wed, 14 Dec 2022 12:35:56 +0100 Subject: [PATCH 099/188] document findings on database format (#8216) --- docs/SUMMARY.md | 1 + docs/misc/database.md | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 docs/misc/database.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 8ba5c2962ae..6887a3b4579 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -49,3 +49,4 @@ # Misc - [Misc](./misc/README.md) +- [Database Format](./misc/database.md) diff --git a/docs/misc/database.md b/docs/misc/database.md new file mode 100644 index 00000000000..4ae9b1087c5 --- /dev/null +++ b/docs/misc/database.md @@ -0,0 +1,36 @@ +# On-Disk Database Format + +We store the database in RocksDB. This document is an attempt to give hints about how to navigate it. + +## RocksDB + +- The column families are defined in `DBCol`, defined in `core/store/src/columns.rs` +- The column families are seen on the rocksdb side as per the `col_name` function defined in `core/store/src/db/rocksdb.rs` + +## The Trie (col5) + +- The trie is stored in column family `State`, number 5 +- In this family, each key is of the form `ShardUId | CryptoHash` where `ShardUId: u64` and `CryptoHash: [u8; 32]` + +## All Historical State Changes (col35) + +- The state changes are stored in column family `StateChanges`, number 35 +- In this family, each key is of the form `BlockHash | Column | AdditionalInfo` where: + + `BlockHash: [u8; 32]` is the block hash for this change + + `Column: u8` is defined near the top of `core/primitives/src/trie_key.rs` + + `AdditionalInfo` depends on `Column` and is can be found in the code for the `TrieKey` struct, same file as `Column` + +### Contract Deployments + +- Contract deployments happen with `Column = 0x01` +- `AdditionalInfo` is the account id for which the contract is being deployed +- The key value contains the contract code alongside other pieces of data. It is possible to extract the contract code by removing everything until the wasm magic number, 0061736D01000000 +- As such, it is possible to dump all the contracts that were ever deployed on-chain using this command: + ``` + ldb --db=. scan --column_family=col35 --hex | \ + grep -E '^0x.{64}01' | \ + sed 's/^.*0061736D01000000/0061736D01000000/' | \ + grep -v ' : ' + ``` + (Note that the last grep is required because not every such value appears to contain contract code) + We should implement a feature to state-viewer that’d allow better visualization of this data, but in the meantime this seems to work. From 9f0f36a25c576898130f9feb40eeacd2b2283ab2 Mon Sep 17 00:00:00 2001 From: Saketh Are Date: Wed, 14 Dec 2022 13:47:07 -0500 Subject: [PATCH 100/188] TIER1 debug: fix usage of AccountKey vs PeerId (#8220) This PR corrects usage of AccountKey and PeerId in the TIER1 debug page. Sample pending TIER1 connections: Screen Shot 2022-12-13 at 5 44 08 PM Sample after connections are established: Screen Shot 2022-12-13 at 5 44 32 PM --- chain/jsonrpc/res/tier1_network_info.html | 77 +++++++++++------------ 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/chain/jsonrpc/res/tier1_network_info.html b/chain/jsonrpc/res/tier1_network_info.html index fd7a111a025..e57a599af97 100644 --- a/chain/jsonrpc/res/tier1_network_info.html +++ b/chain/jsonrpc/res/tier1_network_info.html @@ -16,29 +16,31 @@ let network_info = data.detailed_debug_status.network_info; // tier1_connections contains TIER1 nodes we are currently connected to - network_info.tier1_connections.forEach(function (peer, index) { - let peer_id = peer.peer_id; - rendered.add(peer_id); - - let validator = ""; - data.detailed_debug_status.network_info.known_producers.forEach(element => { - if (element.peer_id == peer_id) { - validator = element.account_id; - } - }); - + network_info.tier1_connections.forEach(function (peer) { + let account_key = ""; let proxies = new Array(); + network_info.tier1_accounts_data.forEach(account => { - if (account.peer_id == peer_id) { + if (account.peer_id == peer.peer_id) { + account_key = account.account_key; account.proxies.forEach(proxy => { proxies.push(proxy.peer_id.substr(8, 5) + "...@" + proxy.addr); }); } }); + rendered.add(account_key); + + let account_id = ""; + network_info.known_producers.forEach(producer => { + if (producer.peer_id == peer.peer_id) { + account_id = producer.account_id; + } + }); + let last_ping = convertTime(peer.last_time_received_message_millis) let last_ping_class = "" - if (data.node_public_key == peer_id) { + if (data.node_public_key == account_key) { last_ping = "n/a" } else if (peer.last_time_received_message_millis > 60 * 1000) { last_ping_class = "peer_far_behind"; @@ -46,9 +48,10 @@ let row = $('.js-tbody-peers').append($('') .append($('').append(add_debug_port_link(peer.addr))) - .append($('').append(validator)) - .append($('').append(peer.peer_id.substr(8, 5) + "...")) + .append($('').append(account_key.substr(8, 5) + "...")) + .append($('').append(account_id)) .append($('').append("[" + proxies.join(",") + "]")) + .append($('').append(peer.peer_id.substr(8, 5) + "...")) .append($('').append(last_ping).addClass(last_ping_class)) .append($('').append(JSON.stringify(peer.tracked_shards))) .append($('').append(JSON.stringify(peer.archival))) @@ -60,17 +63,17 @@ // tier1_accounts_data contains data about TIER1 nodes we would like to connect to network_info.tier1_accounts_data.forEach(account => { - let peer_id = account.peer_id; + let account_key = account.account_key; - if (rendered.has(peer_id)) { + if (rendered.has(account_key)) { return; } - rendered.add(peer_id); + rendered.add(account_key); - let validator = ""; - data.detailed_debug_status.network_info.known_producers.forEach(element => { - if (element.peer_id == peer_id) { - validator = element.account_id; + let account_id = ""; + network_info.known_producers.forEach(producer => { + if (producer.peer_id == account.peer_id) { + account_id = producer.account_id; } }); @@ -81,9 +84,10 @@ let row = $('.js-tbody-peers').append($('') .append($('')) - .append($('').append(validator)) - .append($('').append(peer_id.substr(8, 5) + "...")) + .append($('').append(account_key.substr(8, 5) + "...")) + .append($('').append(account_id)) .append($('').append("[" + proxies.join(",") + "]")) + .append($('').append(account.peer_id.substr(8, 5) + "...")) .append($('')) .append($('')) .append($('')) @@ -93,24 +97,18 @@ ) }); - // tier1_accounts_keys contains accounts whose data we would like to connect - network_info.tier1_accounts_keys.forEach(peer_id => { - if (rendered.has(peer_id)) { + // tier1_accounts_keys contains accounts whose data we would like to collect + network_info.tier1_accounts_keys.forEach(account_key => { + if (rendered.has(account_key)) { return; } - rendered.add(peer_id); - - let validator = ""; - data.detailed_debug_status.network_info.known_producers.forEach(element => { - if (element.peer_id == peer_id) { - validator = element.account_id; - } - }); + rendered.add(account_key); let row = $('.js-tbody-peers').append($('') .append($('')) - .append($('').append(validator)) - .append($('').append(peer_id.substr(8, 5) + "...")) + .append($('').append(account_key.substr(8, 5) + "...")) + .append($('')) + .append($('')) .append($('')) .append($('')) .append($('')) @@ -143,9 +141,10 @@

Address - Validator - PeerId + AccountKey + AccountId Proxies + PeerId Last ping Tracked Shards Archival From 902890169af4c2f92fba971c829cd165fb9f9fe8 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Wed, 14 Dec 2022 23:23:06 +0100 Subject: [PATCH 101/188] primitives: get rid of Cursor (#8194) As per the TODO, take `&[u8; 36]` in ValueRef::decode and stop using Cursor. This makes the method considerably simpler without making the call site more complex. While dealing with ValueRef, stop using Cursor in account_id_to_shard_id as well. Issue: https://github.com/near/nearcore/issues/7327 --- Cargo.lock | 2 +- core/primitives/Cargo.toml | 2 +- core/primitives/src/shard_layout.rs | 8 +++----- core/primitives/src/state.rs | 16 +++++----------- core/store/src/flat_state.rs | 15 +++++++++------ 5 files changed, 19 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 881c64cbe52..abe200c9e34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3412,7 +3412,6 @@ dependencies = [ "assert_matches", "bencher", "borsh", - "byteorder", "bytesize", "cfg-if 1.0.0", "chrono", @@ -3425,6 +3424,7 @@ dependencies = [ "near-o11y", "near-primitives-core", "near-rpc-error-macro", + "near-stdx", "near-vm-errors", "num-rational", "once_cell", diff --git a/core/primitives/Cargo.toml b/core/primitives/Cargo.toml index c8f7df32aca..1a249b991b1 100644 --- a/core/primitives/Cargo.toml +++ b/core/primitives/Cargo.toml @@ -15,7 +15,6 @@ This crate provides the base set of primitives used by other nearcore crates [dependencies] arbitrary.workspace = true borsh.workspace = true -byteorder.workspace = true bytesize.workspace = true cfg-if.workspace = true chrono.workspace = true @@ -31,6 +30,7 @@ reed-solomon-erasure.workspace = true serde.workspace = true serde_json.workspace = true smart-default.workspace = true +stdx.workspace = true strum.workspace = true thiserror.workspace = true tracing.workspace = true diff --git a/core/primitives/src/shard_layout.rs b/core/primitives/src/shard_layout.rs index a8f99f5731d..77329b279a4 100644 --- a/core/primitives/src/shard_layout.rs +++ b/core/primitives/src/shard_layout.rs @@ -1,13 +1,10 @@ use std::cmp::Ordering::Greater; use std::{fmt, str}; -use byteorder::{LittleEndian, ReadBytesExt}; use serde::{Deserialize, Serialize}; -use near_primitives_core::hash::hash; use near_primitives_core::types::ShardId; -use crate::borsh::maybestd::io::Cursor; use crate::hash::CryptoHash; use crate::types::{AccountId, NumShards}; use std::collections::HashMap; @@ -239,8 +236,9 @@ impl ShardLayout { pub fn account_id_to_shard_id(account_id: &AccountId, shard_layout: &ShardLayout) -> ShardId { match shard_layout { ShardLayout::V0(ShardLayoutV0 { num_shards, .. }) => { - let mut cursor = Cursor::new(hash(account_id.as_ref().as_bytes()).0); - cursor.read_u64::().expect("Must not happened") % (num_shards) + let hash = CryptoHash::hash_bytes(account_id.as_ref().as_bytes()); + let (bytes, _) = stdx::split_array::<32, 8, 24>(hash.as_bytes()); + u64::from_le_bytes(*bytes) % num_shards } ShardLayout::V1(ShardLayoutV1 { fixed_shards, boundary_accounts, .. }) => { for (shard_id, fixed_account) in fixed_shards.iter().enumerate() { diff --git a/core/primitives/src/state.rs b/core/primitives/src/state.rs index 797d4990fdb..374c7b974cc 100644 --- a/core/primitives/src/state.rs +++ b/core/primitives/src/state.rs @@ -1,8 +1,6 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use byteorder::{LittleEndian, ReadBytesExt}; use near_primitives_core::hash::{hash, CryptoHash}; -use std::io::{Cursor, Read}; /// State value reference. Used to charge fees for value length before retrieving the value itself. #[derive(BorshSerialize, BorshDeserialize, Clone, PartialEq, Eq, Debug)] @@ -22,14 +20,10 @@ impl ValueRef { } /// Decode value reference from the raw byte array. - /// TODO (#7327): use &[u8; 36] and get rid of Cursor; also check that there are no leftover bytes - pub fn decode(bytes: &[u8]) -> Result { - let mut cursor = Cursor::new(bytes); - let value_length = cursor.read_u32::()?; - let mut arr = [0; 32]; - cursor.read_exact(&mut arr)?; - let value_hash = CryptoHash(arr); - Ok(ValueRef { length: value_length, hash: value_hash }) + pub fn decode(bytes: &[u8; 36]) -> Self { + let (length, hash) = stdx::split_array(bytes); + let length = u32::from_le_bytes(*length); + ValueRef { length, hash: CryptoHash(*hash) } } } @@ -45,7 +39,7 @@ mod tests { let mut value_ref_ser = [0u8; 36]; value_ref_ser[0..4].copy_from_slice(&old_value_ref.length.to_le_bytes()); value_ref_ser[4..36].copy_from_slice(&old_value_ref.hash.0); - let value_ref = ValueRef::decode(&value_ref_ser).unwrap(); + let value_ref = ValueRef::decode(&value_ref_ser); assert_eq!(value_ref.length, value.len() as u32); assert_eq!(value_ref.hash, hash(&value)); } diff --git a/core/store/src/flat_state.rs b/core/store/src/flat_state.rs index 2415786eeb5..f56010c398f 100644 --- a/core/store/src/flat_state.rs +++ b/core/store/src/flat_state.rs @@ -561,12 +561,15 @@ pub mod store_helper { pub(crate) fn get_ref(store: &Store, key: &[u8]) -> Result, FlatStorageError> { let raw_ref = store .get(crate::DBCol::FlatState, key) - .map_err(|_| FlatStorageError::StorageInternalError); - match raw_ref? { - Some(bytes) => ValueRef::decode(&bytes) - .map(Some) - .map_err(|_| FlatStorageError::StorageInternalError), - None => Ok(None), + .map_err(|_| FlatStorageError::StorageInternalError)?; + if let Some(raw_ref) = raw_ref { + let bytes = raw_ref + .as_slice() + .try_into() + .map_err(|_| FlatStorageError::StorageInternalError)?; + Ok(Some(ValueRef::decode(bytes))) + } else { + Ok(None) } } From c6c51d59d8cd5cfcf15cd895d2460d22093b95c9 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Thu, 15 Dec 2022 14:03:07 +0100 Subject: [PATCH 102/188] runtime: create common unit tests for MemoryLike implementations (#8226) Rather than duplicating test cases for WasmerMemory and Wasmer2Memory, create a shared function which tests arbitrary implementation of the MemoryLike trait. This reduces code duplication and makes sure that if new tests are added, they are added to all relevant structs. --- runtime/near-vm-runner/src/memory.rs | 64 +----------------- runtime/near-vm-runner/src/tests.rs | 69 ++++++++++++++++++++ runtime/near-vm-runner/src/wasmer2_runner.rs | 69 +------------------- 3 files changed, 74 insertions(+), 128 deletions(-) diff --git a/runtime/near-vm-runner/src/memory.rs b/runtime/near-vm-runner/src/memory.rs index 8d5661ccd3f..3949ae75567 100644 --- a/runtime/near-vm-runner/src/memory.rs +++ b/runtime/near-vm-runner/src/memory.rs @@ -54,65 +54,7 @@ impl MemoryLike for WasmerMemory { } } -#[cfg(test)] -mod tests { - use near_vm_logic::MemoryLike; - - use wasmer_types::WASM_PAGE_SIZE; - - #[test] - fn memory_read() { - let memory = super::WasmerMemory::new(1, 1); - let mut buffer = vec![42; WASM_PAGE_SIZE]; - memory.read_memory(0, &mut buffer).unwrap(); - // memory should be zeroed at creation. - assert!(buffer.iter().all(|&v| v == 0)); - } - - #[test] - fn fits_memory() { - const PAGE: u64 = WASM_PAGE_SIZE as u64; - - let memory = super::WasmerMemory::new(1, 1); - - memory.fits_memory(0, PAGE).unwrap(); - memory.fits_memory(PAGE / 2, PAGE as u64 / 2).unwrap(); - memory.fits_memory(PAGE - 1, 1).unwrap(); - memory.fits_memory(PAGE, 0).unwrap(); - - memory.fits_memory(0, PAGE + 1).unwrap_err(); - memory.fits_memory(1, PAGE).unwrap_err(); - memory.fits_memory(PAGE - 1, 2).unwrap_err(); - memory.fits_memory(PAGE, 1).unwrap_err(); - } - - #[test] - fn memory_read_oob() { - let memory = super::WasmerMemory::new(1, 1); - let mut buffer = vec![42; WASM_PAGE_SIZE + 1]; - assert!(memory.read_memory(0, &mut buffer).is_err()); - } - - #[test] - fn memory_write() { - let mut memory = super::WasmerMemory::new(1, 1); - let mut buffer = vec![42; WASM_PAGE_SIZE]; - memory.write_memory(WASM_PAGE_SIZE as u64 / 2, &buffer[..WASM_PAGE_SIZE / 2]).unwrap(); - memory.read_memory(0, &mut buffer).unwrap(); - assert!(buffer[..WASM_PAGE_SIZE / 2].iter().all(|&v| v == 0)); - assert!(buffer[WASM_PAGE_SIZE / 2..].iter().all(|&v| v == 42)); - // Now the buffer is half 0s and half 42s - - memory.write_memory(0, &buffer[WASM_PAGE_SIZE / 4..3 * (WASM_PAGE_SIZE / 4)]).unwrap(); - memory.read_memory(0, &mut buffer).unwrap(); - assert!(buffer[..WASM_PAGE_SIZE / 4].iter().all(|&v| v == 0)); - assert!(buffer[WASM_PAGE_SIZE / 4..].iter().all(|&v| v == 42)); - } - - #[test] - fn memory_write_oob() { - let mut memory = super::WasmerMemory::new(1, 1); - let mut buffer = vec![42; WASM_PAGE_SIZE + 1]; - assert!(memory.write_memory(0, &mut buffer).is_err()); - } +#[test] +fn test_memory_like() { + crate::tests::test_memory_like(|| Box::new(WasmerMemory::new(1, 1))); } diff --git a/runtime/near-vm-runner/src/tests.rs b/runtime/near-vm-runner/src/tests.rs index bf396a0b8e5..599345a34e4 100644 --- a/runtime/near-vm-runner/src/tests.rs +++ b/runtime/near-vm-runner/src/tests.rs @@ -63,3 +63,72 @@ fn prepaid_loading_gas(bytes: usize) -> u64 { 0 } } + +/// Tests for implementation of MemoryLike interface. +/// +/// The `factory` returns a [`MemoryLike`] implementation to be tested. The +/// memory must be configured for indices `0..WASM_PAGE_SIZE` to be valid. +/// +/// Panics if any of the tests fails. +pub(crate) fn test_memory_like(factory: impl FnOnce() -> Box) { + const PAGE: u64 = wasmer_types::WASM_PAGE_SIZE as u64; + + struct TestContext { + mem: Box, + buf: [u8; PAGE as usize + 1], + } + + impl TestContext { + fn test_read(&mut self, offset: u64, len: u64, value: u8) { + self.buf.fill(!value); + self.mem.fits_memory(offset, len).unwrap(); + self.mem.read_memory(offset, &mut self.buf[..(len as usize)]).unwrap(); + assert!(self.buf[..(len as usize)].iter().all(|&v| v == value)); + } + + fn test_write(&mut self, offset: u64, len: u64, value: u8) { + self.buf.fill(value); + self.mem.fits_memory(offset, len).unwrap(); + self.mem.write_memory(offset, &self.buf[..(len as usize)]).unwrap(); + } + + fn test_oob(&mut self, offset: u64, len: u64) { + self.buf.fill(42); + self.mem.fits_memory(offset, len).unwrap_err(); + self.mem.read_memory(offset, &mut self.buf[..(len as usize)]).unwrap_err(); + assert!(self.buf[..(len as usize)].iter().all(|&v| v == 42)); + self.mem.write_memory(offset, &self.buf[..(len as usize)]).unwrap_err(); + } + } + + let mut ctx = TestContext { mem: factory(), buf: [0; PAGE as usize + 1] }; + + // Test memory is initialised to zero. + ctx.test_read(0, PAGE, 0); + ctx.test_read(PAGE, 0, 0); + ctx.test_read(0, PAGE / 2, 0); + ctx.test_read(PAGE / 2, PAGE / 2, 0); + + // Test writing works. + ctx.test_write(0, PAGE / 2, 42); + ctx.test_read(0, PAGE / 2, 42); + ctx.test_read(PAGE / 2, PAGE / 2, 0); + + ctx.test_write(PAGE / 4, PAGE / 4, 24); + ctx.test_read(0, PAGE / 4, 42); + ctx.test_read(PAGE / 4, PAGE / 4, 24); + ctx.test_read(PAGE / 2, PAGE / 2, 0); + + // Zero memory. + ctx.test_write(0, PAGE, 0); + ctx.test_read(0, PAGE, 0); + + // Test out-of-bounds checks. + ctx.test_oob(0, PAGE + 1); + ctx.test_oob(1, PAGE); + ctx.test_oob(PAGE - 1, 2); + ctx.test_oob(PAGE, 1); + + // None of the writes in OOB should have any effect. + ctx.test_read(0, PAGE, 0); +} diff --git a/runtime/near-vm-runner/src/wasmer2_runner.rs b/runtime/near-vm-runner/src/wasmer2_runner.rs index 96489d9f7ba..9208d21b527 100644 --- a/runtime/near-vm-runner/src/wasmer2_runner.rs +++ b/runtime/near-vm-runner/src/wasmer2_runner.rs @@ -655,8 +655,6 @@ impl crate::runner::VM for Wasmer2VM { #[cfg(test)] mod tests { - use near_vm_logic::MemoryLike; - use assert_matches::assert_matches; use wasmer_types::WASM_PAGE_SIZE; @@ -669,35 +667,6 @@ mod tests { memory.get_memory_buffer(WASM_PAGE_SIZE as u64, 0).unwrap(); } - #[test] - fn fits_memory() { - const PAGE: u64 = WASM_PAGE_SIZE as u64; - - let memory = super::Wasmer2Memory::new(1, 1).unwrap(); - - memory.fits_memory(0, PAGE).unwrap(); - memory.fits_memory(PAGE / 2, PAGE as u64 / 2).unwrap(); - memory.fits_memory(PAGE - 1, 1).unwrap(); - memory.fits_memory(PAGE, 0).unwrap(); - - memory.fits_memory(0, PAGE + 1).unwrap_err(); - memory.fits_memory(1, PAGE).unwrap_err(); - memory.fits_memory(PAGE - 1, 2).unwrap_err(); - memory.fits_memory(PAGE, 1).unwrap_err(); - } - - #[test] - fn get_memory_buffer_oob1() { - let memory = super::Wasmer2Memory::new(1, 1).unwrap(); - assert!(memory.get_memory_buffer(1 + WASM_PAGE_SIZE as u64, 0).is_err()); - } - - #[test] - fn get_memory_buffer_oob2() { - let memory = super::Wasmer2Memory::new(1, 1).unwrap(); - assert!(memory.get_memory_buffer(WASM_PAGE_SIZE as u64, 1).is_err()); - } - #[test] fn memory_data_offset() { let memory = super::Wasmer2Memory::new(1, 1).unwrap(); @@ -710,41 +679,7 @@ mod tests { } #[test] - fn memory_read() { - let memory = super::Wasmer2Memory::new(1, 1).unwrap(); - let mut buffer = vec![42; WASM_PAGE_SIZE]; - memory.read_memory(0, &mut buffer).unwrap(); - // memory should be zeroed at creation. - assert!(buffer.iter().all(|&v| v == 0)); - } - - #[test] - fn memory_read_oob() { - let memory = super::Wasmer2Memory::new(1, 1).unwrap(); - let mut buffer = vec![42; WASM_PAGE_SIZE + 1]; - assert!(memory.read_memory(0, &mut buffer).is_err()); - } - - #[test] - fn memory_write() { - let mut memory = super::Wasmer2Memory::new(1, 1).unwrap(); - let mut buffer = vec![42; WASM_PAGE_SIZE]; - memory.write_memory(WASM_PAGE_SIZE as u64 / 2, &buffer[..WASM_PAGE_SIZE / 2]).unwrap(); - memory.read_memory(0, &mut buffer).unwrap(); - assert!(buffer[..WASM_PAGE_SIZE / 2].iter().all(|&v| v == 0)); - assert!(buffer[WASM_PAGE_SIZE / 2..].iter().all(|&v| v == 42)); - // Now the buffer is half 0s and half 42s - - memory.write_memory(0, &buffer[WASM_PAGE_SIZE / 4..3 * (WASM_PAGE_SIZE / 4)]).unwrap(); - memory.read_memory(0, &mut buffer).unwrap(); - assert!(buffer[..WASM_PAGE_SIZE / 4].iter().all(|&v| v == 0)); - assert!(buffer[WASM_PAGE_SIZE / 4..].iter().all(|&v| v == 42)); - } - - #[test] - fn memory_write_oob() { - let mut memory = super::Wasmer2Memory::new(1, 1).unwrap(); - let mut buffer = vec![42; WASM_PAGE_SIZE + 1]; - assert!(memory.write_memory(0, &mut buffer).is_err()); + fn test_memory_like() { + crate::tests::test_memory_like(|| Box::new(super::Wasmer2Memory::new(1, 1).unwrap())); } } From 26c07dd9d142f2d241983888f78df04031d77f23 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Thu, 15 Dec 2022 14:49:41 +0100 Subject: [PATCH 103/188] refactor: Remove unnecessary `#[allow(dead_code)]` macros (#8222) As these functions already have the `#[cfg(test)]` macro, they appear to not additionally need `#[allow(dead_code)]`. --- runtime/near-vm-logic/src/logic.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs index df44d77aee8..23bc41d9631 100644 --- a/runtime/near-vm-logic/src/logic.rs +++ b/runtime/near-vm-logic/src/logic.rs @@ -158,19 +158,16 @@ impl<'a> VMLogic<'a> { &self.receipt_manager.action_receipts } - #[allow(dead_code)] #[cfg(test)] pub(crate) fn receipt_manager(&self) -> &ReceiptManager { &self.receipt_manager } - #[allow(dead_code)] #[cfg(test)] pub(crate) fn gas_counter(&self) -> &GasCounter { &self.gas_counter } - #[allow(dead_code)] #[cfg(test)] pub(crate) fn config(&self) -> &VMConfig { &self.config From f35d1c67a11ad9daaa83179afb4c6f3eb75fdc40 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Thu, 15 Dec 2022 15:32:05 +0100 Subject: [PATCH 104/188] refactor: improve documentation and variable naming (#8223) minor tidying up for function level documentation and variable naming to make things more consistent. --- runtime/near-vm-logic/src/gas_counter.rs | 31 +++++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/runtime/near-vm-logic/src/gas_counter.rs b/runtime/near-vm-logic/src/gas_counter.rs index 7183a7cfc60..ed54520863d 100644 --- a/runtime/near-vm-logic/src/gas_counter.rs +++ b/runtime/near-vm-logic/src/gas_counter.rs @@ -87,21 +87,26 @@ impl GasCounter { } } - /// Accounts for burnt and used gas; reports an error if max gas burnt or - /// prepaid gas limit is crossed. Panics when trying to burn more gas than - /// being used, i.e. if `burn_gas > use_gas`. - fn deduct_gas(&mut self, burn_gas: Gas, use_gas: Gas) -> Result<()> { - assert!(burn_gas <= use_gas); - let promise_gas = use_gas - burn_gas; + /// Deducts burnt and used gas. + /// + /// Returns an error if the `max_gax_burnt` or the `prepaid_gas` limits are + /// crossed or there are arithmetic overflows. + /// + /// Panics + /// + /// This function asserts that `gas_burnt <= gas_used` + fn deduct_gas(&mut self, gas_burnt: Gas, gas_used: Gas) -> Result<()> { + assert!(gas_burnt <= gas_used); + let promises_gas = gas_used - gas_burnt; let new_promises_gas = - self.promises_gas.checked_add(promise_gas).ok_or(HostError::IntegerOverflow)?; + self.promises_gas.checked_add(promises_gas).ok_or(HostError::IntegerOverflow)?; let new_burnt_gas = - self.fast_counter.burnt_gas.checked_add(burn_gas).ok_or(HostError::IntegerOverflow)?; + self.fast_counter.burnt_gas.checked_add(gas_burnt).ok_or(HostError::IntegerOverflow)?; let new_used_gas = new_burnt_gas.checked_add(new_promises_gas).ok_or(HostError::IntegerOverflow)?; if new_burnt_gas <= self.max_gas_burnt && new_used_gas <= self.prepaid_gas { use std::cmp::min; - if promise_gas != 0 && !self.is_view { + if promises_gas != 0 && !self.is_view { self.fast_counter.gas_limit = min(self.max_gas_burnt, self.prepaid_gas - new_promises_gas); } @@ -113,10 +118,12 @@ impl GasCounter { } } - // Optimized version of above function for cases where no promises involved. - pub fn burn_gas(&mut self, value: Gas) -> Result<()> { + /// Simpler version of `deduct_gas()` for when no promises are involved. + /// + /// Return an error if there are arithmetic overflows. + pub fn burn_gas(&mut self, gas_burnt: Gas) -> Result<()> { let new_burnt_gas = - self.fast_counter.burnt_gas.checked_add(value).ok_or(HostError::IntegerOverflow)?; + self.fast_counter.burnt_gas.checked_add(gas_burnt).ok_or(HostError::IntegerOverflow)?; if new_burnt_gas <= self.fast_counter.gas_limit { self.fast_counter.burnt_gas = new_burnt_gas; Ok(()) From 11acc913fddb5dd4ed24a28597a2adefb5a8ed9a Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Thu, 15 Dec 2022 16:59:36 +0100 Subject: [PATCH 105/188] runtime: avoid copying register values unnecessarily (#8224) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move VMLogic’s memory and register to separate structs which makes it possible to access them in a way where Rust can track access to them and gas counter separately. This allows us to keep a shared reference on register while writing data to memory. Thanks to that, values of the register can be borrowed and doesn’t need to be cloned. This is easy to see in read_register method The downside is of course that the call sites are more convoluted since gas counter needs to be passed as a separate argument to memory and register access methods. --- core/primitives-core/src/config.rs | 4 + runtime/near-vm-logic/src/lib.rs | 1 + runtime/near-vm-logic/src/logic.rs | 252 +++++++++++++-------------- runtime/near-vm-logic/src/vmstate.rs | 183 +++++++++++++++++++ 4 files changed, 309 insertions(+), 131 deletions(-) create mode 100644 runtime/near-vm-logic/src/vmstate.rs diff --git a/core/primitives-core/src/config.rs b/core/primitives-core/src/config.rs index c9b939d5c5f..17d794bf380 100644 --- a/core/primitives-core/src/config.rs +++ b/core/primitives-core/src/config.rs @@ -57,6 +57,10 @@ pub struct VMLimitConfig { /// 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. diff --git a/runtime/near-vm-logic/src/lib.rs b/runtime/near-vm-logic/src/lib.rs index e1def03580d..fe7a8166a5f 100644 --- a/runtime/near-vm-logic/src/lib.rs +++ b/runtime/near-vm-logic/src/lib.rs @@ -11,6 +11,7 @@ pub(crate) mod receipt_manager; mod tests; pub mod types; mod utils; +mod vmstate; pub use context::VMContext; pub use dependencies::{External, MemoryLike, StorageGetMode, ValuePtr}; diff --git a/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs index 23bc41d9631..8fee7ee296f 100644 --- a/runtime/near-vm-logic/src/logic.rs +++ b/runtime/near-vm-logic/src/logic.rs @@ -21,7 +21,6 @@ use near_primitives_core::types::{ use near_primitives_core::types::{GasDistribution, GasWeight}; use near_vm_errors::{FunctionCallError, InconsistentStateError}; use near_vm_errors::{HostError, VMLogicError}; -use std::collections::HashMap; use std::mem::size_of; pub type Result = ::std::result::Result; @@ -40,7 +39,7 @@ pub struct VMLogic<'a> { /// results of the methods that made the callback are stored in this collection. promise_results: &'a [PromiseResult], /// Pointer to the guest memory. - memory: &'a mut dyn MemoryLike, + memory: crate::vmstate::Memory<'a>, /// Keeping track of the current account balance, which can decrease when we create promises /// and attach balance to them. @@ -57,7 +56,7 @@ pub struct VMLogic<'a> { logs: Vec, /// Registers can be used by the guest to store blobs of data without moving them across /// host-guest boundary. - registers: HashMap>, + registers: crate::vmstate::Registers, /// The DAG of promises, indexed by promise id. promises: Vec, @@ -83,24 +82,6 @@ enum Promise { NotReceipt(Vec), } -macro_rules! memory_get { - ($_type:ty, $name:ident) => { - fn $name(&mut self, offset: u64) -> Result<$_type> { - let mut array = [0u8; size_of::<$_type>()]; - self.memory_get_into(offset, &mut array)?; - Ok(<$_type>::from_le_bytes(array)) - } - }; -} - -macro_rules! memory_set { - ($_type:ty, $name:ident) => { - fn $name(&mut self, offset: u64, value: $_type) -> Result<()> { - self.memory_set_slice(offset, &value.to_le_bytes()) - } - }; -} - impl<'a> VMLogic<'a> { pub fn new_with_protocol_version( ext: &'a mut dyn External, @@ -133,14 +114,14 @@ impl<'a> VMLogic<'a> { config, fees_config, promise_results, - memory, + memory: crate::vmstate::Memory::new(memory), current_account_balance, current_account_locked_balance, current_storage_usage, gas_counter, return_data: ReturnData::None, logs: vec![], - registers: HashMap::new(), + registers: Default::default(), promises: vec![], total_log_length: 0, current_protocol_version, @@ -177,82 +158,22 @@ impl<'a> VMLogic<'a> { // # Memory helper functions # // ########################### - fn memory_get_into(&mut self, offset: u64, buf: &mut [u8]) -> Result<()> { - self.gas_counter.pay_base(read_memory_base)?; - self.gas_counter.pay_per(read_memory_byte, buf.len() as _)?; - self.memory.read_memory(offset, buf).map_err(|_| HostError::MemoryAccessViolation.into()) - } - - fn memory_get_vec(&mut self, offset: u64, len: u64) -> Result> { - self.gas_counter.pay_base(read_memory_base)?; - self.gas_counter.pay_per(read_memory_byte, len)?; - // This check is redundant in the sense that read_memory will perform it - // as well however it’s here to validate that `len` is a valid value. - // See documentation of MemoryLike::read_memory for more information. - self.memory.fits_memory(offset, len).map_err(|_| HostError::MemoryAccessViolation)?; - let mut buf = vec![0; len as usize]; - self.memory.read_memory(offset, &mut buf).map_err(|_| HostError::MemoryAccessViolation)?; - Ok(buf) - } - - memory_get!(u128, memory_get_u128); - memory_get!(u32, memory_get_u32); - memory_get!(u16, memory_get_u16); - memory_get!(u8, memory_get_u8); - fn get_vec_from_memory_or_register(&mut self, offset: u64, len: u64) -> Result> { if len != u64::MAX { - self.memory_get_vec(offset, len) + self.memory.get_vec(&mut self.gas_counter, offset, len) } else { - self.internal_read_register(offset) + self.registers.get(&mut self.gas_counter, offset).map(Vec::from) } } - fn memory_set_slice(&mut self, offset: u64, buf: &[u8]) -> Result<()> { - self.gas_counter.pay_base(write_memory_base)?; - self.gas_counter.pay_per(write_memory_byte, buf.len() as _)?; - self.memory.write_memory(offset, buf).map_err(|_| HostError::MemoryAccessViolation.into()) - } - - memory_set!(u128, memory_set_u128); - // ################# // # Registers API # // ################# - fn internal_read_register(&mut self, register_id: u64) -> Result> { - if let Some(data) = self.registers.get(®ister_id) { - self.gas_counter.pay_base(read_register_base)?; - self.gas_counter.pay_per(read_register_byte, data.len() as _)?; - Ok(data.clone()) - } else { - Err(HostError::InvalidRegisterId { register_id }.into()) - } - } - - fn internal_write_register(&mut self, register_id: u64, data: Vec) -> Result<()> { - self.gas_counter.pay_base(write_register_base)?; - self.gas_counter.pay_per(write_register_byte, data.len() as u64)?; - if data.len() as u64 > self.config.limit_config.max_register_size - || self.registers.len() as u64 >= self.config.limit_config.max_number_registers - { - return Err(HostError::MemoryAccessViolation.into()); - } - self.registers.insert(register_id, data); - - // Calculate the new memory usage. - let usage: usize = - self.registers.values().map(|v| size_of::() + v.len() * size_of::()).sum(); - if usage as u64 > self.config.limit_config.registers_memory_limit { - Err(HostError::MemoryAccessViolation.into()) - } else { - Ok(()) - } - } - /// Convenience function for testing. + #[cfg(test)] pub fn wrapped_internal_write_register(&mut self, register_id: u64, data: &[u8]) -> Result<()> { - self.internal_write_register(register_id, data.to_vec()) + self.registers.set(&mut self.gas_counter, &self.config.limit_config, register_id, data) } /// Writes the entire content from the register `register_id` into the memory of the guest starting with `ptr`. @@ -277,8 +198,8 @@ impl<'a> VMLogic<'a> { /// `base + read_register_base + read_register_byte * num_bytes + write_memory_base + write_memory_byte * num_bytes` pub fn read_register(&mut self, register_id: u64, ptr: u64) -> Result<()> { self.gas_counter.pay_base(base)?; - let data = self.internal_read_register(register_id)?; - self.memory_set_slice(ptr, &data) + let data = self.registers.get(&mut self.gas_counter, register_id)?; + self.memory.set(&mut self.gas_counter, ptr, data) } /// Returns the size of the blob stored in the given register. @@ -294,7 +215,7 @@ impl<'a> VMLogic<'a> { /// `base` pub fn register_len(&mut self, register_id: u64) -> Result { self.gas_counter.pay_base(base)?; - Ok(self.registers.get(®ister_id).map(|r| r.len() as _).unwrap_or(u64::MAX)) + Ok(self.registers.get_len(register_id).unwrap_or(u64::MAX)) } /// Copies `data` from the guest memory into the register. If register is unused will initialize @@ -312,8 +233,8 @@ impl<'a> VMLogic<'a> { /// `base + read_memory_base + read_memory_bytes * num_bytes + write_register_base + write_register_bytes * num_bytes` pub fn write_register(&mut self, register_id: u64, data_len: u64, data_ptr: u64) -> Result<()> { self.gas_counter.pay_base(base)?; - let data = self.memory_get_vec(data_ptr, data_len)?; - self.internal_write_register(register_id, data) + let data = self.memory.get_vec(&mut self.gas_counter, data_ptr, data_len)?; + self.registers.set(&mut self.gas_counter, &self.config.limit_config, register_id, data) } // ################################### @@ -350,12 +271,12 @@ impl<'a> VMLogic<'a> { } .into()); } - buf = self.memory_get_vec(ptr, len)?; + buf = self.memory.get_vec(&mut self.gas_counter, ptr, len)?; } else { buf = vec![]; for i in 0..=max_len { // self.memory_get_u8 will check for u64 overflow on the first iteration (i == 0) - let el = self.memory_get_u8(ptr + i)?; + let el = self.memory.get_u8(&mut self.gas_counter, ptr + i)?; if el == 0 { break; } @@ -379,9 +300,7 @@ impl<'a> VMLogic<'a> { /// * It's up to the caller to set correct len #[cfg(feature = "sandbox")] fn sandbox_get_utf8_string(&mut self, len: u64, ptr: u64) -> Result { - self.memory.fits_memory(ptr, len).map_err(|_| HostError::MemoryAccessViolation)?; - let mut buf = vec![0; len as usize]; - self.memory.read_memory(ptr, &mut buf).map_err(|_| HostError::MemoryAccessViolation)?; + let buf = self.memory.get_vec_for_free(ptr, len)?; String::from_utf8(buf).map_err(|_| HostError::BadUTF8.into()) } @@ -406,7 +325,7 @@ impl<'a> VMLogic<'a> { let max_len = self.config.limit_config.max_total_log_length.saturating_sub(self.total_log_length); if len != u64::MAX { - let input = self.memory_get_vec(ptr, len)?; + let input = self.memory.get_vec(&mut self.gas_counter, ptr, len)?; if len % 2 != 0 { return Err(HostError::BadUTF16.into()); } @@ -426,7 +345,7 @@ impl<'a> VMLogic<'a> { for i in 0..=limit { // self.memory_get_u16 will check for u64 overflow on the first iteration (i == 0) let start = ptr + i * size_of::() as u64; - let el = self.memory_get_u16(start)?; + let el = self.memory.get_u16(&mut self.gas_counter, start)?; if el == 0 { break; } @@ -510,9 +429,11 @@ impl<'a> VMLogic<'a> { pub fn current_account_id(&mut self, register_id: u64) -> Result<()> { self.gas_counter.pay_base(base)?; - self.internal_write_register( + self.registers.set( + &mut self.gas_counter, + &self.config.limit_config, register_id, - self.context.current_account_id.as_ref().as_bytes().to_vec(), + self.context.current_account_id.as_ref().as_bytes(), ) } @@ -538,9 +459,11 @@ impl<'a> VMLogic<'a> { } .into()); } - self.internal_write_register( + self.registers.set( + &mut self.gas_counter, + &self.config.limit_config, register_id, - self.context.signer_account_id.as_ref().as_bytes().to_vec(), + self.context.signer_account_id.as_ref().as_bytes(), ) } @@ -565,7 +488,12 @@ impl<'a> VMLogic<'a> { } .into()); } - self.internal_write_register(register_id, self.context.signer_account_pk.clone()) + self.registers.set( + &mut self.gas_counter, + &self.config.limit_config, + register_id, + self.context.signer_account_pk.as_slice(), + ) } /// All contract calls are a result of a receipt, this receipt might be created by a transaction @@ -589,9 +517,11 @@ impl<'a> VMLogic<'a> { } .into()); } - self.internal_write_register( + self.registers.set( + &mut self.gas_counter, + &self.config.limit_config, register_id, - self.context.predecessor_account_id.as_ref().as_bytes().to_vec(), + self.context.predecessor_account_id.as_ref().as_bytes(), ) } @@ -605,7 +535,12 @@ impl<'a> VMLogic<'a> { pub fn input(&mut self, register_id: u64) -> Result<()> { self.gas_counter.pay_base(base)?; - self.internal_write_register(register_id, self.context.input.clone()) + self.registers.set( + &mut self.gas_counter, + &self.config.limit_config, + register_id, + self.context.input.as_slice(), + ) } /// Returns the current block height. @@ -657,7 +592,7 @@ impl<'a> VMLogic<'a> { let account_id = self.read_and_parse_account_id(account_id_ptr, account_id_len)?; self.gas_counter.pay_base(validator_stake_base)?; let balance = self.ext.validator_stake(&account_id)?.unwrap_or_default(); - self.memory_set_u128(stake_ptr, balance) + self.memory.set_u128(&mut self.gas_counter, stake_ptr, balance) } /// Get the total validator stake of the current epoch. @@ -671,7 +606,7 @@ impl<'a> VMLogic<'a> { self.gas_counter.pay_base(base)?; self.gas_counter.pay_base(validator_total_stake_base)?; let total_stake = self.ext.validator_total_stake()?; - self.memory_set_u128(stake_ptr, total_stake) + self.memory.set_u128(&mut self.gas_counter, stake_ptr, total_stake) } /// Returns the number of bytes used by the contract if it was saved to the trie as of the @@ -701,7 +636,7 @@ impl<'a> VMLogic<'a> { /// `base + memory_write_base + memory_write_size * 16` pub fn account_balance(&mut self, balance_ptr: u64) -> Result<()> { self.gas_counter.pay_base(base)?; - self.memory_set_u128(balance_ptr, self.current_account_balance) + self.memory.set_u128(&mut self.gas_counter, balance_ptr, self.current_account_balance) } /// The current amount of tokens locked due to staking. @@ -711,7 +646,11 @@ impl<'a> VMLogic<'a> { /// `base + memory_write_base + memory_write_size * 16` pub fn account_locked_balance(&mut self, balance_ptr: u64) -> Result<()> { self.gas_counter.pay_base(base)?; - self.memory_set_u128(balance_ptr, self.current_account_locked_balance) + self.memory.set_u128( + &mut self.gas_counter, + balance_ptr, + self.current_account_locked_balance, + ) } /// The balance that was attached to the call that will be immediately deposited before the @@ -733,7 +672,7 @@ impl<'a> VMLogic<'a> { } .into()); } - self.memory_set_u128(balance_ptr, self.context.attached_deposit) + self.memory.set_u128(&mut self.gas_counter, balance_ptr, self.context.attached_deposit) } /// The amount of gas attached to the call that can be used to pay for the gas fees. @@ -817,7 +756,7 @@ impl<'a> VMLogic<'a> { let res = crate::alt_bn128::g1_multiexp(elements)?; - self.internal_write_register(register_id, res.into()) + self.registers.set(&mut self.gas_counter, &self.config.limit_config, register_id, res) } /// Computes sum for signed g1 group elements on alt_bn128 curve \sum_i @@ -860,7 +799,7 @@ impl<'a> VMLogic<'a> { let res = crate::alt_bn128::g1_sum(elements)?; - self.internal_write_register(register_id, res.into()) + self.registers.set(&mut self.gas_counter, &self.config.limit_config, register_id, res) } /// Computes pairing check on alt_bn128 curve. @@ -913,7 +852,12 @@ impl<'a> VMLogic<'a> { /// `base + write_register_base + write_register_byte * num_bytes`. pub fn random_seed(&mut self, register_id: u64) -> Result<()> { self.gas_counter.pay_base(base)?; - self.internal_write_register(register_id, self.context.random_seed.clone()) + self.registers.set( + &mut self.gas_counter, + &self.config.limit_config, + register_id, + self.context.random_seed.as_slice(), + ) } /// Hashes the given value using sha256 and returns it into `register_id`. @@ -934,7 +878,12 @@ impl<'a> VMLogic<'a> { use sha2::Digest; let value_hash = sha2::Sha256::digest(&value); - self.internal_write_register(register_id, value_hash.as_slice().to_vec()) + self.registers.set( + &mut self.gas_counter, + &self.config.limit_config, + register_id, + value_hash.as_slice(), + ) } /// Hashes the given value using keccak256 and returns it into `register_id`. @@ -955,7 +904,12 @@ impl<'a> VMLogic<'a> { use sha3::Digest; let value_hash = sha3::Keccak256::digest(&value); - self.internal_write_register(register_id, value_hash.as_slice().to_vec()) + self.registers.set( + &mut self.gas_counter, + &self.config.limit_config, + register_id, + value_hash.as_slice(), + ) } /// Hashes the given value using keccak512 and returns it into `register_id`. @@ -976,7 +930,12 @@ impl<'a> VMLogic<'a> { use sha3::Digest; let value_hash = sha3::Keccak512::digest(&value); - self.internal_write_register(register_id, value_hash.as_slice().to_vec()) + self.registers.set( + &mut self.gas_counter, + &self.config.limit_config, + register_id, + value_hash.as_slice(), + ) } /// Hashes the given value using RIPEMD-160 and returns it into `register_id`. @@ -1007,7 +966,12 @@ impl<'a> VMLogic<'a> { use ripemd::Digest; let value_hash = ripemd::Ripemd160::digest(&value); - self.internal_write_register(register_id, value_hash.as_slice().to_vec()) + self.registers.set( + &mut self.gas_counter, + &self.config.limit_config, + register_id, + value_hash.as_slice(), + ) } /// Recovers an ECDSA signer address and returns it into `register_id`. @@ -1096,7 +1060,12 @@ impl<'a> VMLogic<'a> { } if let Ok(pk) = signature.recover(hash) { - self.internal_write_register(register_id, pk.as_ref().to_vec())?; + self.registers.set( + &mut self.gas_counter, + &self.config.limit_config, + register_id, + pk.as_ref(), + )?; return Ok(true as u64); }; @@ -1363,7 +1332,8 @@ impl<'a> VMLogic<'a> { self.gas_counter.pay_per(promise_and_per_promise, memory_len)?; // Read indices as little endian u64. - let promise_indices = self.memory_get_vec(promise_idx_ptr, memory_len)?; + let promise_indices = + self.memory.get_vec(&mut self.gas_counter, promise_idx_ptr, memory_len)?; let promise_indices = stdx::as_chunks_exact::<{ size_of::() }, u8>(&promise_indices) .unwrap() .into_iter() @@ -1693,7 +1663,7 @@ impl<'a> VMLogic<'a> { } .into()); } - let amount = self.memory_get_u128(amount_ptr)?; + let amount = self.memory.get_u128(&mut self.gas_counter, amount_ptr)?; let method_name = self.get_vec_from_memory_or_register(method_name_ptr, method_name_len)?; if method_name.is_empty() { return Err(HostError::EmptyMethodName.into()); @@ -1749,7 +1719,7 @@ impl<'a> VMLogic<'a> { } .into()); } - let amount = self.memory_get_u128(amount_ptr)?; + 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); @@ -1800,7 +1770,7 @@ impl<'a> VMLogic<'a> { } .into()); } - let amount = self.memory_get_u128(amount_ptr)?; + let amount = self.memory.get_u128(&mut self.gas_counter, amount_ptr)?; let public_key = self.get_vec_from_memory_or_register(public_key_ptr, public_key_len)?; let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; @@ -1891,7 +1861,7 @@ impl<'a> VMLogic<'a> { .into()); } let public_key = self.get_vec_from_memory_or_register(public_key_ptr, public_key_len)?; - let allowance = self.memory_get_u128(allowance_ptr)?; + let allowance = self.memory.get_u128(&mut self.gas_counter, allowance_ptr)?; let allowance = if allowance > 0 { Some(allowance) } else { None }; let receiver_id = self.read_and_parse_account_id(receiver_id_ptr, receiver_id_len)?; let raw_method_names = @@ -2054,7 +2024,12 @@ impl<'a> VMLogic<'a> { { PromiseResult::NotReady => Ok(0), PromiseResult::Successful(data) => { - self.internal_write_register(register_id, data.clone())?; + self.registers.set( + &mut self.gas_counter, + &self.config.limit_config, + register_id, + data.as_slice(), + )?; Ok(1) } PromiseResult::Failed => Ok(2), @@ -2250,8 +2225,8 @@ impl<'a> VMLogic<'a> { self.check_can_add_a_log_message()?; // Underflow checked above. - let msg_len = self.memory_get_u32((msg_ptr - 4) as u64)?; - let filename_len = self.memory_get_u32((filename_ptr - 4) as u64)?; + let msg_len = self.memory.get_u32(&mut self.gas_counter, (msg_ptr - 4) as u64)?; + let filename_len = self.memory.get_u32(&mut self.gas_counter, (filename_ptr - 4) as u64)?; let msg = self.get_utf16_string(msg_len as u64, msg_ptr as u64)?; let filename = self.get_utf16_string(filename_len as u64, filename_ptr as u64)?; @@ -2384,7 +2359,12 @@ impl<'a> VMLogic<'a> { .current_storage_usage .checked_add(value.len() as u64) .ok_or(InconsistentStateError::IntegerOverflow)?; - self.internal_write_register(register_id, old_value)?; + self.registers.set( + &mut self.gas_counter, + &self.config.limit_config, + register_id, + old_value, + )?; Ok(1) } None => { @@ -2463,7 +2443,12 @@ impl<'a> VMLogic<'a> { ); match read { Some(value) => { - self.internal_write_register(register_id, value)?; + self.registers.set( + &mut self.gas_counter, + &self.config.limit_config, + register_id, + value, + )?; Ok(1) } None => Ok(0), @@ -2537,7 +2522,12 @@ impl<'a> VMLogic<'a> { + storage_config.num_extra_bytes_record, ) .ok_or(InconsistentStateError::IntegerOverflow)?; - self.internal_write_register(register_id, value)?; + self.registers.set( + &mut self.gas_counter, + &self.config.limit_config, + register_id, + value, + )?; Ok(1) } None => Ok(0), diff --git a/runtime/near-vm-logic/src/vmstate.rs b/runtime/near-vm-logic/src/vmstate.rs new file mode 100644 index 00000000000..5017eca8c72 --- /dev/null +++ b/runtime/near-vm-logic/src/vmstate.rs @@ -0,0 +1,183 @@ +use crate::dependencies::MemoryLike; +use crate::gas_counter::GasCounter; + +use near_primitives_core::config::ExtCosts::*; +use near_primitives_core::config::VMLimitConfig; +use near_vm_errors::{HostError, VMLogicError}; + +use core::mem::size_of; + +type Result = ::std::result::Result; + +/// Guest memory. +/// +/// Provides interface to access the guest memory while correctly accounting for +/// gas usage. +/// +/// Really the main point of this struct is that it is a separate object so when +/// its methods are called, such as `memory.get_into(&mut gas_counter, ...)`, +/// the compiler can deconstruct the access to each field of [`VMLogic`] and do +/// more granular lifetime analysis. In particular, this design is what allows +/// us to forgo copying register value in [`VMLogic::read_register`]. +pub(super) struct Memory<'a>(&'a mut dyn MemoryLike); + +macro_rules! memory_get { + ($_type:ty, $name:ident) => { + pub(super) fn $name( + &mut self, + gas_counter: &mut GasCounter, + offset: u64, + ) -> Result<$_type> { + let mut array = [0u8; size_of::<$_type>()]; + self.get_into(gas_counter, offset, &mut array)?; + Ok(<$_type>::from_le_bytes(array)) + } + }; +} + +macro_rules! memory_set { + ($_type:ty, $name:ident) => { + pub(super) fn $name( + &mut self, + gas_counter: &mut GasCounter, + offset: u64, + value: $_type, + ) -> Result<()> { + self.set(gas_counter, offset, &value.to_le_bytes()) + } + }; +} + +impl<'a> Memory<'a> { + pub(super) fn new(mem: &'a mut dyn MemoryLike) -> Self { + Self(mem) + } + + /// Copies data from guest memory into provided buffer accounting for gas. + fn get_into(&self, gas_counter: &mut GasCounter, offset: u64, buf: &mut [u8]) -> Result<()> { + gas_counter.pay_base(read_memory_base)?; + let len = u64::try_from(buf.len()).map_err(|_| HostError::MemoryAccessViolation)?; + gas_counter.pay_per(read_memory_byte, len)?; + self.0.read_memory(offset, buf).map_err(|_| HostError::MemoryAccessViolation.into()) + } + + /// Copies data from guest memory into a newly allocated vector accounting for gas. + pub(super) fn get_vec( + &self, + gas_counter: &mut GasCounter, + offset: u64, + len: u64, + ) -> Result> { + gas_counter.pay_base(read_memory_base)?; + gas_counter.pay_per(read_memory_byte, len)?; + self.get_vec_for_free(offset, len) + } + + /// Like [`Self::get_vec`] but does not pay gas fees. + pub(super) fn get_vec_for_free(&self, offset: u64, len: u64) -> Result> { + // This check is redundant in the sense that read_memory will perform it + // as well however it’s here to validate that `len` is a valid value. + // See documentation of MemoryLike::read_memory for more information. + self.0.fits_memory(offset, len).map_err(|_| HostError::MemoryAccessViolation)?; + let mut buf = vec![0; len as usize]; + self.0.read_memory(offset, &mut buf).map_err(|_| HostError::MemoryAccessViolation)?; + Ok(buf) + } + + /// Copies data from provided buffer into guest memory accounting for gas. + pub(super) fn set( + &mut self, + gas_counter: &mut GasCounter, + offset: u64, + buf: &[u8], + ) -> Result<()> { + gas_counter.pay_base(write_memory_base)?; + gas_counter.pay_per(write_memory_byte, buf.len() as _)?; + self.0.write_memory(offset, buf).map_err(|_| HostError::MemoryAccessViolation.into()) + } + + memory_get!(u128, get_u128); + memory_get!(u32, get_u32); + memory_get!(u16, get_u16); + memory_get!(u8, get_u8); + memory_set!(u128, set_u128); +} + +/// Registers to use by the guest. +/// +/// Provides interface to access registers while correctly accounting for gas +/// usage. +/// +/// See documentation of [`Memory`] for more motivation for this struct. +#[derive(Default, Clone)] +pub(super) struct Registers(std::collections::HashMap>); + +impl Registers { + /// Returns register with given index. + /// + /// Returns an error if (i) there’s not enough gas to perform the register + /// read or (ii) register with given index doesn’t exist. + pub(super) fn get<'s>( + &'s self, + gas_counter: &mut GasCounter, + register_id: u64, + ) -> Result<&'s [u8]> { + if let Some(data) = self.0.get(®ister_id) { + gas_counter.pay_base(read_register_base)?; + let len = u64::try_from(data.len()).map_err(|_| HostError::MemoryAccessViolation)?; + gas_counter.pay_per(read_register_byte, len)?; + Ok(&data[..]) + } else { + Err(HostError::InvalidRegisterId { register_id }.into()) + } + } + + /// Returns length of register with given index or None if no such register. + pub(super) fn get_len(&self, register_id: u64) -> Option { + self.0.get(®ister_id).map(|data| data.len() as u64) + } + + /// Sets register with given index. + /// + /// Returns an error if (i) there’s not enough gas to perform the register + /// write or (ii) if setting the register would violate configured limits. + pub(super) fn set( + &mut self, + gas_counter: &mut GasCounter, + config: &VMLimitConfig, + register_id: u64, + data: T, + ) -> Result<()> + where + T: Into> + AsRef<[u8]>, + { + let data_len = + u64::try_from(data.as_ref().len()).map_err(|_| HostError::MemoryAccessViolation)?; + gas_counter.pay_base(write_register_base)?; + gas_counter.pay_per(write_register_byte, data_len)?; + // Fun fact: if we are at the limit and we replace a register, we’ll + // fail even though we should be succeeding. This bug is now part of + // the protocol so we can’t change it. + if data_len > config.max_register_size || self.0.len() as u64 >= config.max_number_registers + { + return Err(HostError::MemoryAccessViolation.into()); + } + match self.0.insert(register_id, data.into()) { + Some(old_value) if old_value.len() as u64 >= data_len => { + // If there was old value and it was no shorter than the new + // one, there’s no need to check new memory usage since it + // didn’t increase. + } + _ => { + // Calculate and check the new memory usage. + // TODO(mina86): Memorise usage in a field so we don’t have to + // go through all registers each time. + let usage: usize = self.0.values().map(|v| size_of::() + v.len()).sum(); + if usage as u64 > config.registers_memory_limit { + return Err(HostError::MemoryAccessViolation.into()); + } + } + } + Ok(()) + } +} From 291f1fc190bf01bb5eb9efe28e9ee4b04cce34a3 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Fri, 16 Dec 2022 13:09:11 +0100 Subject: [PATCH 106/188] use nextest for better test isolation (#8207) it will allow us to set panic=abort per test (because each test is executed in a separate subprocess) and set test timeouts without developing a custom test harness. Additionally I've set the default test timeout to 2min (period = 1min, killing test after 2 periods), which seems reasonable, given that all presubmit tests execute <1min as of today. --- .buildkite/pipeline.yml | 13 +++++++++---- .config/nextest.toml | 2 ++ docs/practices/testing/README.md | 15 +++++++++------ 3 files changed, 20 insertions(+), 10 deletions(-) create mode 100644 .config/nextest.toml diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 4319bbbf32b..68f0f3a60bc 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -18,7 +18,8 @@ steps: - label: "cargo test integration-tests*" command: | source ~/.cargo/env && set -eux - cargo test --locked -p 'integration-tests' + cargo install cargo-nextest + cargo nextest run --locked -p 'integration-tests' timeout: 60 agents: @@ -28,7 +29,8 @@ steps: - label: "cargo test not integration-tests*" command: | source ~/.cargo/env && set -eux - cargo test --locked --workspace -p '*' --exclude 'integration-tests*' + cargo install cargo-nextest + cargo nextest run --locked --workspace -p '*' --exclude 'integration-tests*' timeout: 60 agents: @@ -38,7 +40,8 @@ steps: - label: "cargo test nightly integration-tests*" command: | source ~/.cargo/env && set -eux - cargo test --features nightly,test_features -p 'integration-tests' + cargo install cargo-nextest + cargo nextest run --features nightly,test_features -p 'integration-tests' timeout: 60 agents: @@ -48,7 +51,9 @@ steps: - label: "cargo test nightly not integration-tests*" command: | source ~/.cargo/env && set -eux - cargo test --workspace --features nightly,test_features,mock_node -p '*' --exclude 'integration-tests*' + cargo install cargo-nextest + cargo nextest run --workspace --features nightly,test_features,mock_node -p '*' --exclude 'integration-tests*' + cargo test --doc timeout: 60 agents: diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 00000000000..6a350df62e4 --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,2 @@ +[profile.default] +slow-timeout = { period = "60s", terminate-after = 2, grace-period = "0s" } diff --git a/docs/practices/testing/README.md b/docs/practices/testing/README.md index 285bccb43a5..532b0309379 100644 --- a/docs/practices/testing/README.md +++ b/docs/practices/testing/README.md @@ -11,8 +11,10 @@ 4. The general rule of thumb for a reviewer is to first review the tests, and ensure that they can convince themselves that the code change that passes the tests must be correct. Only then the code should be reviewed. -5. Have the assertions in the tests as specific as possible. For example, do not - do `assert!(result.is_err())`, expect the specific error instead. +5. Have the assertions in the tests as specific as possible, + however do not make the tests change-detectors of the concrete implementation. + (assert only properties which are required for correctness). + For example, do not do `assert!(result.is_err())`, expect the specific error instead. # Tests hierarchy @@ -21,9 +23,10 @@ In NEAR Reference Client we largely split tests into three categories: 1. Relatively cheap sanity or fast fuzz tests. It includes all the `#[test]` Rust tests not decorated by features. Our repo is configured in such a way that all such tests are ran on every PR, and failing at least one of them is - blocking the PR from being pushed. + blocking the PR from being merged. -To run such tests locally run `cargo test --all` +To run such tests locally run `cargo nextest run --all`. +It requires nextest harness which can be installed by running `cargo install cargo-nextest` first. 2. Expensive tests. This includes all the fuzzy tests that run many iterations, as well as tests that spin up multiple nodes and run them until they reach a @@ -31,9 +34,9 @@ To run such tests locally run `cargo test --all` `#[cfg(feature="expensive-tests")]`. It is not trivial to enable features that are not declared in the top level crate, and thus the easiest way to run such tests is to enable all the features by passing `--all-features` to - `cargo test`, e.g: + `cargo nextest run`, e.g: -`cargo test --package near-client --test cross_shard_tx +`cargo nextest run --package near-client --test cross_shard_tx tests::test_cross_shard_tx --all-features` 3. Python tests. We have an infrastructure to spin up nodes, both locally and From e489431f1b2887d266550c54960697d6228d1a8c Mon Sep 17 00:00:00 2001 From: Andrei Kashin Date: Fri, 16 Dec 2022 13:07:15 +0000 Subject: [PATCH 107/188] doc: Fix typo in parameter_definition.md (#8233) Replace "form" with "from" --- docs/architecture/gas/parameter_definition.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/gas/parameter_definition.md b/docs/architecture/gas/parameter_definition.md index 4f2d4b01a11..ee76ec8ff6b 100644 --- a/docs/architecture/gas/parameter_definition.md +++ b/docs/architecture/gas/parameter_definition.md @@ -46,7 +46,7 @@ Before adding anything, please review the basic principles for gas parameters. multiplied by `N`. (Example: `N` = number of bytes when reading a value from storage.) - Charge gas before executing the workload. -- Parameters should be independent form specific implementation choices in +- Parameters should be independent from specific implementation choices in nearcore. - Ideally, contract developers can easily understand what the cost is simply by reading the name in a gas profile. From 9d10acfd25a12f30a67ff690909588e25524b7d5 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Fri, 16 Dec 2022 14:40:03 +0100 Subject: [PATCH 108/188] runtime: add vmstate::Registers unit tests (#8231) * runtime: add vmstate::Registers unit tests * review --- runtime/near-vm-logic/src/vmstate.rs | 113 +++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/runtime/near-vm-logic/src/vmstate.rs b/runtime/near-vm-logic/src/vmstate.rs index 5017eca8c72..43a0317881c 100644 --- a/runtime/near-vm-logic/src/vmstate.rs +++ b/runtime/near-vm-logic/src/vmstate.rs @@ -181,3 +181,116 @@ impl Registers { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::Registers; + + use crate::gas_counter::GasCounter; + + use near_primitives_core::config::{ExtCostsConfig, VMLimitConfig}; + use near_vm_errors::HostError; + + struct RegistersTestContext { + gas: GasCounter, + cfg: VMLimitConfig, + regs: Registers, + } + + impl RegistersTestContext { + fn new() -> Self { + let costs = ExtCostsConfig::test(); + Self { + gas: GasCounter::new(costs, u64::MAX, 0, u64::MAX, false), + cfg: VMLimitConfig::test(), + regs: Default::default(), + } + } + + #[track_caller] + fn assert_set_success(&mut self, register_id: u64, value: &str) { + self.regs.set(&mut self.gas, &self.cfg, register_id, value.as_bytes()).unwrap(); + self.assert_read(register_id, Some(value)); + } + + #[track_caller] + fn assert_set_failure(&mut self, register_id: u64, value: &str) { + let want = Err(HostError::MemoryAccessViolation.into()); + let got = self.regs.set(&mut self.gas, &self.cfg, register_id, value.as_bytes()); + assert_eq!(want, got); + } + + #[track_caller] + fn assert_read(&mut self, register_id: u64, value: Option<&str>) { + if let Some(value) = value { + assert_eq!(Ok(value.as_bytes()), self.regs.get(&mut self.gas, register_id)); + assert_eq!(Some(value.len() as u64), self.regs.get_len(register_id)); + } else { + let err = HostError::InvalidRegisterId { register_id }.into(); + assert_eq!(Err(err), self.regs.get(&mut self.gas, register_id)); + assert_eq!(None, self.regs.get_len(register_id)); + } + } + + #[track_caller] + fn assert_used_gas(&self, gas: u64) { + assert_eq!((gas, gas), (self.gas.burnt_gas(), self.gas.used_gas())); + } + } + + /// Tests basic setting and reading of registers. + #[test] + fn registers_set() { + let mut ctx = RegistersTestContext::new(); + ctx.assert_read(42, None); + ctx.assert_read(24, None); + ctx.assert_set_success(42, "foo"); + ctx.assert_read(24, None); + ctx.assert_used_gas(5394388050); + } + + /// Tests limit on number of registers. + #[test] + fn registers_max_number_limit() { + let mut ctx = RegistersTestContext::new(); + ctx.cfg.max_number_registers = 2; + + ctx.assert_set_success(42, "foo"); + ctx.assert_set_success(24, "bar"); + + // max_number_registers is 2 so cannot set third register + ctx.assert_set_failure(12, "baz"); + + // Due to historical bug, changing a register is not possible either + // once limit is reached: + ctx.assert_set_failure(42, "O_o"); + ctx.assert_set_failure(24, "O_o"); + + ctx.assert_used_gas(19419557634); + } + + /// Tests limit on a size of a single register. + #[test] + fn registers_register_size_limit() { + let mut ctx = RegistersTestContext::new(); + ctx.cfg.max_register_size = 3; + ctx.assert_set_success(42, "foo"); + ctx.assert_set_failure(24, "quux"); + ctx.assert_used_gas(8275116792); + } + + /// Tests limit on total memory usage. + #[test] + fn registers_usage_limit() { + let mut ctx = RegistersTestContext::new(); + ctx.cfg.registers_memory_limit = 11; + ctx.assert_set_success(42, "foo"); + // Replacing value is fine. + ctx.assert_set_success(42, "bar"); + ctx.assert_set_success(42, ""); + ctx.assert_set_success(42, "baz"); + // But three bytes is a limit (usage is sizeof(u64) + data.len()). + ctx.assert_set_failure(42, "quux"); + ctx.assert_used_gas(24446580564); + } +} From 511d91b6ab296b51af12183e5252e4412a4ced94 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Fri, 16 Dec 2022 16:00:00 +0100 Subject: [PATCH 109/188] removed redundant test and unused code (#8229) test was flaky and redundant wrt more stable test: https://cs.github.com/near/nearcore/blob/f35d1c67a11ad9daaa83179afb4c6f3eb75fdc40/chain/network/src/peer_manager/tests/routing.rs#L1339 --- chain/network/src/test_utils.rs | 30 ------- .../src/tests/network/ban_peers.rs | 24 ------ .../src/tests/network/churn_attack.rs | 2 +- integration-tests/src/tests/network/mod.rs | 1 - integration-tests/src/tests/network/runner.rs | 80 +++---------------- 5 files changed, 13 insertions(+), 124 deletions(-) delete mode 100644 integration-tests/src/tests/network/ban_peers.rs diff --git a/chain/network/src/test_utils.rs b/chain/network/src/test_utils.rs index 64f5d00943d..9b9a56de306 100644 --- a/chain/network/src/test_utils.rs +++ b/chain/network/src/test_utils.rs @@ -1,5 +1,4 @@ use crate::network_protocol::PeerInfo; -use crate::types::ReasonForBan; use crate::types::{ MsgRecipient, NetworkInfo, NetworkResponses, PeerManagerMessageRequest, PeerManagerMessageResponse, SetChainInfo, @@ -254,35 +253,6 @@ impl Handler> for PeerManagerActor { } } -/// Ban peer for unit tests. -/// Calls `try_ban_peer` in `PeerManagerActor`. -#[derive(Message)] -#[rtype(result = "()")] -pub struct BanPeerSignal { - pub peer_id: PeerId, - pub ban_reason: ReasonForBan, -} - -impl BanPeerSignal { - pub fn new(peer_id: PeerId) -> Self { - Self { peer_id, ban_reason: ReasonForBan::None } - } -} - -impl Handler> for PeerManagerActor { - type Result = (); - - fn handle( - &mut self, - msg: WithSpanContext, - _ctx: &mut Self::Context, - ) -> Self::Result { - let (_span, msg) = handler_debug_span!(target: "network", msg); - debug!(target: "network", "Ban peer: {:?}", msg.peer_id); - self.state.disconnect_and_ban(&self.clock, &msg.peer_id, msg.ban_reason); - } -} - // Mocked `PeerManager` adapter, has a queue of `PeerManagerMessageRequest` messages. #[derive(Default)] pub struct MockPeerManagerAdapter { diff --git a/integration-tests/src/tests/network/ban_peers.rs b/integration-tests/src/tests/network/ban_peers.rs deleted file mode 100644 index ebe371bb0c2..00000000000 --- a/integration-tests/src/tests/network/ban_peers.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::tests::network::runner::*; -use near_network::time; - -/// Check we don't try to connect to a banned peer and we don't accept -/// incoming connection from it. -#[test] -fn dont_connect_to_banned_peer() -> anyhow::Result<()> { - let mut runner = Runner::new(2, 2) - .enable_outbound() - .use_boot_nodes(vec![0, 1]) - .ban_window(time::Duration::seconds(60)); - - runner.push(Action::CheckRoutingTable(0, vec![(1, vec![1])])); - runner.push(Action::CheckRoutingTable(1, vec![(0, vec![0])])); - - runner.push_action(ban_peer(0, 1)); - // It needs to wait a large timeout so we are sure both peer don't establish a connection. - runner.push(Action::Wait(time::Duration::milliseconds(1000))); - - runner.push(Action::CheckRoutingTable(0, vec![])); - runner.push(Action::CheckRoutingTable(1, vec![])); - - start_test(runner) -} diff --git a/integration-tests/src/tests/network/churn_attack.rs b/integration-tests/src/tests/network/churn_attack.rs index fc5cda1be6e..7e85de75aae 100644 --- a/integration-tests/src/tests/network/churn_attack.rs +++ b/integration-tests/src/tests/network/churn_attack.rs @@ -1,4 +1,4 @@ -pub use crate::tests::network::runner::*; +use crate::tests::network::runner::*; /// Spin up four nodes and connect them in a square. /// Each node will have at most two connections. diff --git a/integration-tests/src/tests/network/mod.rs b/integration-tests/src/tests/network/mod.rs index 714bb3f5fe3..af8102428ce 100644 --- a/integration-tests/src/tests/network/mod.rs +++ b/integration-tests/src/tests/network/mod.rs @@ -1,4 +1,3 @@ -mod ban_peers; mod churn_attack; mod full_network; mod peer_handshake; diff --git a/integration-tests/src/tests/network/runner.rs b/integration-tests/src/tests/network/runner.rs index 64e4b6b39b6..e759a2a9633 100644 --- a/integration-tests/src/tests/network/runner.rs +++ b/integration-tests/src/tests/network/runner.rs @@ -8,9 +8,7 @@ use near_network::actix::ActixSystem; use near_network::blacklist; use near_network::config; use near_network::tcp; -use near_network::test_utils::{ - expected_routing_tables, open_port, peer_id_from_seed, BanPeerSignal, GetInfo, -}; +use near_network::test_utils::{expected_routing_tables, open_port, peer_id_from_seed, GetInfo}; use near_network::time; use near_network::types::NetworkRecipient; use near_network::types::{ @@ -32,9 +30,9 @@ use std::pin::Pin; use std::sync::Arc; use tracing::debug; -pub type ControlFlow = std::ops::ControlFlow<()>; -pub type BoxFuture<'a, T> = Pin + 'a>>; -pub type ActionFn = +pub(crate) type ControlFlow = std::ops::ControlFlow<()>; +pub(crate) type BoxFuture<'a, T> = Pin + 'a>>; +pub(crate) type ActionFn = Box Fn(&'a mut RunningInfo) -> BoxFuture<'a, anyhow::Result>>; /// Sets up a node with a valid Client, Peer @@ -100,21 +98,9 @@ fn setup_network_node( peer_manager } -#[derive(Debug, Clone)] -pub struct Ping { - pub source: usize, - pub nonce: u64, -} - -#[derive(Debug, Clone)] -pub struct Pong { - pub source: usize, - pub nonce: u64, -} - // TODO: Deprecate this in favor of separate functions. #[derive(Debug, Clone)] -pub enum Action { +pub(crate) enum Action { AddEdge { from: usize, to: usize, @@ -132,7 +118,7 @@ pub enum Action { }, } -pub struct RunningInfo { +pub(crate) struct RunningInfo { runner: Runner, nodes: Vec>, } @@ -285,7 +271,7 @@ impl TestConfig { } } -pub struct Runner { +pub(crate) struct Runner { test_config: Vec, state_machine: StateMachine, validators: Vec, @@ -357,12 +343,6 @@ impl Runner { self } - /// Set ban window range. - pub fn ban_window(mut self, ban_window: time::Duration) -> Self { - self.apply_all(move |test_config| test_config.ban_window = ban_window); - self - } - /// Allow message to connect among themselves without triggering new connections. pub fn enable_outbound(mut self) -> Self { self.apply_all(|test_config| { @@ -455,7 +435,7 @@ impl Runner { /// Executes the test. /// It will fail if it doesn't solve all actions. /// start_test will block until test is complete. -pub fn start_test(runner: Runner) -> anyhow::Result<()> { +pub(crate) fn start_test(runner: Runner) -> anyhow::Result<()> { init_test_logger(); let r = tokio::runtime::Builder::new_current_thread().enable_all().build()?; r.block_on(async { @@ -507,7 +487,7 @@ impl RunningInfo { } } -pub fn assert_expected_peers(node_id: usize, peers: Vec) -> ActionFn { +pub(crate) fn assert_expected_peers(node_id: usize, peers: Vec) -> ActionFn { Box::new(move |info: &mut RunningInfo| { let peers = peers.clone(); Box::pin(async move { @@ -531,7 +511,7 @@ pub fn assert_expected_peers(node_id: usize, peers: Vec) -> ActionFn { /// Check that the number of connections of `node_id` is in the range: /// [expected_connections_lo, expected_connections_hi] /// Use None to denote semi-open interval -pub fn check_expected_connections( +pub(crate) fn check_expected_connections( node_id: usize, expected_connections_lo: Option, expected_connections_hi: Option, @@ -553,7 +533,7 @@ pub fn check_expected_connections( } /// Restart a node that was already stopped. -pub fn restart(node_id: usize) -> ActionFn { +pub(crate) fn restart(node_id: usize) -> ActionFn { Box::new(move |info: &mut RunningInfo| { Box::pin(async move { debug!(target: "test", ?node_id, "runner.rs: restart"); @@ -563,26 +543,9 @@ pub fn restart(node_id: usize) -> ActionFn { }) } -async fn ban_peer_inner( - info: &mut RunningInfo, - target_peer: usize, - banned_peer: usize, -) -> anyhow::Result { - debug!(target: "test", target_peer, banned_peer, "runner.rs: ban_peer"); - let banned_peer_id = info.runner.test_config[banned_peer].peer_id(); - let pm = &info.get_node(target_peer)?.actix.addr; - pm.send(BanPeerSignal::new(banned_peer_id).with_span_context()).await?; - Ok(ControlFlow::Break(())) -} - -/// Ban peer `banned_peer` from perspective of `target_peer`. -pub fn ban_peer(target_peer: usize, banned_peer: usize) -> ActionFn { - Box::new(move |info| Box::pin(ban_peer_inner(info, target_peer, banned_peer))) -} - /// Change account id from a stopped peer. Notice this will also change its peer id, since /// peer_id is derived from account id with NetworkConfig::from_seed -pub fn change_account_id(node_id: usize, account_id: AccountId) -> ActionFn { +pub(crate) fn change_account_id(node_id: usize, account_id: AccountId) -> ActionFn { Box::new(move |info: &mut RunningInfo| { let account_id = account_id.clone(); Box::pin(async move { @@ -591,22 +554,3 @@ pub fn change_account_id(node_id: usize, account_id: AccountId) -> ActionFn { }) }) } - -/// Wait for predicate to return True. -#[allow(dead_code)] -pub fn wait_for(predicate: T) -> ActionFn -where - T: 'static + Fn() -> bool, -{ - let predicate = Arc::new(predicate); - Box::new(move |_info: &mut RunningInfo| { - let predicate = predicate.clone(); - Box::pin(async move { - debug!(target: "test", "runner.rs: wait_for predicate"); - if predicate() { - return Ok(ControlFlow::Break(())); - } - Ok(ControlFlow::Continue(())) - }) - }) -} From 214c322ac80504bb85f91d5511143cfed9ba8060 Mon Sep 17 00:00:00 2001 From: robin-near <111538878+robin-near@users.noreply.github.com> Date: Fri, 16 Dec 2022 10:51:16 -0800 Subject: [PATCH 110/188] [Debug UI] New debug UI with last blocks view migrated and new cluster view (#8174) * [Debug UI] New debug UI with last blocks view migrated and new cluster view. * Correct README. --- tools/debug-ui/.gitignore | 23 + tools/debug-ui/README.md | 41 + tools/debug-ui/package-lock.json | 30363 +++++++++++++++++++++ tools/debug-ui/package.json | 43 + tools/debug-ui/public/index.html | 26 + tools/debug-ui/src/App.scss | 26 + tools/debug-ui/src/App.tsx | 48 + tools/debug-ui/src/ClusterNodeView.scss | 49 + tools/debug-ui/src/ClusterNodeView.tsx | 114 + tools/debug-ui/src/ClusterView.scss | 8 + tools/debug-ui/src/ClusterView.tsx | 74 + tools/debug-ui/src/HeaderBar.scss | 37 + tools/debug-ui/src/HeaderBar.tsx | 69 + tools/debug-ui/src/LatestBlocksView.scss | 126 + tools/debug-ui/src/LatestBlocksView.tsx | 376 + tools/debug-ui/src/NetworkInfoView.scss | 7 + tools/debug-ui/src/NetworkInfoView.tsx | 155 + tools/debug-ui/src/api.tsx | 289 + tools/debug-ui/src/index.css | 13 + tools/debug-ui/src/index.tsx | 30 + tools/debug-ui/src/react-app-env.d.ts | 1 + tools/debug-ui/tsconfig.json | 26 + 22 files changed, 31944 insertions(+) create mode 100644 tools/debug-ui/.gitignore create mode 100644 tools/debug-ui/README.md create mode 100644 tools/debug-ui/package-lock.json create mode 100644 tools/debug-ui/package.json create mode 100644 tools/debug-ui/public/index.html create mode 100644 tools/debug-ui/src/App.scss create mode 100644 tools/debug-ui/src/App.tsx create mode 100644 tools/debug-ui/src/ClusterNodeView.scss create mode 100644 tools/debug-ui/src/ClusterNodeView.tsx create mode 100644 tools/debug-ui/src/ClusterView.scss create mode 100644 tools/debug-ui/src/ClusterView.tsx create mode 100644 tools/debug-ui/src/HeaderBar.scss create mode 100644 tools/debug-ui/src/HeaderBar.tsx create mode 100644 tools/debug-ui/src/LatestBlocksView.scss create mode 100644 tools/debug-ui/src/LatestBlocksView.tsx create mode 100644 tools/debug-ui/src/NetworkInfoView.scss create mode 100644 tools/debug-ui/src/NetworkInfoView.tsx create mode 100644 tools/debug-ui/src/api.tsx create mode 100644 tools/debug-ui/src/index.css create mode 100644 tools/debug-ui/src/index.tsx create mode 100644 tools/debug-ui/src/react-app-env.d.ts create mode 100644 tools/debug-ui/tsconfig.json diff --git a/tools/debug-ui/.gitignore b/tools/debug-ui/.gitignore new file mode 100644 index 00000000000..4d29575de80 --- /dev/null +++ b/tools/debug-ui/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/tools/debug-ui/README.md b/tools/debug-ui/README.md new file mode 100644 index 00000000000..a27068597d3 --- /dev/null +++ b/tools/debug-ui/README.md @@ -0,0 +1,41 @@ +# Nearcore Debug UI + +## How to Use +Clone nearcore, go to this directory, run `npm install` (only needed for first time), and then +``` +npm start +``` + +This will serve the UI at localhost:3000. + +Go to `http://localhost:3000/` to look at the debug UI of a near node. + +The RPC address can be either IP:port, or just IP (which will default to port 3030). + +## How to deploy in production +TBD. + +## Development + +The code is written in TypeScript with the React framework. The one thing most unintuitive about +React is React Hooks (the useState, useMemo, useCallback, useEffect, etc.) Understanding how +hooks work is a **must**: https://reactjs.org/docs/hooks-intro.html + +A few less-well-known hooks that are used often in this codebase: + +* `useMemo(func, [deps])` (from core React): returns func(), but only recomputing func()if any deps change from the last invocation (by shallow equality of the each dep). + +* `useEffect(func, [deps])` (from core React): similar to useMemo, but instead of returning func(), +just executes it, and func() is allowed to have side effects by mutating state (calling setXXX (that +comes from `const [XXX, setXXX] = useState(...);`)). + +* `useQuery([keys], () => promise)` (from react-query): returns `{data, error, isLoading}` which +represents fetching some data using the given promise. This is used to render asynchronously fetched +data. While the data is loading, `isLoading` is true; if there is an error, `error` is truthy; and +finally when there is data, `data` is truthy. This can be used to then render each state +accordingly. The keys given to the query are used to memoize the query, so that queries with the +same keys are only fetched once. + +It's also helpful to understand at a high level how the react-router library works; this is used +to support deep-linking in the URL (e.g. `/127.0.0.1/cluster` leads to the cluster page), allowing +the UI to be served as a single application. diff --git a/tools/debug-ui/package-lock.json b/tools/debug-ui/package-lock.json new file mode 100644 index 00000000000..f5886b5ac4c --- /dev/null +++ b/tools/debug-ui/package-lock.json @@ -0,0 +1,30363 @@ +{ + "name": "debug-ui", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "debug-ui", + "version": "0.1.0", + "dependencies": { + "@types/node": "^16.18.3", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-query": "^3.39.2", + "react-router": "^6.4.4", + "react-router-dom": "^6.4.4", + "react-scripts": "^5.0.1", + "react-xarrows": "^2.0.2", + "typescript": "^4.9.3" + }, + "devDependencies": { + "typescript-plugin-css-modules": "^4.1.1" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.0.1.tgz", + "integrity": "sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==", + "dev": true + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.5.tgz", + "integrity": "sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.5.tgz", + "integrity": "sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==", + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-module-transforms": "^7.20.2", + "@babel/helpers": "^7.20.5", + "@babel/parser": "^7.20.5", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/eslint-parser": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.19.1.tgz", + "integrity": "sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ==", + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.11.0", + "eslint": "^7.5.0 || ^8.0.0" + } + }, + "node_modules/@babel/eslint-parser/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@babel/eslint-parser/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz", + "integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==", + "dependencies": { + "@babel/types": "^7.20.5", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", + "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "dependencies": { + "@babel/helper-explode-assignable-expression": "^7.18.6", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", + "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", + "dependencies": { + "@babel/compat-data": "^7.20.0", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.5.tgz", + "integrity": "sha512-3RCdA/EmEaikrhayahwToF0fpweU/8o2p8vhc1c/1kftHOdTKuC65kik/TLc+qfbS8JKw4qqJbne4ovICDhmww==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-member-expression-to-functions": "^7.18.9", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.19.1", + "@babel/helper-split-export-declaration": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.20.5.tgz", + "integrity": "sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "regexpu-core": "^5.2.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "dependencies": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-explode-assignable-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", + "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", + "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "dependencies": { + "@babel/template": "^7.18.10", + "@babel/types": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz", + "integrity": "sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==", + "dependencies": { + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz", + "integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.20.2", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.1", + "@babel/types": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", + "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-wrap-function": "^7.18.9", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz", + "integrity": "sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.18.9", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/traverse": "^7.19.1", + "@babel/types": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "dependencies": { + "@babel/types": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", + "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "dependencies": { + "@babel/types": "^7.20.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", + "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", + "dependencies": { + "@babel/helper-function-name": "^7.19.0", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.6.tgz", + "integrity": "sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==", + "dependencies": { + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz", + "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", + "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz", + "integrity": "sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", + "@babel/plugin-proposal-optional-chaining": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.1.tgz", + "integrity": "sha512-Gh5rchzSwE4kC+o/6T8waD0WHEQIsDmjltY8WnWRXHUdH8axZhuH86Ov9M72YhJfDrZseQwuuWaaIT/TmePp3g==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-static-block": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz", + "integrity": "sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.20.5.tgz", + "integrity": "sha512-Lac7PpRJXcC3s9cKsBfl+uc+DYXU5FD06BrTFunQO6QIQT+DwyzDPURAowI3bcvD1dZF/ank1Z5rstUJn3Hn4Q==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-replace-supers": "^7.19.1", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/plugin-syntax-decorators": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz", + "integrity": "sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.2.tgz", + "integrity": "sha512-Ks6uej9WFK+fvIMesSqbAto5dD8Dz4VuuFvGJFKgIGSkJuRGcrwGECPA1fDgQK3/DbExBJpEkTeYeB8geIFCSQ==", + "dependencies": { + "@babel/compat-data": "^7.20.1", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz", + "integrity": "sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.20.5.tgz", + "integrity": "sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.19.0.tgz", + "integrity": "sha512-xaBZUEDntt4faL1yN8oIFlhfXeQAWJW7CLKYsHTUqriCUbj8xOra8bfxxKGi/UwExPFBuPdH4XfHc9rGQhrVkQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz", + "integrity": "sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", + "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", + "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz", + "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz", + "integrity": "sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz", + "integrity": "sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-remap-async-to-generator": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", + "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.5.tgz", + "integrity": "sha512-WvpEIW9Cbj9ApF3yJCjIEEf1EiNJLtXagOrL5LNWEZOo3jv8pmPoYTSNJQvqej8OavVlgOoOPw6/htGZro6IkA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.2.tgz", + "integrity": "sha512-9rbPp0lCVVoagvtEyQKSo5L8oo0nQS/iif+lwlAz29MccX2642vWDlSZK+2T2buxbopotId2ld7zZAzRfz9j1g==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-replace-supers": "^7.19.1", + "@babel/helper-split-export-declaration": "^7.18.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz", + "integrity": "sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.2.tgz", + "integrity": "sha512-mENM+ZHrvEgxLTBXUiQ621rRXZes3KWUv6NdQlrnr1TkWVw+hUjQBZuP2X32qKlrlG2BzgR95gkuCRSkJl8vIw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", + "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", + "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", + "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.19.0.tgz", + "integrity": "sha512-sgeMlNaQVbCSpgLSKP4ZZKfsJVnFnNQlUSk6gPYzR/q7tzCgQF2t8RBKAP6cKJeZdveei7Q7Jm527xepI8lNLg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/plugin-syntax-flow": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.18.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", + "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", + "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "dependencies": { + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", + "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", + "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.19.6.tgz", + "integrity": "sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==", + "dependencies": { + "@babel/helper-module-transforms": "^7.19.6", + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.19.6.tgz", + "integrity": "sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==", + "dependencies": { + "@babel/helper-module-transforms": "^7.19.6", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-simple-access": "^7.19.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.6.tgz", + "integrity": "sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==", + "dependencies": { + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-module-transforms": "^7.19.6", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-validator-identifier": "^7.19.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", + "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "dependencies": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", + "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", + "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", + "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.5.tgz", + "integrity": "sha512-h7plkOmcndIUWXZFLgpbrh2+fXAi47zcUX7IrOQuZdLD0I0KvjJ6cvo3BEcAOsDOcZhVKGJqv07mkSqK0y2isQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", + "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.20.2.tgz", + "integrity": "sha512-KS/G8YI8uwMGKErLFOHS/ekhqdHhpEloxs43NecQHVgo2QuQSyJhGIY1fL8UGl9wy5ItVwwoUL4YxVqsplGq2g==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz", + "integrity": "sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.19.0.tgz", + "integrity": "sha512-UVEvX3tXie3Szm3emi1+G63jyw1w5IcMY0FSKM+CRnKRI5Mr1YbCNgsSTwoTwKphQEG9P+QqmuRFneJPZuHNhg==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/plugin-syntax-jsx": "^7.18.6", + "@babel/types": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz", + "integrity": "sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz", + "integrity": "sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz", + "integrity": "sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "regenerator-transform": "^0.15.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", + "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz", + "integrity": "sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.19.0", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", + "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz", + "integrity": "sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", + "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", + "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", + "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.20.2.tgz", + "integrity": "sha512-jvS+ngBfrnTUBfOQq8NfGnSbF9BrqlR6hjJ2yVxMkmO5nL/cdifNbI30EfjRlN4g5wYWNnMPyj5Sa6R1pbLeag==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.20.2", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-typescript": "^7.20.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", + "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", + "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", + "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", + "dependencies": { + "@babel/compat-data": "^7.20.1", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.20.1", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.18.6", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.20.2", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.18.6", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.18.6", + "@babel/plugin-transform-async-to-generator": "^7.18.6", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.20.2", + "@babel/plugin-transform-classes": "^7.20.2", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.20.2", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.19.6", + "@babel/plugin-transform-modules-commonjs": "^7.19.6", + "@babel/plugin-transform-modules-systemjs": "^7.19.6", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.20.1", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.18.6", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.19.0", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.20.2", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "core-js-compat": "^3.25.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.18.6.tgz", + "integrity": "sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-transform-react-display-name": "^7.18.6", + "@babel/plugin-transform-react-jsx": "^7.18.6", + "@babel/plugin-transform-react-jsx-development": "^7.18.6", + "@babel/plugin-transform-react-pure-annotations": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz", + "integrity": "sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-transform-typescript": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz", + "integrity": "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==", + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.20.6.tgz", + "integrity": "sha512-tqeujPiuEfcH067mx+7otTQWROVMKHXEaOQcAeNV5dDdbPWvPcFA8/W9LXw2NfjNmOetqLl03dfnG2WALPlsRQ==", + "dependencies": { + "core-js-pure": "^3.25.1", + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", + "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.18.10", + "@babel/types": "^7.18.10" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz", + "integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==", + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.5", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.20.5", + "@babel/types": "^7.20.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/@babel/types": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz", + "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==", + "dependencies": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" + }, + "node_modules/@csstools/normalize.css": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.0.0.tgz", + "integrity": "sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg==" + }, + "node_modules/@csstools/postcss-cascade-layers": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz", + "integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==", + "dependencies": { + "@csstools/selector-specificity": "^2.0.2", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-color-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", + "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-color-function/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", + "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-font-format-keywords/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", + "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-hwb-function/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", + "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-ic-unit/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", + "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz", + "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-nested-calc/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", + "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-normalize-display-values/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", + "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-oklab-function/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", + "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", + "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-stepped-value-functions/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz", + "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz", + "integrity": "sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-trigonometric-functions/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/@csstools/postcss-unset-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", + "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz", + "integrity": "sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==", + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2", + "postcss-selector-parser": "^6.0.10" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", + "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", + "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", + "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==", + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", + "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/console/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/console/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@jest/console/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", + "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==", + "dependencies": { + "@jest/console": "^27.5.1", + "@jest/reporters": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^27.5.1", + "jest-config": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-resolve-dependencies": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "jest-watcher": "^27.5.1", + "micromatch": "^4.0.4", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@jest/core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", + "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", + "dependencies": { + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", + "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", + "dependencies": { + "@jest/types": "^27.5.1", + "@sinonjs/fake-timers": "^8.0.1", + "@types/node": "*", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", + "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/types": "^27.5.1", + "expect": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", + "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-haste-map": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^8.1.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@jest/reporters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", + "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", + "dependencies": { + "@sinclair/typebox": "^0.24.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", + "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", + "dependencies": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9", + "source-map": "^0.6.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", + "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", + "dependencies": { + "@jest/console": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz", + "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==", + "dependencies": { + "@jest/test-result": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-runtime": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", + "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", + "dependencies": { + "@babel/core": "^7.1.0", + "@jest/types": "^27.5.1", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-util": "^27.5.1", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/transform/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/transform/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@jest/transform/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz", + "integrity": "sha512-j0Ya0hCFZPd4x40qLzbhGsh9TMtdb+CJQiso+WxLOPNasohq9cc5SNUcwsZaRH6++Xh91Xkm/xHCkuIiIu0LUA==", + "dependencies": { + "ansi-html-community": "^0.0.8", + "common-path-prefix": "^3.0.0", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "find-up": "^5.0.0", + "html-entities": "^2.1.0", + "loader-utils": "^2.0.4", + "schema-utils": "^3.0.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "@types/webpack": "4.x || 5.x", + "react-refresh": ">=0.10.0 <1.0.0", + "sockjs-client": "^1.4.0", + "type-fest": ">=0.17.0 <4.0.0", + "webpack": ">=4.43.0 <6.0.0", + "webpack-dev-server": "3.x || 4.x", + "webpack-hot-middleware": "2.x", + "webpack-plugin-serve": "0.x || 1.x" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + }, + "sockjs-client": { + "optional": true + }, + "type-fest": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + }, + "webpack-hot-middleware": { + "optional": true + }, + "webpack-plugin-serve": { + "optional": true + } + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.4.tgz", + "integrity": "sha512-gTL8H5USTAKOyVA4xczzDJnC3HMssdFa3tRlwBicXynx9XfiXwneHnYQogwSKpdCkjXISrEKSTtX62rLpNEVQg==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "dependencies": { + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", + "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/pluginutils/node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", + "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==" + }, + "node_modules/@sinclair/typebox": { + "version": "0.24.51", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", + "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==" + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", + "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@surma/rollup-plugin-off-main-thread": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "dependencies": { + "ejs": "^3.1.6", + "json5": "^2.2.0", + "magic-string": "^0.25.0", + "string.prototype.matchall": "^4.0.6" + } + }, + "node_modules/@surma/rollup-plugin-off-main-thread/node_modules/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", + "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", + "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", + "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", + "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", + "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", + "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", + "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", + "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", + "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", + "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "^5.0.1", + "@svgr/babel-plugin-replace-jsx-attribute-value": "^5.0.1", + "@svgr/babel-plugin-svg-dynamic-title": "^5.4.0", + "@svgr/babel-plugin-svg-em-dimensions": "^5.4.0", + "@svgr/babel-plugin-transform-react-native-svg": "^5.4.0", + "@svgr/babel-plugin-transform-svg-component": "^5.5.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/core": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", + "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", + "dependencies": { + "@svgr/plugin-jsx": "^5.5.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", + "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", + "dependencies": { + "@babel/types": "^7.12.6" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", + "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", + "dependencies": { + "@babel/core": "^7.12.3", + "@svgr/babel-preset": "^5.5.0", + "@svgr/hast-util-to-babel-ast": "^5.5.0", + "svg-parser": "^2.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-svgo": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", + "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", + "dependencies": { + "cosmiconfig": "^7.0.0", + "deepmerge": "^4.2.2", + "svgo": "^1.2.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/webpack": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz", + "integrity": "sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/plugin-transform-react-constant-elements": "^7.12.1", + "@babel/preset-env": "^7.12.1", + "@babel/preset-react": "^7.12.5", + "@svgr/core": "^5.5.0", + "@svgr/plugin-jsx": "^5.5.0", + "@svgr/plugin-svgo": "^5.5.0", + "loader-utils": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/webpack/node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/@svgr/webpack/node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@svgr/webpack/node_modules/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@svgr/webpack/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.1.20", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.20.tgz", + "integrity": "sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ==", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz", + "integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==", + "dependencies": { + "@babel/types": "^7.3.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", + "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", + "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.4.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz", + "integrity": "sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", + "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==" + }, + "node_modules/@types/express": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", + "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.31", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", + "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", + "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.9", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", + "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" + }, + "node_modules/@types/mime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" + }, + "node_modules/@types/node": { + "version": "16.18.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.3.tgz", + "integrity": "sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg==" + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + }, + "node_modules/@types/prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + }, + "node_modules/@types/q": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", + "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==" + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "node_modules/@types/react": { + "version": "18.0.25", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.25.tgz", + "integrity": "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.0.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.9.tgz", + "integrity": "sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + }, + "node_modules/@types/semver": { + "version": "7.3.13", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==" + }, + "node_modules/@types/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "dependencies": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.33", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", + "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", + "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==" + }, + "node_modules/@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.45.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.45.1.tgz", + "integrity": "sha512-cOizjPlKEh0bXdFrBLTrI/J6B/QMlhwE9auOov53tgB+qMukH6/h8YAK/qw+QJGct/PTbdh2lytGyipxCcEtAw==", + "dependencies": { + "@typescript-eslint/scope-manager": "5.45.1", + "@typescript-eslint/type-utils": "5.45.1", + "@typescript-eslint/utils": "5.45.1", + "debug": "^4.3.4", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "5.45.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.45.1.tgz", + "integrity": "sha512-WlXwY9dbmc0Lzu6xQOZ3yN8u/ws/1R8zPC16O217LMZJCbV2hJezqkWMUB+jMwguOJW+cukCDe92vcwwf8zwjQ==", + "dependencies": { + "@typescript-eslint/utils": "5.45.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.45.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.45.1.tgz", + "integrity": "sha512-JQ3Ep8bEOXu16q0ztsatp/iQfDCtvap7sp/DKo7DWltUquj5AfCOpX2zSzJ8YkAVnrQNqQ5R62PBz2UtrfmCkA==", + "dependencies": { + "@typescript-eslint/scope-manager": "5.45.1", + "@typescript-eslint/types": "5.45.1", + "@typescript-eslint/typescript-estree": "5.45.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.45.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.45.1.tgz", + "integrity": "sha512-D6fCileR6Iai7E35Eb4Kp+k0iW7F1wxXYrOhX/3dywsOJpJAQ20Fwgcf+P/TDtvQ7zcsWsrJaglaQWDhOMsspQ==", + "dependencies": { + "@typescript-eslint/types": "5.45.1", + "@typescript-eslint/visitor-keys": "5.45.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.45.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.45.1.tgz", + "integrity": "sha512-aosxFa+0CoYgYEl3aptLe1svP910DJq68nwEJzyQcrtRhC4BN0tJAvZGAe+D0tzjJmFXe+h4leSsiZhwBa2vrA==", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.45.1", + "@typescript-eslint/utils": "5.45.1", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/@typescript-eslint/types": { + "version": "5.45.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.45.1.tgz", + "integrity": "sha512-HEW3U0E5dLjUT+nk7b4lLbOherS1U4ap+b9pfu2oGsW3oPu7genRaY9dDv3nMczC1rbnRY2W/D7SN05wYoGImg==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.45.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.1.tgz", + "integrity": "sha512-76NZpmpCzWVrrb0XmYEpbwOz/FENBi+5W7ipVXAsG3OoFrQKJMiaqsBMbvGRyLtPotGqUfcY7Ur8j0dksDJDng==", + "dependencies": { + "@typescript-eslint/types": "5.45.1", + "@typescript-eslint/visitor-keys": "5.45.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.45.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.45.1.tgz", + "integrity": "sha512-rlbC5VZz68+yjAzQBc4I7KDYVzWG2X/OrqoZrMahYq3u8FFtmQYc+9rovo/7wlJH5kugJ+jQXV5pJMnofGmPRw==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.45.1", + "@typescript-eslint/types": "5.45.1", + "@typescript-eslint/typescript-estree": "5.45.1", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.45.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.1.tgz", + "integrity": "sha512-cy9ln+6rmthYWjH9fmx+5FU/JDpjQb586++x2FZlveq7GdGuLLW9a2Jcst2TGekH82bXpfmRNSwP9tyEs6RjvQ==", + "dependencies": { + "@typescript-eslint/types": "5.45.1", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "dependencies": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + } + }, + "node_modules/acorn-globals/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dependencies": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "node_modules/acorn-node/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/address/-/address-1.2.1.tgz", + "integrity": "sha512-B+6bi5D34+fDYENiH5qOlA0cV2rAGKuWZ9LeyUUehbXy8e0VS9e498yO0Jeeh+iM+6KbfudHTFjXw2MmJD4QRA==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "dependencies": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" + }, + "node_modules/array-includes": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", + "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.reduce": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz", + "integrity": "sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", + "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.1.3" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "node_modules/ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==" + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.13", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", + "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-lite": "^1.0.30001426", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/autoprefixer/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/axe-core": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.5.2.tgz", + "integrity": "sha512-u2MVsXfew5HBvjsczCv+xlwdNnB1oQR9HlAcsejZttNjKKSkeDNVwB1vMThIUIFI9GoT57Vtk8iQLwqOfAkboA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", + "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==" + }, + "node_modules/babel-jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", + "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", + "dependencies": { + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/babel-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/babel-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-loader": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", + "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", + "dependencies": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "engines": { + "node": ">= 8.9" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "webpack": ">=2" + } + }, + "node_modules/babel-loader/node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/babel-loader/node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/babel-loader/node_modules/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/babel-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/babel-loader/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dependencies": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/babel-loader/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", + "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-named-asset-import": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz", + "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==", + "peerDependencies": { + "@babel/core": "^7.1.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "dependencies": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.3", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", + "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3", + "core-js-compat": "^3.25.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-transform-react-remove-prop-types": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", + "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==" + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", + "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", + "dependencies": { + "babel-plugin-jest-hoist": "^27.5.1", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-react-app": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz", + "integrity": "sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg==", + "dependencies": { + "@babel/core": "^7.16.0", + "@babel/plugin-proposal-class-properties": "^7.16.0", + "@babel/plugin-proposal-decorators": "^7.16.4", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", + "@babel/plugin-proposal-numeric-separator": "^7.16.0", + "@babel/plugin-proposal-optional-chaining": "^7.16.0", + "@babel/plugin-proposal-private-methods": "^7.16.0", + "@babel/plugin-transform-flow-strip-types": "^7.16.0", + "@babel/plugin-transform-react-display-name": "^7.16.0", + "@babel/plugin-transform-runtime": "^7.16.4", + "@babel/preset-env": "^7.16.4", + "@babel/preset-react": "^7.16.0", + "@babel/preset-typescript": "^7.16.0", + "@babel/runtime": "^7.16.3", + "babel-plugin-macros": "^3.1.0", + "babel-plugin-transform-react-remove-prop-types": "^0.4.24" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==" + }, + "node_modules/bfj": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz", + "integrity": "sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw==", + "dependencies": { + "bluebird": "^3.5.5", + "check-types": "^11.1.1", + "hoopy": "^0.1.4", + "tryer": "^1.0.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/bonjour-service": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.0.14.tgz", + "integrity": "sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ==", + "dependencies": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/broadcast-channel": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", + "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", + "dependencies": { + "@babel/runtime": "^7.7.2", + "detect-node": "^2.1.0", + "js-sha3": "0.8.0", + "microseconds": "0.2.0", + "nano-time": "1.0.0", + "oblivious-set": "1.0.0", + "rimraf": "3.0.2", + "unload": "2.2.0" + } + }, + "node_modules/browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" + }, + "node_modules/browserslist": { + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001436", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001436.tgz", + "integrity": "sha512-ZmWkKsnC2ifEPoWUvSAIGyOYwT+keAaaWPHiQ9DfMqS1t6tfuyFYoWR78TeZtznkEQ64+vGXH9cZrElwR2Mrxg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] + }, + "node_modules/case-sensitive-paths-webpack-plugin": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", + "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/check-types": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.2.tgz", + "integrity": "sha512-HBiYvXvn9Z70Z88XKjz3AEKd4HJhBXsa3j7xFnITAzoS8+q6eIGi8qDB8FKPBAjtuxjI/zFpwuiCb8oDtKOYrA==" + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.0.tgz", + "integrity": "sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", + "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==" + }, + "node_modules/clean-css": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.1.tgz", + "integrity": "sha512-lCr8OHhiWCTw4v8POJovCoh4T7I9U11yVsPjMWWnnMmp9ZowCxyad1Pathle/9HjaDp+fdQKjO9fQydE6RHTZg==", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "dependencies": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==" + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" + }, + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==" + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==" + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/core-js": { + "version": "3.26.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.1.tgz", + "integrity": "sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.26.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.1.tgz", + "integrity": "sha512-622/KzTudvXCDLRw70iHW4KKs1aGpcRcowGWyYJr2DEBfRrd6hNJybxSWJFuZYD4ma86xhrwDDHxmDaIq4EA8A==", + "dependencies": { + "browserslist": "^4.21.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-pure": { + "version": "3.26.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.26.1.tgz", + "integrity": "sha512-VVXcDpp/xJ21KdULRq/lXdLzQAtX7+37LzpyfFM973il0tWSsDEoyzG38G14AjTpK9VTfiNM9jnFauq/CpaWGQ==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/css-blank-pseudo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", + "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "bin": { + "css-blank-pseudo": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-declaration-sorter": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz", + "integrity": "sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==", + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-has-pseudo": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", + "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "bin": { + "css-has-pseudo": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-loader": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.2.tgz", + "integrity": "sha512-oqGbbVcBJkm8QwmnNzrFrWTnudnRZC+1eXikLJl0n4ljcfotgRifpg2a1lKy8jTrc4/d9A/ap1GFq1jDKG7J+Q==", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.18", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-loader/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-minimizer-webpack-plugin": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz", + "integrity": "sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==", + "dependencies": { + "cssnano": "^5.0.6", + "jest-worker": "^27.0.2", + "postcss": "^8.3.5", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@parcel/css": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", + "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", + "bin": { + "css-prefers-color-scheme": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" + }, + "node_modules/css-selector-tokenizer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz", + "integrity": "sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, + "node_modules/css-tree": { + "version": "1.0.0-alpha.37", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", + "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "dependencies": { + "mdn-data": "2.0.4", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssdb": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.2.0.tgz", + "integrity": "sha512-JYlIsE7eKHSi0UNuCyo96YuIDFqvhGgHw4Ck6lsN+DP0Tp8M64UTDT2trGbkMDqnCoEjks7CkS0XcjU0rkvBdg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "5.1.14", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.14.tgz", + "integrity": "sha512-Oou7ihiTocbKqi0J1bB+TRJIQX5RMR3JghA8hcWSw9mjBLQ5Y3RWqEDoYG3sRNlAbCIXpqMoZGbq5KDR3vdzgw==", + "dependencies": { + "cssnano-preset-default": "^5.2.13", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-preset-default": { + "version": "5.2.13", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.13.tgz", + "integrity": "sha512-PX7sQ4Pb+UtOWuz8A1d+Rbi+WimBIxJTRyBdgGp1J75VU0r/HFQeLnMYgHiCAp6AR4rqrc7Y4R+1Rjk3KJz6DQ==", + "dependencies": { + "css-declaration-sorter": "^6.3.1", + "cssnano-utils": "^3.1.0", + "postcss-calc": "^8.2.3", + "postcss-colormin": "^5.3.0", + "postcss-convert-values": "^5.1.3", + "postcss-discard-comments": "^5.1.2", + "postcss-discard-duplicates": "^5.1.0", + "postcss-discard-empty": "^5.1.1", + "postcss-discard-overridden": "^5.1.0", + "postcss-merge-longhand": "^5.1.7", + "postcss-merge-rules": "^5.1.3", + "postcss-minify-font-values": "^5.1.0", + "postcss-minify-gradients": "^5.1.1", + "postcss-minify-params": "^5.1.4", + "postcss-minify-selectors": "^5.2.1", + "postcss-normalize-charset": "^5.1.0", + "postcss-normalize-display-values": "^5.1.0", + "postcss-normalize-positions": "^5.1.1", + "postcss-normalize-repeat-style": "^5.1.1", + "postcss-normalize-string": "^5.1.0", + "postcss-normalize-timing-functions": "^5.1.0", + "postcss-normalize-unicode": "^5.1.1", + "postcss-normalize-url": "^5.1.0", + "postcss-normalize-whitespace": "^5.1.1", + "postcss-ordered-values": "^5.1.3", + "postcss-reduce-initial": "^5.1.1", + "postcss-reduce-transforms": "^5.1.0", + "postcss-svgo": "^5.1.0", + "postcss-unique-selectors": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "node_modules/cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + }, + "node_modules/csstype": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" + }, + "node_modules/data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dependencies": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + }, + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "node_modules/deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defined": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" + }, + "node_modules/detect-port-alt": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", + "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", + "dependencies": { + "address": "^1.0.1", + "debug": "^2.6.0" + }, + "bin": { + "detect": "bin/detect-port", + "detect-port": "bin/detect-port" + }, + "engines": { + "node": ">= 4.2.1" + } + }, + "node_modules/detect-port-alt/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/detect-port-alt/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/detective": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", + "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "dependencies": { + "acorn-node": "^1.8.2", + "defined": "^1.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "detective": "bin/detective.js" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + }, + "node_modules/diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, + "node_modules/dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==" + }, + "node_modules/dns-packet": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.4.0.tgz", + "integrity": "sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "dependencies": { + "webidl-conversions": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/domexception/node_modules/webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/ejs": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", + "integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.284", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", + "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==" + }, + "node_modules/emittery": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", + "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha512-knHEZMgs8BB+MInokmNTg/OyPlAddghe1YBgNwJBc5zsJi/uyIcXoSDsL/W9ymOsBoBGdPIHXYJ9+qKFwRwDng==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", + "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-abstract": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", + "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" + }, + "node_modules/es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==" + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", + "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.29.0.tgz", + "integrity": "sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==", + "dependencies": { + "@eslint/eslintrc": "^1.3.3", + "@humanwhocodes/config-array": "^0.11.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.15.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-react-app": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", + "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", + "dependencies": { + "@babel/core": "^7.16.0", + "@babel/eslint-parser": "^7.16.3", + "@rushstack/eslint-patch": "^1.1.0", + "@typescript-eslint/eslint-plugin": "^5.5.0", + "@typescript-eslint/parser": "^5.5.0", + "babel-preset-react-app": "^10.0.1", + "confusing-browser-globals": "^1.0.11", + "eslint-plugin-flowtype": "^8.0.3", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jest": "^25.3.0", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.27.1", + "eslint-plugin-react-hooks": "^4.3.0", + "eslint-plugin-testing-library": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "eslint": "^8.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dependencies": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", + "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-flowtype": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", + "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==", + "dependencies": { + "lodash": "^4.17.21", + "string-natural-compare": "^3.0.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@babel/plugin-syntax-flow": "^7.14.5", + "@babel/plugin-transform-react-jsx": "^7.14.9", + "eslint": "^8.1.0" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", + "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "dependencies": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.3", + "has": "^1.0.3", + "is-core-module": "^2.8.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.5", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/eslint-plugin-import/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/eslint-plugin-import/node_modules/tsconfig-paths": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", + "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", + "dependencies": { + "@typescript-eslint/experimental-utils": "^5.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^4.0.0 || ^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz", + "integrity": "sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q==", + "dependencies": { + "@babel/runtime": "^7.18.9", + "aria-query": "^4.2.2", + "array-includes": "^3.1.5", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.4.3", + "axobject-query": "^2.2.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "has": "^1.0.3", + "jsx-ast-utils": "^3.3.2", + "language-tags": "^1.0.5", + "minimatch": "^3.1.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.31.11", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.11.tgz", + "integrity": "sha512-TTvq5JsT5v56wPa9OYHzsrOlHzKZKjV+aLgS+55NJP/cuzdiQPC7PfYoUjMoxlffKtvijpk7vA/jmuqRb9nohw==", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.3", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.8" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", + "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-testing-library": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.9.1.tgz", + "integrity": "sha512-6BQp3tmb79jLLasPHJmy8DnxREe+2Pgf7L+7o09TSWPfdqqtQfRZmZNetr5mOs3yqZk/MRNxpN3RUpJe0wB4LQ==", + "dependencies": { + "@typescript-eslint/utils": "^5.13.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0", + "npm": ">=6" + }, + "peerDependencies": { + "eslint": "^7.5.0 || ^8.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", + "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==", + "dependencies": { + "@types/eslint": "^7.29.0 || ^8.4.1", + "jest-worker": "^28.0.2", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/jest-worker": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", + "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/eslint-webpack-plugin/node_modules/schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", + "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", + "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", + "dependencies": { + "@jest/types": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", + "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/file-loader/node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/file-loader/node_modules/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/file-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.1.tgz", + "integrity": "sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/filesize": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", + "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-cache-dir/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz", + "integrity": "sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==", + "dependencies": { + "@babel/code-frame": "^7.8.3", + "@types/json-schema": "^7.0.5", + "chalk": "^4.1.0", + "chokidar": "^3.4.2", + "cosmiconfig": "^6.0.0", + "deepmerge": "^4.2.2", + "fs-extra": "^9.0.0", + "glob": "^7.1.6", + "memfs": "^3.1.2", + "minimatch": "^3.0.4", + "schema-utils": "2.7.0", + "semver": "^7.3.2", + "tapable": "^1.0.0" + }, + "engines": { + "node": ">=10", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "eslint": ">= 6", + "typescript": ">= 2.7", + "vue-template-compiler": "*", + "webpack": ">= 4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + } + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "dependencies": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generic-names": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-1.0.3.tgz", + "integrity": "sha512-b6OHfQuKasIKM9b6YPkX+KUj/TLBTx3B/1aT1T5F12FEuEqyFMdr59OMS53aoaSw8eVtapdqieX6lbg5opaOhA==", + "dev": true, + "dependencies": { + "loader-utils": "^0.2.16" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==" + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==" + }, + "node_modules/harmony-reflect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", + "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==" + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hoopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", + "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "dependencies": { + "whatwg-encoding": "^1.0.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", + "integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==", + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "webpack": "^5.20.0" + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + }, + "node_modules/identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", + "dependencies": { + "harmony-reflect": "^1.4.6" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", + "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immer": { + "version": "9.0.16", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.16.tgz", + "integrity": "sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/immutable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", + "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", + "devOptional": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ipaddr.js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", + "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-root": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", + "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.8.5", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", + "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", + "dependencies": { + "@jest/core": "^27.5.1", + "import-local": "^3.0.2", + "jest-cli": "^27.5.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", + "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==", + "dependencies": { + "@jest/types": "^27.5.1", + "execa": "^5.0.0", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-circus": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz", + "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "expect": "^27.5.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-circus/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-circus/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", + "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", + "dependencies": { + "@jest/core": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "prompts": "^2.0.1", + "yargs": "^16.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", + "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==", + "dependencies": { + "@babel/core": "^7.8.0", + "@jest/test-sequencer": "^27.5.1", + "@jest/types": "^27.5.1", + "babel-jest": "^27.5.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.9", + "jest-circus": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-jasmine2": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-config/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-config/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-docblock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", + "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-each": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", + "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", + "dependencies": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "jest-get-type": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-each/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-each/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-each/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", + "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1", + "jsdom": "^16.6.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", + "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", + "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^27.5.1", + "jest-serializer": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "micromatch": "^4.0.4", + "walker": "^1.0.7" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-jasmine2": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", + "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "expect": "^27.5.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-jasmine2/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-jasmine2/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-jasmine2/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-jasmine2/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-jasmine2/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-jasmine2/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-leak-detector": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", + "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==", + "dependencies": { + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-matcher-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-message-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-message-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", + "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", + "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz", + "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==", + "dependencies": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "resolve": "^1.20.0", + "resolve.exports": "^1.1.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz", + "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==", + "dependencies": { + "@jest/types": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-snapshot": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-resolve/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-resolve/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", + "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==", + "dependencies": { + "@jest/console": "^27.5.1", + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-leak-detector": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "source-map-support": "^0.5.6", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runner/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runner/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-runner/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", + "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/globals": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "execa": "^5.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runtime/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runtime/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-runtime/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-serializer": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", + "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", + "dependencies": { + "@types/node": "*", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", + "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", + "dependencies": { + "@babel/core": "^7.7.2", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.0.0", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__traverse": "^7.0.4", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^27.5.1", + "semver": "^7.3.2" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-snapshot/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz", + "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==", + "dependencies": { + "@jest/types": "^27.5.1", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^27.5.1", + "leven": "^3.1.0", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-validate/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-validate/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-1.1.0.tgz", + "integrity": "sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw==", + "dependencies": { + "ansi-escapes": "^4.3.1", + "chalk": "^4.0.0", + "jest-regex-util": "^28.0.0", + "jest-watcher": "^28.0.0", + "slash": "^4.0.0", + "string-length": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "jest": "^27.0.0 || ^28.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@jest/console": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz", + "integrity": "sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==", + "dependencies": { + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3", + "slash": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@jest/console/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@jest/test-result": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz", + "integrity": "sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==", + "dependencies": { + "@jest/console": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@jest/types": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", + "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", + "dependencies": { + "@jest/schemas": "^28.1.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/@types/yargs": { + "version": "17.0.16", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.16.tgz", + "integrity": "sha512-Mh3OP0oh8X7O7F9m5AplC+XHYLBWuPKNkGVD3gIZFLFebBnuFI2Nz5Sf8WLvwGxECJ8YjifQvFdh79ubODkdug==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-watch-typeahead/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-watch-typeahead/node_modules/emittery": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", + "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-message-util": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", + "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^28.1.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^28.1.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-message-util/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-regex-util": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", + "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-util": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", + "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", + "dependencies": { + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-watcher": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz", + "integrity": "sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==", + "dependencies": { + "@jest/test-result": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.10.2", + "jest-util": "^28.1.3", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/pretty-format": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", + "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", + "dependencies": { + "@jest/schemas": "^28.1.3", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/jest-watch-typeahead/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watch-typeahead/node_modules/string-length": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", + "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==", + "dependencies": { + "char-regex": "^2.0.0", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watch-typeahead/node_modules/string-length/node_modules/char-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.1.tgz", + "integrity": "sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/jest-watch-typeahead/node_modules/strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz", + "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==", + "dependencies": { + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "jest-util": "^27.5.1", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-watcher/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-watcher/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-watcher/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-sdsl": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", + "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", + "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "dependencies": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.6", + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, + "node_modules/json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", + "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==", + "dependencies": { + "array-includes": "^3.1.5", + "object.assign": "^4.1.3" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", + "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==" + }, + "node_modules/language-tags": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.6.tgz", + "integrity": "sha512-HNkaCgM8wZgE/BZACeotAAgpL9FUjEnhgF0FVQMIgH//zqTPreLYMb3rWYkYAqPoF75Jwuycp1da7uz66cfFQg==", + "dependencies": { + "language-subtag-registry": "^0.3.20" + } + }, + "node_modules/less": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", + "integrity": "sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==", + "dev": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", + "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha512-tiv66G0SmiOx+pLWMtGEkfSEejxvb6N6uRrQjfWJIT79W9GMpgKeCAmm9aVBKtd4WEgntciI8CsGqjpDoCWJug==", + "dev": true, + "dependencies": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/match-sorter": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz", + "integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "remove-accents": "0.4.2" + } + }, + "node_modules/mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.12.tgz", + "integrity": "sha512-BcjuQn6vfqP+k100e0E9m61Hyqa//Brp+I3f0OBmN0ATHlFA8vx3Lt8z57R3u2bPqe3WGDBC+nF72fTH7isyEw==", + "dependencies": { + "fs-monkey": "^1.0.3" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/microseconds": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", + "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==" + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.2.tgz", + "integrity": "sha512-EdlUizq13o0Pd+uCp+WO/JpkLvHRVGt97RqfeGhXqAcorYo1ypJSpkV+WDT0vY/kmh/p7wRdJNJtuyK540PXDw==", + "dependencies": { + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/nano-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", + "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==", + "dependencies": { + "big-integer": "^1.6.16" + } + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==" + }, + "node_modules/needle": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz", + "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==", + "dev": true, + "optional": true, + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" + }, + "node_modules/node-releases": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", + "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", + "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", + "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.getownpropertydescriptors": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz", + "integrity": "sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw==", + "dependencies": { + "array.prototype.reduce": "^1.0.5", + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.hasown": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", + "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", + "dependencies": { + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/oblivious-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", + "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==" + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.4.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz", + "integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz", + "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-browser-comments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz", + "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==", + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "browserslist": ">=4", + "postcss": ">=8" + } + }, + "node_modules/postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "dependencies": { + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-calc/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } + }, + "node_modules/postcss-clamp/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-color-functional-notation": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", + "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-color-functional-notation/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-color-hex-alpha": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz", + "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-hex-alpha/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz", + "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-color-rebeccapurple/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-colormin": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.0.tgz", + "integrity": "sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg==", + "dependencies": { + "browserslist": "^4.16.6", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-colormin/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-convert-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", + "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-convert-values/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-custom-media": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz", + "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/postcss-custom-media/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-custom-properties": { + "version": "12.1.11", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz", + "integrity": "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-custom-properties/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-custom-selectors": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz", + "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==", + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz", + "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-discard-comments": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-empty": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz", + "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-double-position-gradients/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-env-function": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", + "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-env-function/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-filter-plugins": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/postcss-filter-plugins/-/postcss-filter-plugins-3.0.1.tgz", + "integrity": "sha512-tRKbW4wWBEkSSFuJtamV2wkiV9rj6Yy7P3Y13+zaynlPEEZt8EgYKn3y/RBpMeIhNmHXFlSdzofml65hD5OafA==", + "dev": true, + "dependencies": { + "postcss": "^6.0.14" + } + }, + "node_modules/postcss-filter-plugins/node_modules/postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "dependencies": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/postcss-flexbugs-fixes": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", + "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", + "peerDependencies": { + "postcss": "^8.1.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", + "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", + "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-gap-properties": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", + "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-icss-keyframes": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/postcss-icss-keyframes/-/postcss-icss-keyframes-0.2.1.tgz", + "integrity": "sha512-4m+hLY5TVqoTM198KKnzdNudyu1OvtqwD+8kVZ9PNiEO4+IfHYoyVvEXsOHjV8nZ1k6xowf+nY4HlUfZhOFvvw==", + "dev": true, + "dependencies": { + "icss-utils": "^3.0.1", + "postcss": "^6.0.2", + "postcss-value-parser": "^3.3.0" + } + }, + "node_modules/postcss-icss-keyframes/node_modules/icss-utils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-3.0.1.tgz", + "integrity": "sha512-ANhVLoEfe0KoC9+z4yiTaXOneB49K6JIXdS+yAgH0NERELpdIT7kkj2XxUPuHafeHnn8umXnECSpsfk1RTaUew==", + "dev": true, + "dependencies": { + "postcss": "^6.0.2" + } + }, + "node_modules/postcss-icss-keyframes/node_modules/postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "dependencies": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/postcss-icss-selectors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/postcss-icss-selectors/-/postcss-icss-selectors-2.0.3.tgz", + "integrity": "sha512-dxFtq+wscbU9faJaH8kIi98vvCPDbt+qg1g9GoG0os1PY3UvgY1Y2G06iZrZb1iVC9cyFfafwSY1IS+IQpRQ4w==", + "dev": true, + "dependencies": { + "css-selector-tokenizer": "^0.7.0", + "generic-names": "^1.0.2", + "icss-utils": "^3.0.1", + "lodash": "^4.17.4", + "postcss": "^6.0.2" + } + }, + "node_modules/postcss-icss-selectors/node_modules/icss-utils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-3.0.1.tgz", + "integrity": "sha512-ANhVLoEfe0KoC9+z4yiTaXOneB49K6JIXdS+yAgH0NERELpdIT7kkj2XxUPuHafeHnn8umXnECSpsfk1RTaUew==", + "dev": true, + "dependencies": { + "postcss": "^6.0.2" + } + }, + "node_modules/postcss-icss-selectors/node_modules/postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "dependencies": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/postcss-image-set-function": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz", + "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-image-set-function/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-import/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-initial": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", + "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-lab-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz", + "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-lab-function/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-loader": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", + "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", + "dependencies": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-loader/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postcss-logical": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-media-minmax": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", + "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-merge-longhand/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-merge-rules": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.3.tgz", + "integrity": "sha512-LbLd7uFC00vpOuMvyZop8+vvhnfRGpp2S+IMQKeuOZZapPRY4SMq5ErjQeHbHsjCUgJkRNrlU+LmxsKIqPKQlA==", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^3.1.0", + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", + "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-font-values/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-minify-gradients": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", + "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", + "dependencies": { + "colord": "^2.9.1", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-gradients/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-minify-params": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", + "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", + "dependencies": { + "browserslist": "^4.21.4", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-params/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-minify-selectors": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", + "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nesting": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz", + "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==", + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-normalize": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz", + "integrity": "sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA==", + "dependencies": { + "@csstools/normalize.css": "*", + "postcss-browser-comments": "^4", + "sanitize.css": "*" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "browserslist": ">= 4", + "postcss": ">= 8" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", + "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-display-values/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-normalize-positions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", + "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-positions/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", + "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-repeat-style/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-normalize-string": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", + "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-string/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", + "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-timing-functions/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-normalize-unicode": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", + "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-unicode/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-normalize-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", + "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", + "dependencies": { + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-url/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-normalize-whitespace": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", + "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-whitespace/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-opacity-percentage": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.2.tgz", + "integrity": "sha512-lyUfF7miG+yewZ8EAk9XUBIlrHyUE6fijnesuz+Mj5zrIHIEw6KcIZSOk/elVMqzLvREmXB83Zi/5QpNRYd47w==", + "funding": [ + { + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" + } + ], + "engines": { + "node": "^12 || ^14 || >=16" + } + }, + "node_modules/postcss-ordered-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", + "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", + "dependencies": { + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-ordered-values/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-overflow-shorthand": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz", + "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-overflow-shorthand/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz", + "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-place/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-preset-env": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz", + "integrity": "sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==", + "dependencies": { + "@csstools/postcss-cascade-layers": "^1.1.1", + "@csstools/postcss-color-function": "^1.1.1", + "@csstools/postcss-font-format-keywords": "^1.0.1", + "@csstools/postcss-hwb-function": "^1.0.2", + "@csstools/postcss-ic-unit": "^1.0.1", + "@csstools/postcss-is-pseudo-class": "^2.0.7", + "@csstools/postcss-nested-calc": "^1.0.0", + "@csstools/postcss-normalize-display-values": "^1.0.1", + "@csstools/postcss-oklab-function": "^1.1.1", + "@csstools/postcss-progressive-custom-properties": "^1.3.0", + "@csstools/postcss-stepped-value-functions": "^1.0.1", + "@csstools/postcss-text-decoration-shorthand": "^1.0.0", + "@csstools/postcss-trigonometric-functions": "^1.0.2", + "@csstools/postcss-unset-value": "^1.0.2", + "autoprefixer": "^10.4.13", + "browserslist": "^4.21.4", + "css-blank-pseudo": "^3.0.3", + "css-has-pseudo": "^3.0.4", + "css-prefers-color-scheme": "^6.0.3", + "cssdb": "^7.1.0", + "postcss-attribute-case-insensitive": "^5.0.2", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^4.2.4", + "postcss-color-hex-alpha": "^8.0.4", + "postcss-color-rebeccapurple": "^7.1.1", + "postcss-custom-media": "^8.0.2", + "postcss-custom-properties": "^12.1.10", + "postcss-custom-selectors": "^6.0.3", + "postcss-dir-pseudo-class": "^6.0.5", + "postcss-double-position-gradients": "^3.1.2", + "postcss-env-function": "^4.0.6", + "postcss-focus-visible": "^6.0.4", + "postcss-focus-within": "^5.0.4", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^3.0.5", + "postcss-image-set-function": "^4.0.7", + "postcss-initial": "^4.0.1", + "postcss-lab-function": "^4.2.1", + "postcss-logical": "^5.0.4", + "postcss-media-minmax": "^5.0.0", + "postcss-nesting": "^10.2.0", + "postcss-opacity-percentage": "^1.1.2", + "postcss-overflow-shorthand": "^3.0.4", + "postcss-page-break": "^3.0.4", + "postcss-place": "^7.0.5", + "postcss-pseudo-class-any-link": "^7.1.6", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-preset-env/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz", + "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.1.tgz", + "integrity": "sha512-//jeDqWcHPuXGZLoolFrUXBDyuEGbr9S2rMo19bkTIjBQ4PqkaO+oI8wua5BOUxpfi97i3PCoInsiFIEBfkm9w==", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", + "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-transforms/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz", + "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", + "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", + "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^2.7.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/postcss-svgo/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/postcss-svgo/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "node_modules/postcss-svgo/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss-svgo/node_modules/svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", + "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-app-polyfill": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz", + "integrity": "sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w==", + "dependencies": { + "core-js": "^3.19.2", + "object-assign": "^4.1.1", + "promise": "^8.1.0", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.9", + "whatwg-fetch": "^3.6.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/react-dev-utils": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", + "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", + "dependencies": { + "@babel/code-frame": "^7.16.0", + "address": "^1.1.2", + "browserslist": "^4.18.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "detect-port-alt": "^1.1.6", + "escape-string-regexp": "^4.0.0", + "filesize": "^8.0.6", + "find-up": "^5.0.0", + "fork-ts-checker-webpack-plugin": "^6.5.0", + "global-modules": "^2.0.0", + "globby": "^11.0.4", + "gzip-size": "^6.0.0", + "immer": "^9.0.7", + "is-root": "^2.1.0", + "loader-utils": "^3.2.0", + "open": "^8.4.0", + "pkg-up": "^3.1.0", + "prompts": "^2.4.2", + "react-error-overlay": "^6.0.11", + "recursive-readdir": "^2.2.2", + "shell-quote": "^1.7.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/react-dev-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/react-dev-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/react-dev-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/react-dev-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/react-dev-utils/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dev-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/react-dev-utils/node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/react-dev-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-error-overlay": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", + "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "node_modules/react-query": { + "version": "3.39.2", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.2.tgz", + "integrity": "sha512-F6hYDKyNgDQfQOuR1Rsp3VRzJnWHx6aRnnIZHMNGGgbL3SBgpZTDg8MQwmxOgpCAoqZJA+JSNCydF1xGJqKOCA==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "broadcast-channel": "^3.4.1", + "match-sorter": "^6.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", + "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.4.4.tgz", + "integrity": "sha512-SA6tSrUCRfuLWeYsTJDuriRqfFIsrSvuH7SqAJHegx9ZgxadE119rU8oOX/rG5FYEthpdEaEljdjDlnBxvfr+Q==", + "dependencies": { + "@remix-run/router": "1.0.4" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.4.4.tgz", + "integrity": "sha512-0Axverhw5d+4SBhLqLpzPhNkmv7gahUwlUVIOrRLGJ4/uwt30JVajVJXqv2Qr/LCwyvHhQc7YyK1Do8a9Jj7qA==", + "dependencies": { + "@remix-run/router": "1.0.4", + "react-router": "6.4.4" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-scripts": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", + "integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==", + "dependencies": { + "@babel/core": "^7.16.0", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", + "@svgr/webpack": "^5.5.0", + "babel-jest": "^27.4.2", + "babel-loader": "^8.2.3", + "babel-plugin-named-asset-import": "^0.3.8", + "babel-preset-react-app": "^10.0.1", + "bfj": "^7.0.2", + "browserslist": "^4.18.1", + "camelcase": "^6.2.1", + "case-sensitive-paths-webpack-plugin": "^2.4.0", + "css-loader": "^6.5.1", + "css-minimizer-webpack-plugin": "^3.2.0", + "dotenv": "^10.0.0", + "dotenv-expand": "^5.1.0", + "eslint": "^8.3.0", + "eslint-config-react-app": "^7.0.1", + "eslint-webpack-plugin": "^3.1.1", + "file-loader": "^6.2.0", + "fs-extra": "^10.0.0", + "html-webpack-plugin": "^5.5.0", + "identity-obj-proxy": "^3.0.0", + "jest": "^27.4.3", + "jest-resolve": "^27.4.2", + "jest-watch-typeahead": "^1.0.0", + "mini-css-extract-plugin": "^2.4.5", + "postcss": "^8.4.4", + "postcss-flexbugs-fixes": "^5.0.2", + "postcss-loader": "^6.2.1", + "postcss-normalize": "^10.0.1", + "postcss-preset-env": "^7.0.1", + "prompts": "^2.4.2", + "react-app-polyfill": "^3.0.0", + "react-dev-utils": "^12.0.1", + "react-refresh": "^0.11.0", + "resolve": "^1.20.0", + "resolve-url-loader": "^4.0.0", + "sass-loader": "^12.3.0", + "semver": "^7.3.5", + "source-map-loader": "^3.0.0", + "style-loader": "^3.3.1", + "tailwindcss": "^3.0.2", + "terser-webpack-plugin": "^5.2.5", + "webpack": "^5.64.4", + "webpack-dev-server": "^4.6.0", + "webpack-manifest-plugin": "^4.0.2", + "workbox-webpack-plugin": "^6.4.1" + }, + "bin": { + "react-scripts": "bin/react-scripts.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + }, + "peerDependencies": { + "react": ">= 16", + "typescript": "^3.2.1 || ^4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/react-scripts/node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "engines": { + "node": ">=10" + } + }, + "node_modules/react-scripts/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-xarrows": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-xarrows/-/react-xarrows-2.0.2.tgz", + "integrity": "sha512-tDlAqaxHNmy0vegW/6NdhoWyXJq1LANX/WUAlHyzoHe9BwFVnJPPDghmDjYeVr7XWFmBrVTUrHsrW7GKYI6HtQ==", + "dependencies": { + "@types/prop-types": "^15.7.3", + "lodash": "^4.17.21", + "prop-types": "^15.7.2" + }, + "funding": { + "type": "individual", + "url": "https://www.paypal.com/donate?hosted_button_id=CRQ343F9VTRS8" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recursive-readdir": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "dependencies": { + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "node_modules/regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", + "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/regexpu-core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz", + "integrity": "sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsgen": "^0.7.1", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", + "integrity": "sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==" + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/reserved-words": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/reserved-words/-/reserved-words-0.1.2.tgz", + "integrity": "sha512-0S5SrIUJ9LfpbVl4Yzij6VipUdafHrOTzvmfazSw/jeZrZtQK303OPZW+obtkaw7jQlTQppy0UvZWm9872PbRw==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz", + "integrity": "sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==", + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^7.0.35", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=8.9" + }, + "peerDependencies": { + "rework": "1.0.1", + "rework-visit": "1.0.0" + }, + "peerDependenciesMeta": { + "rework": { + "optional": true + }, + "rework-visit": { + "optional": true + } + } + }, + "node_modules/resolve-url-loader/node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/resolve-url-loader/node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/resolve-url-loader/node_modules/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" + }, + "node_modules/resolve-url-loader/node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/resolve.exports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", + "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "jest-worker": "^26.2.1", + "serialize-javascript": "^4.0.0", + "terser": "^5.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0" + } + }, + "node_modules/rollup-plugin-terser/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup-plugin-terser/node_modules/jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/rollup-plugin-terser/node_modules/serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/rollup-plugin-terser/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sanitize.css": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", + "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==" + }, + "node_modules/sass": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.1.tgz", + "integrity": "sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ==", + "devOptional": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/sass-loader": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", + "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", + "dependencies": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==" + }, + "node_modules/selfsigned": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", + "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "dependencies": { + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz", + "integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.2.tgz", + "integrity": "sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg==", + "dependencies": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead" + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/spdy-transport/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/spdy-transport/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/spdy/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/spdy/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-natural-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", + "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", + "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.3", + "side-channel": "^1.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-loader": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", + "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/stylehacks": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", + "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/stylus": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz", + "integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.0.1", + "debug": "^4.3.2", + "glob": "^7.1.6", + "sax": "~1.2.4", + "source-map": "^0.7.3" + }, + "bin": { + "stylus": "bin/stylus" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://opencollective.com/stylus" + } + }, + "node_modules/stylus/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/stylus/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/stylus/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" + }, + "node_modules/svgo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", + "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", + "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", + "dependencies": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.37", + "csso": "^4.0.2", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/svgo/node_modules/css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "node_modules/svgo/node_modules/css-what": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", + "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/svgo/node_modules/dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dependencies": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "node_modules/svgo/node_modules/domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/svgo/node_modules/domutils/node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "node_modules/svgo/node_modules/nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dependencies": { + "boolbase": "~1.0.0" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, + "node_modules/tailwindcss": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz", + "integrity": "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==", + "dependencies": { + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "color-name": "^1.1.4", + "detective": "^5.2.1", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.12", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "lilconfig": "^2.0.6", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.18", + "postcss-import": "^14.1.0", + "postcss-js": "^4.0.0", + "postcss-load-config": "^3.1.4", + "postcss-nested": "6.0.0", + "postcss-selector-parser": "^6.0.10", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.22.1" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/tailwindcss/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tailwindcss/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dependencies": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.1.tgz", + "integrity": "sha512-xvQfyfA1ayT0qdK47zskQgRZeWLoOQ8JQ6mIgRGVNwZKdQMU+5FkCBjmv4QjcrTzyZquRw2FVtlJSRUmMKQslw==", + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", + "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.14", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0", + "terser": "^5.14.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + }, + "node_modules/throat": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", + "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==" + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", + "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tryer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", + "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" + }, + "node_modules/tsconfig-paths": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.1.1.tgz", + "integrity": "sha512-VgPrtLKpRgEAJsMj5Q/I/mXouC6A/7eJ/X4Nuk6o0cRPwBtznYxTCU4FodbexbzH9somBPEXYi0ZkUViUpJ21Q==", + "dev": true, + "dependencies": { + "json5": "^2.2.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", + "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/typescript-plugin-css-modules": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/typescript-plugin-css-modules/-/typescript-plugin-css-modules-4.1.1.tgz", + "integrity": "sha512-kpVxGkY/go9eV5TP1YUDJ6SqwBx2OIuVStMCxKyg9PhJVFXjLYR7AuItVLwoz0NCdiemH91WhtgAjb96jI34DA==", + "dev": true, + "dependencies": { + "dotenv": "^16.0.3", + "icss-utils": "^5.1.0", + "less": "^4.1.3", + "lodash.camelcase": "^4.3.0", + "postcss": "^8.4.19", + "postcss-filter-plugins": "^3.0.1", + "postcss-icss-keyframes": "^0.2.1", + "postcss-icss-selectors": "^2.0.3", + "postcss-load-config": "^3.1.4", + "reserved-words": "^0.1.2", + "sass": "^1.56.1", + "source-map-js": "^1.0.2", + "stylus": "^0.59.0", + "tsconfig-paths": "^4.1.1" + }, + "peerDependencies": { + "typescript": ">=3.9.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unload": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", + "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", + "dependencies": { + "@babel/runtime": "^7.6.2", + "detect-node": "^2.0.4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==" + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/util.promisify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", + "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.2", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", + "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", + "dependencies": { + "browser-process-hrtime": "^1.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "dependencies": { + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "engines": { + "node": ">=10.4" + } + }, + "node_modules/webpack": { + "version": "5.75.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", + "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==", + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", + "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/webpack-dev-middleware/node_modules/schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz", + "integrity": "sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==", + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.1", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.4.2" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-server/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-manifest-plugin": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz", + "integrity": "sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow==", + "dependencies": { + "tapable": "^2.0.0", + "webpack-sources": "^2.2.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "peerDependencies": { + "webpack": "^4.44.2 || ^5.47.0" + } + }, + "node_modules/webpack-manifest-plugin/node_modules/webpack-sources": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", + "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", + "dependencies": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==" + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dependencies": { + "iconv-lite": "0.4.24" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + }, + "node_modules/whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + }, + "node_modules/whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "dependencies": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workbox-background-sync": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz", + "integrity": "sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g==", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.5.4.tgz", + "integrity": "sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw==", + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-build": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.5.4.tgz", + "integrity": "sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA==", + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.11.1", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^5.2.0", + "@rollup/plugin-node-resolve": "^11.2.1", + "@rollup/plugin-replace": "^2.4.1", + "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^7.1.6", + "lodash": "^4.17.20", + "pretty-bytes": "^5.3.0", + "rollup": "^2.43.1", + "rollup-plugin-terser": "^7.0.0", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "6.5.4", + "workbox-broadcast-update": "6.5.4", + "workbox-cacheable-response": "6.5.4", + "workbox-core": "6.5.4", + "workbox-expiration": "6.5.4", + "workbox-google-analytics": "6.5.4", + "workbox-navigation-preload": "6.5.4", + "workbox-precaching": "6.5.4", + "workbox-range-requests": "6.5.4", + "workbox-recipes": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4", + "workbox-streams": "6.5.4", + "workbox-sw": "6.5.4", + "workbox-window": "6.5.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", + "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "dependencies": { + "json-schema": "^0.4.0", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/workbox-build/node_modules/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/workbox-build/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/workbox-build/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/workbox-build/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workbox-build/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/workbox-build/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + }, + "node_modules/workbox-build/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.5.4.tgz", + "integrity": "sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug==", + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-core": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.5.4.tgz", + "integrity": "sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q==" + }, + "node_modules/workbox-expiration": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.5.4.tgz", + "integrity": "sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ==", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-google-analytics": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.5.4.tgz", + "integrity": "sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg==", + "dependencies": { + "workbox-background-sync": "6.5.4", + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.5.4.tgz", + "integrity": "sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng==", + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-precaching": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.5.4.tgz", + "integrity": "sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg==", + "dependencies": { + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "node_modules/workbox-range-requests": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.5.4.tgz", + "integrity": "sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg==", + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-recipes": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.5.4.tgz", + "integrity": "sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA==", + "dependencies": { + "workbox-cacheable-response": "6.5.4", + "workbox-core": "6.5.4", + "workbox-expiration": "6.5.4", + "workbox-precaching": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "node_modules/workbox-routing": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.5.4.tgz", + "integrity": "sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg==", + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-strategies": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.5.4.tgz", + "integrity": "sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw==", + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-streams": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.5.4.tgz", + "integrity": "sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg==", + "dependencies": { + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4" + } + }, + "node_modules/workbox-sw": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.5.4.tgz", + "integrity": "sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA==" + }, + "node_modules/workbox-webpack-plugin": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.5.4.tgz", + "integrity": "sha512-LmWm/zoaahe0EGmMTrSLUi+BjyR3cdGEfU3fS6PN1zKFYbqAKuQ+Oy/27e4VSXsyIwAw8+QDfk1XHNGtZu9nQg==", + "dependencies": { + "fast-json-stable-stringify": "^2.1.0", + "pretty-bytes": "^5.4.1", + "upath": "^1.2.0", + "webpack-sources": "^1.4.3", + "workbox-build": "6.5.4" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "webpack": "^4.4.0 || ^5.9.0" + } + }, + "node_modules/workbox-webpack-plugin/node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "node_modules/workbox-window": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.5.4.tgz", + "integrity": "sha512-HnLZJDwYBE+hpG25AQBO8RUWBJRaCsI9ksQJEp3aCOFCaG5kqaToAYXFRAHxzRluM2cQbGzdQF5rjKPWPA1fug==", + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "6.5.4" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@adobe/css-tools": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.0.1.tgz", + "integrity": "sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==", + "dev": true + }, + "@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "requires": { + "@babel/highlight": "^7.18.6" + } + }, + "@babel/compat-data": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.5.tgz", + "integrity": "sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g==" + }, + "@babel/core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.5.tgz", + "integrity": "sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==", + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-module-transforms": "^7.20.2", + "@babel/helpers": "^7.20.5", + "@babel/parser": "^7.20.5", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "@babel/eslint-parser": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.19.1.tgz", + "integrity": "sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ==", + "requires": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "@babel/generator": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz", + "integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==", + "requires": { + "@babel/types": "^7.20.5", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", + "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "requires": { + "@babel/helper-explode-assignable-expression": "^7.18.6", + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", + "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", + "requires": { + "@babel/compat-data": "^7.20.0", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.5.tgz", + "integrity": "sha512-3RCdA/EmEaikrhayahwToF0fpweU/8o2p8vhc1c/1kftHOdTKuC65kik/TLc+qfbS8JKw4qqJbne4ovICDhmww==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-member-expression-to-functions": "^7.18.9", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.19.1", + "@babel/helper-split-export-declaration": "^7.18.6" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.20.5.tgz", + "integrity": "sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "regexpu-core": "^5.2.1" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "requires": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==" + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", + "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-function-name": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", + "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "requires": { + "@babel/template": "^7.18.10", + "@babel/types": "^7.19.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz", + "integrity": "sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==", + "requires": { + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-module-transforms": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz", + "integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==", + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.20.2", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.1", + "@babel/types": "^7.20.2" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==" + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", + "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-wrap-function": "^7.18.9", + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-replace-supers": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz", + "integrity": "sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==", + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.18.9", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/traverse": "^7.19.1", + "@babel/types": "^7.19.0" + } + }, + "@babel/helper-simple-access": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "requires": { + "@babel/types": "^7.20.2" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", + "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "requires": { + "@babel/types": "^7.20.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" + }, + "@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==" + }, + "@babel/helper-wrap-function": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", + "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", + "requires": { + "@babel/helper-function-name": "^7.19.0", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5" + } + }, + "@babel/helpers": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.6.tgz", + "integrity": "sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==", + "requires": { + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5" + } + }, + "@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz", + "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==" + }, + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", + "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz", + "integrity": "sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", + "@babel/plugin-proposal-optional-chaining": "^7.18.9" + } + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.1.tgz", + "integrity": "sha512-Gh5rchzSwE4kC+o/6T8waD0WHEQIsDmjltY8WnWRXHUdH8axZhuH86Ov9M72YhJfDrZseQwuuWaaIT/TmePp3g==", + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-class-static-block": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz", + "integrity": "sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-proposal-decorators": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.20.5.tgz", + "integrity": "sha512-Lac7PpRJXcC3s9cKsBfl+uc+DYXU5FD06BrTFunQO6QIQT+DwyzDPURAowI3bcvD1dZF/ank1Z5rstUJn3Hn4Q==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-replace-supers": "^7.19.1", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/plugin-syntax-decorators": "^7.19.0" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz", + "integrity": "sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.2.tgz", + "integrity": "sha512-Ks6uej9WFK+fvIMesSqbAto5dD8Dz4VuuFvGJFKgIGSkJuRGcrwGECPA1fDgQK3/DbExBJpEkTeYeB8geIFCSQ==", + "requires": { + "@babel/compat-data": "^7.20.1", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.1" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz", + "integrity": "sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.20.5.tgz", + "integrity": "sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-decorators": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.19.0.tgz", + "integrity": "sha512-xaBZUEDntt4faL1yN8oIFlhfXeQAWJW7CLKYsHTUqriCUbj8xOra8bfxxKGi/UwExPFBuPdH4XfHc9rGQhrVkQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.19.0" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-flow": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz", + "integrity": "sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-import-assertions": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", + "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.19.0" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", + "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz", + "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.19.0" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz", + "integrity": "sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz", + "integrity": "sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==", + "requires": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-remap-async-to-generator": "^7.18.6" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", + "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.5.tgz", + "integrity": "sha512-WvpEIW9Cbj9ApF3yJCjIEEf1EiNJLtXagOrL5LNWEZOo3jv8pmPoYTSNJQvqej8OavVlgOoOPw6/htGZro6IkA==", + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.2.tgz", + "integrity": "sha512-9rbPp0lCVVoagvtEyQKSo5L8oo0nQS/iif+lwlAz29MccX2642vWDlSZK+2T2buxbopotId2ld7zZAzRfz9j1g==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-replace-supers": "^7.19.1", + "@babel/helper-split-export-declaration": "^7.18.6", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz", + "integrity": "sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.2.tgz", + "integrity": "sha512-mENM+ZHrvEgxLTBXUiQ621rRXZes3KWUv6NdQlrnr1TkWVw+hUjQBZuP2X32qKlrlG2BzgR95gkuCRSkJl8vIw==", + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", + "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", + "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", + "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-flow-strip-types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.19.0.tgz", + "integrity": "sha512-sgeMlNaQVbCSpgLSKP4ZZKfsJVnFnNQlUSk6gPYzR/q7tzCgQF2t8RBKAP6cKJeZdveei7Q7Jm527xepI8lNLg==", + "requires": { + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/plugin-syntax-flow": "^7.18.6" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.18.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", + "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", + "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "requires": { + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", + "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", + "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.19.6.tgz", + "integrity": "sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==", + "requires": { + "@babel/helper-module-transforms": "^7.19.6", + "@babel/helper-plugin-utils": "^7.19.0" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.19.6.tgz", + "integrity": "sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==", + "requires": { + "@babel/helper-module-transforms": "^7.19.6", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-simple-access": "^7.19.4" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.6.tgz", + "integrity": "sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==", + "requires": { + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-module-transforms": "^7.19.6", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-validator-identifier": "^7.19.1" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", + "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "requires": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", + "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", + "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", + "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.5.tgz", + "integrity": "sha512-h7plkOmcndIUWXZFLgpbrh2+fXAi47zcUX7IrOQuZdLD0I0KvjJ6cvo3BEcAOsDOcZhVKGJqv07mkSqK0y2isQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", + "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-react-constant-elements": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.20.2.tgz", + "integrity": "sha512-KS/G8YI8uwMGKErLFOHS/ekhqdHhpEloxs43NecQHVgo2QuQSyJhGIY1fL8UGl9wy5ItVwwoUL4YxVqsplGq2g==", + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-react-display-name": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz", + "integrity": "sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.19.0.tgz", + "integrity": "sha512-UVEvX3tXie3Szm3emi1+G63jyw1w5IcMY0FSKM+CRnKRI5Mr1YbCNgsSTwoTwKphQEG9P+QqmuRFneJPZuHNhg==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/plugin-syntax-jsx": "^7.18.6", + "@babel/types": "^7.19.0" + } + }, + "@babel/plugin-transform-react-jsx-development": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz", + "integrity": "sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==", + "requires": { + "@babel/plugin-transform-react-jsx": "^7.18.6" + } + }, + "@babel/plugin-transform-react-pure-annotations": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz", + "integrity": "sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz", + "integrity": "sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "regenerator-transform": "^0.15.1" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", + "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-runtime": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz", + "integrity": "sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==", + "requires": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.19.0", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", + "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz", + "integrity": "sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==", + "requires": { + "@babel/helper-plugin-utils": "^7.19.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", + "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", + "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", + "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-typescript": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.20.2.tgz", + "integrity": "sha512-jvS+ngBfrnTUBfOQq8NfGnSbF9BrqlR6hjJ2yVxMkmO5nL/cdifNbI30EfjRlN4g5wYWNnMPyj5Sa6R1pbLeag==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.20.2", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-typescript": "^7.20.0" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", + "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", + "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/preset-env": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", + "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", + "requires": { + "@babel/compat-data": "^7.20.1", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.20.1", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.18.6", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.20.2", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.18.6", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.18.6", + "@babel/plugin-transform-async-to-generator": "^7.18.6", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.20.2", + "@babel/plugin-transform-classes": "^7.20.2", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.20.2", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.19.6", + "@babel/plugin-transform-modules-commonjs": "^7.19.6", + "@babel/plugin-transform-modules-systemjs": "^7.19.6", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.20.1", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.18.6", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.19.0", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.20.2", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "core-js-compat": "^3.25.1", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/preset-react": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.18.6.tgz", + "integrity": "sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-transform-react-display-name": "^7.18.6", + "@babel/plugin-transform-react-jsx": "^7.18.6", + "@babel/plugin-transform-react-jsx-development": "^7.18.6", + "@babel/plugin-transform-react-pure-annotations": "^7.18.6" + } + }, + "@babel/preset-typescript": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz", + "integrity": "sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-transform-typescript": "^7.18.6" + } + }, + "@babel/runtime": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz", + "integrity": "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "@babel/runtime-corejs3": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.20.6.tgz", + "integrity": "sha512-tqeujPiuEfcH067mx+7otTQWROVMKHXEaOQcAeNV5dDdbPWvPcFA8/W9LXw2NfjNmOetqLl03dfnG2WALPlsRQ==", + "requires": { + "core-js-pure": "^3.25.1", + "regenerator-runtime": "^0.13.11" + } + }, + "@babel/template": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", + "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.18.10", + "@babel/types": "^7.18.10" + } + }, + "@babel/traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz", + "integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==", + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.5", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.20.5", + "@babel/types": "^7.20.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "@babel/types": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz", + "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==", + "requires": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" + }, + "@csstools/normalize.css": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.0.0.tgz", + "integrity": "sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg==" + }, + "@csstools/postcss-cascade-layers": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz", + "integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==", + "requires": { + "@csstools/selector-specificity": "^2.0.2", + "postcss-selector-parser": "^6.0.10" + } + }, + "@csstools/postcss-color-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", + "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "@csstools/postcss-font-format-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", + "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "@csstools/postcss-hwb-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", + "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "@csstools/postcss-ic-unit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", + "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "@csstools/postcss-is-pseudo-class": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", + "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", + "requires": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + } + }, + "@csstools/postcss-nested-calc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz", + "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "@csstools/postcss-normalize-display-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", + "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "@csstools/postcss-oklab-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", + "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "@csstools/postcss-progressive-custom-properties": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", + "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "@csstools/postcss-stepped-value-functions": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", + "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "@csstools/postcss-text-decoration-shorthand": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz", + "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "@csstools/postcss-trigonometric-functions": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz", + "integrity": "sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "@csstools/postcss-unset-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", + "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", + "requires": {} + }, + "@csstools/selector-specificity": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz", + "integrity": "sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==", + "requires": {} + }, + "@eslint/eslintrc": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", + "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==", + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "globals": { + "version": "13.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", + "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", + "requires": { + "type-fest": "^0.20.2" + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" + } + } + }, + "@humanwhocodes/config-array": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", + "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==", + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==" + }, + "@jest/console": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", + "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "slash": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/core": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", + "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==", + "requires": { + "@jest/console": "^27.5.1", + "@jest/reporters": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^27.5.1", + "jest-config": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-resolve-dependencies": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "jest-watcher": "^27.5.1", + "micromatch": "^4.0.4", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/environment": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", + "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", + "requires": { + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1" + } + }, + "@jest/fake-timers": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", + "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", + "requires": { + "@jest/types": "^27.5.1", + "@sinonjs/fake-timers": "^8.0.1", + "@types/node": "*", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" + } + }, + "@jest/globals": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", + "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", + "requires": { + "@jest/environment": "^27.5.1", + "@jest/types": "^27.5.1", + "expect": "^27.5.1" + } + }, + "@jest/reporters": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", + "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==", + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-haste-map": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^8.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/schemas": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", + "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", + "requires": { + "@sinclair/typebox": "^0.24.1" + } + }, + "@jest/source-map": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", + "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9", + "source-map": "^0.6.0" + } + }, + "@jest/test-result": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", + "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", + "requires": { + "@jest/console": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz", + "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==", + "requires": { + "@jest/test-result": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-runtime": "^27.5.1" + } + }, + "@jest/transform": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", + "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^27.5.1", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-util": "^27.5.1", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" + }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + } + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" + }, + "@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "requires": { + "eslint-scope": "5.1.1" + }, + "dependencies": { + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + } + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz", + "integrity": "sha512-j0Ya0hCFZPd4x40qLzbhGsh9TMtdb+CJQiso+WxLOPNasohq9cc5SNUcwsZaRH6++Xh91Xkm/xHCkuIiIu0LUA==", + "requires": { + "ansi-html-community": "^0.0.8", + "common-path-prefix": "^3.0.0", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "find-up": "^5.0.0", + "html-entities": "^2.1.0", + "loader-utils": "^2.0.4", + "schema-utils": "^3.0.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" + }, + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" + }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==" + } + } + }, + "@remix-run/router": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.4.tgz", + "integrity": "sha512-gTL8H5USTAKOyVA4xczzDJnC3HMssdFa3tRlwBicXynx9XfiXwneHnYQogwSKpdCkjXISrEKSTtX62rLpNEVQg==" + }, + "@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "requires": { + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" + } + }, + "@rollup/plugin-node-resolve": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", + "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", + "requires": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + } + }, + "@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "requires": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "dependencies": { + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" + } + } + }, + "@rushstack/eslint-patch": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", + "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==" + }, + "@sinclair/typebox": { + "version": "0.24.51", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", + "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==" + }, + "@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", + "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@surma/rollup-plugin-off-main-thread": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "requires": { + "ejs": "^3.1.6", + "json5": "^2.2.0", + "magic-string": "^0.25.0", + "string.prototype.matchall": "^4.0.6" + }, + "dependencies": { + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" + } + } + }, + "@svgr/babel-plugin-add-jsx-attribute": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", + "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==" + }, + "@svgr/babel-plugin-remove-jsx-attribute": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", + "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==" + }, + "@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", + "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==" + }, + "@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", + "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==" + }, + "@svgr/babel-plugin-svg-dynamic-title": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", + "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==" + }, + "@svgr/babel-plugin-svg-em-dimensions": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", + "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==" + }, + "@svgr/babel-plugin-transform-react-native-svg": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", + "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==" + }, + "@svgr/babel-plugin-transform-svg-component": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", + "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==" + }, + "@svgr/babel-preset": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", + "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", + "requires": { + "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", + "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "^5.0.1", + "@svgr/babel-plugin-replace-jsx-attribute-value": "^5.0.1", + "@svgr/babel-plugin-svg-dynamic-title": "^5.4.0", + "@svgr/babel-plugin-svg-em-dimensions": "^5.4.0", + "@svgr/babel-plugin-transform-react-native-svg": "^5.4.0", + "@svgr/babel-plugin-transform-svg-component": "^5.5.0" + } + }, + "@svgr/core": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", + "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", + "requires": { + "@svgr/plugin-jsx": "^5.5.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^7.0.0" + } + }, + "@svgr/hast-util-to-babel-ast": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", + "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", + "requires": { + "@babel/types": "^7.12.6" + } + }, + "@svgr/plugin-jsx": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", + "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", + "requires": { + "@babel/core": "^7.12.3", + "@svgr/babel-preset": "^5.5.0", + "@svgr/hast-util-to-babel-ast": "^5.5.0", + "svg-parser": "^2.0.2" + } + }, + "@svgr/plugin-svgo": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", + "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", + "requires": { + "cosmiconfig": "^7.0.0", + "deepmerge": "^4.2.2", + "svgo": "^1.2.2" + } + }, + "@svgr/webpack": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz", + "integrity": "sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==", + "requires": { + "@babel/core": "^7.12.3", + "@babel/plugin-transform-react-constant-elements": "^7.12.1", + "@babel/preset-env": "^7.12.1", + "@babel/preset-react": "^7.12.5", + "@svgr/core": "^5.5.0", + "@svgr/plugin-jsx": "^5.5.0", + "@svgr/plugin-svgo": "^5.5.0", + "loader-utils": "^2.0.0" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" + }, + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" + }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + } + } + }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" + }, + "@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==" + }, + "@types/babel__core": { + "version": "7.1.20", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.20.tgz", + "integrity": "sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ==", + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz", + "integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==", + "requires": { + "@babel/types": "^7.3.0" + } + }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/bonjour": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", + "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "requires": { + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/connect-history-api-fallback": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", + "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", + "requires": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "@types/eslint": { + "version": "8.4.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz", + "integrity": "sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==", + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", + "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==" + }, + "@types/express": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", + "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.31", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", + "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/graceful-fs": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", + "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", + "requires": { + "@types/node": "*" + } + }, + "@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==" + }, + "@types/http-proxy": { + "version": "1.17.9", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", + "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==" + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" + }, + "@types/mime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" + }, + "@types/node": { + "version": "16.18.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.3.tgz", + "integrity": "sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg==" + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + }, + "@types/prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==" + }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + }, + "@types/q": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", + "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==" + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "@types/react": { + "version": "18.0.25", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.25.tgz", + "integrity": "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==", + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.0.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.9.tgz", + "integrity": "sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg==", + "requires": { + "@types/react": "*" + } + }, + "@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "requires": { + "@types/node": "*" + } + }, + "@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + }, + "@types/semver": { + "version": "7.3.13", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==" + }, + "@types/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "requires": { + "@types/express": "*" + } + }, + "@types/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "requires": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "@types/sockjs": { + "version": "0.3.33", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", + "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "requires": { + "@types/node": "*" + } + }, + "@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" + }, + "@types/trusted-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", + "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==" + }, + "@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "requires": { + "@types/node": "*" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.45.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.45.1.tgz", + "integrity": "sha512-cOizjPlKEh0bXdFrBLTrI/J6B/QMlhwE9auOov53tgB+qMukH6/h8YAK/qw+QJGct/PTbdh2lytGyipxCcEtAw==", + "requires": { + "@typescript-eslint/scope-manager": "5.45.1", + "@typescript-eslint/type-utils": "5.45.1", + "@typescript-eslint/utils": "5.45.1", + "debug": "^4.3.4", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "5.45.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.45.1.tgz", + "integrity": "sha512-WlXwY9dbmc0Lzu6xQOZ3yN8u/ws/1R8zPC16O217LMZJCbV2hJezqkWMUB+jMwguOJW+cukCDe92vcwwf8zwjQ==", + "requires": { + "@typescript-eslint/utils": "5.45.1" + } + }, + "@typescript-eslint/parser": { + "version": "5.45.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.45.1.tgz", + "integrity": "sha512-JQ3Ep8bEOXu16q0ztsatp/iQfDCtvap7sp/DKo7DWltUquj5AfCOpX2zSzJ8YkAVnrQNqQ5R62PBz2UtrfmCkA==", + "requires": { + "@typescript-eslint/scope-manager": "5.45.1", + "@typescript-eslint/types": "5.45.1", + "@typescript-eslint/typescript-estree": "5.45.1", + "debug": "^4.3.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.45.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.45.1.tgz", + "integrity": "sha512-D6fCileR6Iai7E35Eb4Kp+k0iW7F1wxXYrOhX/3dywsOJpJAQ20Fwgcf+P/TDtvQ7zcsWsrJaglaQWDhOMsspQ==", + "requires": { + "@typescript-eslint/types": "5.45.1", + "@typescript-eslint/visitor-keys": "5.45.1" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.45.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.45.1.tgz", + "integrity": "sha512-aosxFa+0CoYgYEl3aptLe1svP910DJq68nwEJzyQcrtRhC4BN0tJAvZGAe+D0tzjJmFXe+h4leSsiZhwBa2vrA==", + "requires": { + "@typescript-eslint/typescript-estree": "5.45.1", + "@typescript-eslint/utils": "5.45.1", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "@typescript-eslint/types": { + "version": "5.45.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.45.1.tgz", + "integrity": "sha512-HEW3U0E5dLjUT+nk7b4lLbOherS1U4ap+b9pfu2oGsW3oPu7genRaY9dDv3nMczC1rbnRY2W/D7SN05wYoGImg==" + }, + "@typescript-eslint/typescript-estree": { + "version": "5.45.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.1.tgz", + "integrity": "sha512-76NZpmpCzWVrrb0XmYEpbwOz/FENBi+5W7ipVXAsG3OoFrQKJMiaqsBMbvGRyLtPotGqUfcY7Ur8j0dksDJDng==", + "requires": { + "@typescript-eslint/types": "5.45.1", + "@typescript-eslint/visitor-keys": "5.45.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/utils": { + "version": "5.45.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.45.1.tgz", + "integrity": "sha512-rlbC5VZz68+yjAzQBc4I7KDYVzWG2X/OrqoZrMahYq3u8FFtmQYc+9rovo/7wlJH5kugJ+jQXV5pJMnofGmPRw==", + "requires": { + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.45.1", + "@typescript-eslint/types": "5.45.1", + "@typescript-eslint/typescript-estree": "5.45.1", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0", + "semver": "^7.3.7" + }, + "dependencies": { + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.45.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.1.tgz", + "integrity": "sha512-cy9ln+6rmthYWjH9fmx+5FU/JDpjQb586++x2FZlveq7GdGuLLW9a2Jcst2TGekH82bXpfmRNSwP9tyEs6RjvQ==", + "requires": { + "@typescript-eslint/types": "5.45.1", + "eslint-visitor-keys": "^3.3.0" + } + }, + "@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "requires": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==" + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==" + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==" + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==" + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==" + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + }, + "abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==" + }, + "acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "requires": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + } + } + }, + "acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "requires": {} + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "requires": {} + }, + "acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "requires": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + } + } + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" + }, + "address": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/address/-/address-1.2.1.tgz", + "integrity": "sha512-B+6bi5D34+fDYENiH5qOlA0cV2rAGKuWZ9LeyUUehbXy8e0VS9e498yO0Jeeh+iM+6KbfudHTFjXw2MmJD4QRA==" + }, + "adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "requires": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" + }, + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" + }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + } + } + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "requires": {} + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==" + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "requires": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + } + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" + }, + "array-includes": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" + }, + "array.prototype.flat": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flatmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", + "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.reduce": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz", + "integrity": "sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + } + }, + "array.prototype.tosorted": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", + "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.1.3" + } + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==" + }, + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" + }, + "autoprefixer": { + "version": "10.4.13", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", + "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==", + "requires": { + "browserslist": "^4.21.4", + "caniuse-lite": "^1.0.30001426", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "axe-core": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.5.2.tgz", + "integrity": "sha512-u2MVsXfew5HBvjsczCv+xlwdNnB1oQR9HlAcsejZttNjKKSkeDNVwB1vMThIUIFI9GoT57Vtk8iQLwqOfAkboA==" + }, + "axobject-query": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", + "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==" + }, + "babel-jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", + "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", + "requires": { + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "babel-loader": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", + "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", + "requires": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" + }, + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" + }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + } + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", + "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "requires": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + } + }, + "babel-plugin-named-asset-import": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz", + "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==", + "requires": {} + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "requires": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.3", + "semver": "^6.1.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", + "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.3", + "core-js-compat": "^3.25.1" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.3" + } + }, + "babel-plugin-transform-react-remove-prop-types": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", + "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==" + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", + "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", + "requires": { + "babel-plugin-jest-hoist": "^27.5.1", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "babel-preset-react-app": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz", + "integrity": "sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg==", + "requires": { + "@babel/core": "^7.16.0", + "@babel/plugin-proposal-class-properties": "^7.16.0", + "@babel/plugin-proposal-decorators": "^7.16.4", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", + "@babel/plugin-proposal-numeric-separator": "^7.16.0", + "@babel/plugin-proposal-optional-chaining": "^7.16.0", + "@babel/plugin-proposal-private-methods": "^7.16.0", + "@babel/plugin-transform-flow-strip-types": "^7.16.0", + "@babel/plugin-transform-react-display-name": "^7.16.0", + "@babel/plugin-transform-runtime": "^7.16.4", + "@babel/preset-env": "^7.16.4", + "@babel/preset-react": "^7.16.0", + "@babel/preset-typescript": "^7.16.0", + "@babel/runtime": "^7.16.3", + "babel-plugin-macros": "^3.1.0", + "babel-plugin-transform-react-remove-prop-types": "^0.4.24" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==" + }, + "bfj": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz", + "integrity": "sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw==", + "requires": { + "bluebird": "^3.5.5", + "check-types": "^11.1.1", + "hoopy": "^0.1.4", + "tryer": "^1.0.1" + } + }, + "big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==" + }, + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "bonjour-service": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.0.14.tgz", + "integrity": "sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ==", + "requires": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "broadcast-channel": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", + "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", + "requires": { + "@babel/runtime": "^7.7.2", + "detect-node": "^2.1.0", + "js-sha3": "0.8.0", + "microseconds": "0.2.0", + "nano-time": "1.0.0", + "oblivious-set": "1.0.0", + "rimraf": "3.0.2", + "unload": "2.2.0" + } + }, + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" + }, + "browserslist": { + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "requires": { + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==" + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" + }, + "camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" + }, + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "requires": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001436", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001436.tgz", + "integrity": "sha512-ZmWkKsnC2ifEPoWUvSAIGyOYwT+keAaaWPHiQ9DfMqS1t6tfuyFYoWR78TeZtznkEQ64+vGXH9cZrElwR2Mrxg==" + }, + "case-sensitive-paths-webpack-plugin": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", + "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==" + }, + "check-types": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.2.tgz", + "integrity": "sha512-HBiYvXvn9Z70Z88XKjz3AEKd4HJhBXsa3j7xFnITAzoS8+q6eIGi8qDB8FKPBAjtuxjI/zFpwuiCb8oDtKOYrA==" + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" + }, + "ci-info": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.0.tgz", + "integrity": "sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==" + }, + "cjs-module-lexer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", + "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==" + }, + "clean-css": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.1.tgz", + "integrity": "sha512-lCr8OHhiWCTw4v8POJovCoh4T7I9U11yVsPjMWWnnMmp9ZowCxyad1Pathle/9HjaDp+fdQKjO9fQydE6RHTZg==", + "requires": { + "source-map": "~0.6.0" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==" + }, + "coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "requires": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + } + }, + "collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==" + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" + }, + "colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" + }, + "common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==" + }, + "common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==" + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==" + }, + "connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==" + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "requires": { + "is-what": "^3.14.1" + } + }, + "core-js": { + "version": "3.26.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.1.tgz", + "integrity": "sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA==" + }, + "core-js-compat": { + "version": "3.26.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.1.tgz", + "integrity": "sha512-622/KzTudvXCDLRw70iHW4KKs1aGpcRcowGWyYJr2DEBfRrd6hNJybxSWJFuZYD4ma86xhrwDDHxmDaIq4EA8A==", + "requires": { + "browserslist": "^4.21.4" + } + }, + "core-js-pure": { + "version": "3.26.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.26.1.tgz", + "integrity": "sha512-VVXcDpp/xJ21KdULRq/lXdLzQAtX7+37LzpyfFM973il0tWSsDEoyzG38G14AjTpK9VTfiNM9jnFauq/CpaWGQ==" + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" + }, + "css-blank-pseudo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", + "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "css-declaration-sorter": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz", + "integrity": "sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==", + "requires": {} + }, + "css-has-pseudo": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", + "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "css-loader": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.2.tgz", + "integrity": "sha512-oqGbbVcBJkm8QwmnNzrFrWTnudnRZC+1eXikLJl0n4ljcfotgRifpg2a1lKy8jTrc4/d9A/ap1GFq1jDKG7J+Q==", + "requires": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.18", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "css-minimizer-webpack-plugin": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz", + "integrity": "sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==", + "requires": { + "cssnano": "^5.0.6", + "jest-worker": "^27.0.2", + "postcss": "^8.3.5", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + } + } + }, + "css-prefers-color-scheme": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", + "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", + "requires": {} + }, + "css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + } + }, + "css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" + }, + "css-selector-tokenizer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz", + "integrity": "sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, + "css-tree": { + "version": "1.0.0-alpha.37", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", + "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "requires": { + "mdn-data": "2.0.4", + "source-map": "^0.6.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" + }, + "cssdb": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.2.0.tgz", + "integrity": "sha512-JYlIsE7eKHSi0UNuCyo96YuIDFqvhGgHw4Ck6lsN+DP0Tp8M64UTDT2trGbkMDqnCoEjks7CkS0XcjU0rkvBdg==" + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + }, + "cssnano": { + "version": "5.1.14", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.14.tgz", + "integrity": "sha512-Oou7ihiTocbKqi0J1bB+TRJIQX5RMR3JghA8hcWSw9mjBLQ5Y3RWqEDoYG3sRNlAbCIXpqMoZGbq5KDR3vdzgw==", + "requires": { + "cssnano-preset-default": "^5.2.13", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + } + }, + "cssnano-preset-default": { + "version": "5.2.13", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.13.tgz", + "integrity": "sha512-PX7sQ4Pb+UtOWuz8A1d+Rbi+WimBIxJTRyBdgGp1J75VU0r/HFQeLnMYgHiCAp6AR4rqrc7Y4R+1Rjk3KJz6DQ==", + "requires": { + "css-declaration-sorter": "^6.3.1", + "cssnano-utils": "^3.1.0", + "postcss-calc": "^8.2.3", + "postcss-colormin": "^5.3.0", + "postcss-convert-values": "^5.1.3", + "postcss-discard-comments": "^5.1.2", + "postcss-discard-duplicates": "^5.1.0", + "postcss-discard-empty": "^5.1.1", + "postcss-discard-overridden": "^5.1.0", + "postcss-merge-longhand": "^5.1.7", + "postcss-merge-rules": "^5.1.3", + "postcss-minify-font-values": "^5.1.0", + "postcss-minify-gradients": "^5.1.1", + "postcss-minify-params": "^5.1.4", + "postcss-minify-selectors": "^5.2.1", + "postcss-normalize-charset": "^5.1.0", + "postcss-normalize-display-values": "^5.1.0", + "postcss-normalize-positions": "^5.1.1", + "postcss-normalize-repeat-style": "^5.1.1", + "postcss-normalize-string": "^5.1.0", + "postcss-normalize-timing-functions": "^5.1.0", + "postcss-normalize-unicode": "^5.1.1", + "postcss-normalize-url": "^5.1.0", + "postcss-normalize-whitespace": "^5.1.1", + "postcss-ordered-values": "^5.1.3", + "postcss-reduce-initial": "^5.1.1", + "postcss-reduce-transforms": "^5.1.0", + "postcss-svgo": "^5.1.0", + "postcss-unique-selectors": "^5.1.1" + } + }, + "cssnano-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", + "requires": {} + }, + "csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "requires": { + "css-tree": "^1.1.2" + }, + "dependencies": { + "css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "requires": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + } + }, + "mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + } + } + }, + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==" + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + } + } + }, + "csstype": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" + }, + "damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" + }, + "data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + } + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==" + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" + }, + "default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "requires": { + "execa": "^5.0.0" + } + }, + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==" + }, + "define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "defined": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==" + }, + "detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" + }, + "detect-port-alt": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", + "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", + "requires": { + "address": "^1.0.1", + "debug": "^2.6.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "detective": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", + "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "requires": { + "acorn-node": "^1.8.2", + "defined": "^1.0.0", + "minimist": "^1.2.6" + } + }, + "didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + }, + "diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==" + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "requires": { + "path-type": "^4.0.0" + } + }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==" + }, + "dns-packet": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.4.0.tgz", + "integrity": "sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==", + "requires": { + "@leichtgewicht/ip-codec": "^2.0.1" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "requires": { + "utila": "~0.4" + } + }, + "dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, + "domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "requires": { + "webidl-conversions": "^5.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==" + } + } + }, + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "dev": true + }, + "dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" + }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "ejs": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", + "integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==", + "requires": { + "jake": "^10.8.5" + } + }, + "electron-to-chromium": { + "version": "1.4.284", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", + "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==" + }, + "emittery": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", + "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==" + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha512-knHEZMgs8BB+MInokmNTg/OyPlAddghe1YBgNwJBc5zsJi/uyIcXoSDsL/W9ymOsBoBGdPIHXYJ9+qKFwRwDng==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "enhanced-resolve": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", + "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + }, + "errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "requires": { + "stackframe": "^1.3.4" + } + }, + "es-abstract": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", + "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + } + }, + "es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" + }, + "es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==" + }, + "es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "requires": { + "has": "^1.0.3" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + }, + "escodegen": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", + "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "requires": { + "prelude-ls": "~1.1.2" + } + } + } + }, + "eslint": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.29.0.tgz", + "integrity": "sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==", + "requires": { + "@eslint/eslintrc": "^1.3.3", + "@humanwhocodes/config-array": "^0.11.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.15.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", + "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", + "requires": { + "type-fest": "^0.20.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" + } + } + }, + "eslint-config-react-app": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", + "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", + "requires": { + "@babel/core": "^7.16.0", + "@babel/eslint-parser": "^7.16.3", + "@rushstack/eslint-patch": "^1.1.0", + "@typescript-eslint/eslint-plugin": "^5.5.0", + "@typescript-eslint/parser": "^5.5.0", + "babel-preset-react-app": "^10.0.1", + "confusing-browser-globals": "^1.0.11", + "eslint-plugin-flowtype": "^8.0.3", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jest": "^25.3.0", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.27.1", + "eslint-plugin-react-hooks": "^4.3.0", + "eslint-plugin-testing-library": "^5.0.1" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "requires": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + } + }, + "eslint-module-utils": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", + "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", + "requires": { + "debug": "^3.2.7" + } + }, + "eslint-plugin-flowtype": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", + "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==", + "requires": { + "lodash": "^4.17.21", + "string-natural-compare": "^3.0.1" + } + }, + "eslint-plugin-import": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", + "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "requires": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.3", + "has": "^1.0.3", + "is-core-module": "^2.8.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.5", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "requires": { + "esutils": "^2.0.2" + } + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "tsconfig-paths": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + } + } + }, + "eslint-plugin-jest": { + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", + "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", + "requires": { + "@typescript-eslint/experimental-utils": "^5.0.0" + } + }, + "eslint-plugin-jsx-a11y": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz", + "integrity": "sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q==", + "requires": { + "@babel/runtime": "^7.18.9", + "aria-query": "^4.2.2", + "array-includes": "^3.1.5", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.4.3", + "axobject-query": "^2.2.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "has": "^1.0.3", + "jsx-ast-utils": "^3.3.2", + "language-tags": "^1.0.5", + "minimatch": "^3.1.2", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "eslint-plugin-react": { + "version": "7.31.11", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.11.tgz", + "integrity": "sha512-TTvq5JsT5v56wPa9OYHzsrOlHzKZKjV+aLgS+55NJP/cuzdiQPC7PfYoUjMoxlffKtvijpk7vA/jmuqRb9nohw==", + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.3", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.8" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "requires": { + "esutils": "^2.0.2" + } + }, + "resolve": { + "version": "2.0.0-next.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", + "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "requires": {} + }, + "eslint-plugin-testing-library": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.9.1.tgz", + "integrity": "sha512-6BQp3tmb79jLLasPHJmy8DnxREe+2Pgf7L+7o09TSWPfdqqtQfRZmZNetr5mOs3yqZk/MRNxpN3RUpJe0wB4LQ==", + "requires": { + "@typescript-eslint/utils": "^5.13.0" + } + }, + "eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==" + } + } + }, + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==" + }, + "eslint-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", + "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==", + "requires": { + "@types/eslint": "^7.29.0 || ^8.4.1", + "jest-worker": "^28.0.2", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-worker": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", + "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "espree": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "requires": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==" + }, + "expect": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", + "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", + "requires": { + "@jest/types": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1" + } + }, + "express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, + "fastq": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", + "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", + "requires": { + "reusify": "^1.0.4" + } + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "requires": { + "bser": "2.1.1" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "requires": { + "flat-cache": "^3.0.4" + } + }, + "file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" + }, + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" + }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + } + } + }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.1.tgz", + "integrity": "sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "filesize": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", + "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "dependencies": { + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "fork-ts-checker-webpack-plugin": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz", + "integrity": "sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==", + "requires": { + "@babel/code-frame": "^7.8.3", + "@types/json-schema": "^7.0.5", + "chalk": "^4.1.0", + "chokidar": "^3.4.2", + "cosmiconfig": "^6.0.0", + "deepmerge": "^4.2.2", + "fs-extra": "^9.0.0", + "glob": "^7.1.6", + "memfs": "^3.1.2", + "minimatch": "^3.0.4", + "schema-utils": "2.7.0", + "semver": "^7.3.2", + "tapable": "^1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + } + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "requires": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + } + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" + } + } + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" + }, + "generic-names": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-1.0.3.tgz", + "integrity": "sha512-b6OHfQuKasIKM9b6YPkX+KUj/TLBTx3B/1aT1T5F12FEuEqyFMdr59OMS53aoaSw8eVtapdqieX6lbg5opaOhA==", + "dev": true, + "requires": { + "loader-utils": "^0.2.16" + } + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==" + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==" + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "requires": { + "global-prefix": "^3.0.0" + } + }, + "global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "requires": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "dependencies": { + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" + }, + "gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "requires": { + "duplexer": "^0.1.2" + } + }, + "handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==" + }, + "harmony-reflect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", + "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==" + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "hoopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", + "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==" + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "requires": { + "whatwg-encoding": "^1.0.5" + } + }, + "html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==" + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + }, + "html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "requires": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + } + }, + "html-webpack-plugin": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", + "integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==", + "requires": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + } + }, + "htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==" + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "requires": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "requires": {} + }, + "idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + }, + "identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", + "requires": { + "harmony-reflect": "^1.4.6" + } + }, + "ignore": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", + "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==" + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true + }, + "immer": { + "version": "9.0.16", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.16.tgz", + "integrity": "sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ==" + }, + "immutable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", + "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", + "devOptional": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + } + } + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "ipaddr.js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", + "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" + }, + "is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==" + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==" + }, + "is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==" + }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==" + }, + "is-root": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", + "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==" + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==" + }, + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jake": { + "version": "10.8.5", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", + "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", + "requires": { + "@jest/core": "^27.5.1", + "import-local": "^3.0.2", + "jest-cli": "^27.5.1" + } + }, + "jest-changed-files": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", + "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==", + "requires": { + "@jest/types": "^27.5.1", + "execa": "^5.0.0", + "throat": "^6.0.1" + } + }, + "jest-circus": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz", + "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==", + "requires": { + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "expect": "^27.5.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3", + "throat": "^6.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-cli": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", + "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", + "requires": { + "@jest/core": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "prompts": "^2.0.1", + "yargs": "^16.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-config": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", + "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==", + "requires": { + "@babel/core": "^7.8.0", + "@jest/test-sequencer": "^27.5.1", + "@jest/types": "^27.5.1", + "babel-jest": "^27.5.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.9", + "jest-circus": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-jasmine2": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-docblock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", + "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==", + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", + "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", + "requires": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "jest-get-type": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-environment-jsdom": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", + "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", + "requires": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1", + "jsdom": "^16.6.0" + } + }, + "jest-environment-node": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", + "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==", + "requires": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" + } + }, + "jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==" + }, + "jest-haste-map": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", + "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", + "requires": { + "@jest/types": "^27.5.1", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^27.5.1", + "jest-serializer": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "micromatch": "^4.0.4", + "walker": "^1.0.7" + } + }, + "jest-jasmine2": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", + "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==", + "requires": { + "@jest/environment": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "expect": "^27.5.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "throat": "^6.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-leak-detector": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", + "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==", + "requires": { + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + } + }, + "jest-matcher-utils": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-mock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", + "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*" + } + }, + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "requires": {} + }, + "jest-regex-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", + "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==" + }, + "jest-resolve": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz", + "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==", + "requires": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "resolve": "^1.20.0", + "resolve.exports": "^1.1.0", + "slash": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-resolve-dependencies": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz", + "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==", + "requires": { + "@jest/types": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-snapshot": "^27.5.1" + } + }, + "jest-runner": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", + "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==", + "requires": { + "@jest/console": "^27.5.1", + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-leak-detector": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "source-map-support": "^0.5.6", + "throat": "^6.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-runtime": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", + "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", + "requires": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/globals": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "execa": "^5.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-serializer": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", + "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", + "requires": { + "@types/node": "*", + "graceful-fs": "^4.2.9" + } + }, + "jest-snapshot": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", + "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", + "requires": { + "@babel/core": "^7.7.2", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.0.0", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__traverse": "^7.0.4", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^27.5.1", + "semver": "^7.3.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-validate": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz", + "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==", + "requires": { + "@jest/types": "^27.5.1", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^27.5.1", + "leven": "^3.1.0", + "pretty-format": "^27.5.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-watch-typeahead": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-1.1.0.tgz", + "integrity": "sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw==", + "requires": { + "ansi-escapes": "^4.3.1", + "chalk": "^4.0.0", + "jest-regex-util": "^28.0.0", + "jest-watcher": "^28.0.0", + "slash": "^4.0.0", + "string-length": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "dependencies": { + "@jest/console": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz", + "integrity": "sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==", + "requires": { + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3", + "slash": "^3.0.0" + }, + "dependencies": { + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" + } + } + }, + "@jest/test-result": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz", + "integrity": "sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==", + "requires": { + "@jest/console": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/types": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", + "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", + "requires": { + "@jest/schemas": "^28.1.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.16", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.16.tgz", + "integrity": "sha512-Mh3OP0oh8X7O7F9m5AplC+XHYLBWuPKNkGVD3gIZFLFebBnuFI2Nz5Sf8WLvwGxECJ8YjifQvFdh79ubODkdug==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "emittery": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", + "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-message-util": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", + "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^28.1.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^28.1.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" + } + } + }, + "jest-regex-util": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", + "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==" + }, + "jest-util": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", + "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", + "requires": { + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jest-watcher": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz", + "integrity": "sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==", + "requires": { + "@jest/test-result": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.10.2", + "jest-util": "^28.1.3", + "string-length": "^4.0.1" + }, + "dependencies": { + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "pretty-format": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", + "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", + "requires": { + "@jest/schemas": "^28.1.3", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + } + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==" + }, + "string-length": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", + "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==", + "requires": { + "char-regex": "^2.0.0", + "strip-ansi": "^7.0.1" + }, + "dependencies": { + "char-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.1.tgz", + "integrity": "sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==" + } + } + }, + "strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "requires": { + "ansi-regex": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + } + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-watcher": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz", + "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==", + "requires": { + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "jest-util": "^27.5.1", + "string-length": "^4.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-sdsl": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", + "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==" + }, + "js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsdom": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", + "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "requires": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.6", + "xml-name-validator": "^3.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==", + "dev": true + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==" + }, + "jsx-ast-utils": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", + "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==", + "requires": { + "array-includes": "^3.1.5", + "object.assign": "^4.1.3" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" + }, + "klona": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", + "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==" + }, + "language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==" + }, + "language-tags": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.6.tgz", + "integrity": "sha512-HNkaCgM8wZgE/BZACeotAAgpL9FUjEnhgF0FVQMIgH//zqTPreLYMb3rWYkYAqPoF75Jwuycp1da7uz66cfFQg==", + "requires": { + "language-subtag-registry": "^0.3.20" + } + }, + "less": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", + "integrity": "sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==", + "dev": true, + "requires": { + "copy-anything": "^2.0.1", + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "parse-node-version": "^1.0.1", + "source-map": "~0.6.0", + "tslib": "^2.3.0" + } + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lilconfig": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", + "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==" + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==" + }, + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha512-tiv66G0SmiOx+pLWMtGEkfSEejxvb6N6uRrQjfWJIT79W9GMpgKeCAmm9aVBKtd4WEgntciI8CsGqjpDoCWJug==", + "dev": true, + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "requires": { + "tslib": "^2.0.3" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "requires": { + "sourcemap-codec": "^1.4.8" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "requires": { + "tmpl": "1.0.5" + } + }, + "match-sorter": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz", + "integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==", + "requires": { + "@babel/runtime": "^7.12.5", + "remove-accents": "0.4.2" + } + }, + "mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "memfs": { + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.12.tgz", + "integrity": "sha512-BcjuQn6vfqP+k100e0E9m61Hyqa//Brp+I3f0OBmN0ATHlFA8vx3Lt8z57R3u2bPqe3WGDBC+nF72fTH7isyEw==", + "requires": { + "fs-monkey": "^1.0.3" + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "microseconds": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", + "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "mini-css-extract-plugin": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.2.tgz", + "integrity": "sha512-EdlUizq13o0Pd+uCp+WO/JpkLvHRVGt97RqfeGhXqAcorYo1ypJSpkV+WDT0vY/kmh/p7wRdJNJtuyK540PXDw==", + "requires": { + "schema-utils": "^4.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + } + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "requires": { + "minimist": "^1.2.6" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "requires": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + } + }, + "nano-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", + "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==", + "requires": { + "big-integer": "^1.6.16" + } + }, + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==" + }, + "needle": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz", + "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==", + "dev": true, + "optional": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + } + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" + }, + "node-releases": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" + }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "requires": { + "path-key": "^3.0.0" + } + }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "requires": { + "boolbase": "^1.0.0" + } + }, + "nwsapi": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", + "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", + "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "object.fromentries": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", + "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz", + "integrity": "sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw==", + "requires": { + "array.prototype.reduce": "^1.0.5", + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "object.hasown": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", + "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", + "requires": { + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "object.values": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "oblivious-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", + "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==" + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "requires": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true + }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true + }, + "pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==" + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + } + } + }, + "pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "requires": { + "find-up": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" + } + } + }, + "postcss": { + "version": "8.4.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz", + "integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==", + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "postcss-attribute-case-insensitive": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz", + "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==", + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-browser-comments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz", + "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==", + "requires": {} + }, + "postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "requires": { + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-color-functional-notation": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", + "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-color-hex-alpha": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz", + "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-color-rebeccapurple": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz", + "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-colormin": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.0.tgz", + "integrity": "sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg==", + "requires": { + "browserslist": "^4.16.6", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-convert-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", + "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", + "requires": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-custom-media": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz", + "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-custom-properties": { + "version": "12.1.11", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz", + "integrity": "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-custom-selectors": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz", + "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==", + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-dir-pseudo-class": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz", + "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==", + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-discard-comments": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", + "requires": {} + }, + "postcss-discard-duplicates": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", + "requires": {} + }, + "postcss-discard-empty": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", + "requires": {} + }, + "postcss-discard-overridden": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", + "requires": {} + }, + "postcss-double-position-gradients": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz", + "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==", + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-env-function": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", + "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-filter-plugins": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/postcss-filter-plugins/-/postcss-filter-plugins-3.0.1.tgz", + "integrity": "sha512-tRKbW4wWBEkSSFuJtamV2wkiV9rj6Yy7P3Y13+zaynlPEEZt8EgYKn3y/RBpMeIhNmHXFlSdzofml65hD5OafA==", + "dev": true, + "requires": { + "postcss": "^6.0.14" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-flexbugs-fixes": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", + "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", + "requires": {} + }, + "postcss-focus-visible": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", + "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "postcss-focus-within": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", + "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "requires": {} + }, + "postcss-gap-properties": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", + "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", + "requires": {} + }, + "postcss-icss-keyframes": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/postcss-icss-keyframes/-/postcss-icss-keyframes-0.2.1.tgz", + "integrity": "sha512-4m+hLY5TVqoTM198KKnzdNudyu1OvtqwD+8kVZ9PNiEO4+IfHYoyVvEXsOHjV8nZ1k6xowf+nY4HlUfZhOFvvw==", + "dev": true, + "requires": { + "icss-utils": "^3.0.1", + "postcss": "^6.0.2", + "postcss-value-parser": "^3.3.0" + }, + "dependencies": { + "icss-utils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-3.0.1.tgz", + "integrity": "sha512-ANhVLoEfe0KoC9+z4yiTaXOneB49K6JIXdS+yAgH0NERELpdIT7kkj2XxUPuHafeHnn8umXnECSpsfk1RTaUew==", + "dev": true, + "requires": { + "postcss": "^6.0.2" + } + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-icss-selectors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/postcss-icss-selectors/-/postcss-icss-selectors-2.0.3.tgz", + "integrity": "sha512-dxFtq+wscbU9faJaH8kIi98vvCPDbt+qg1g9GoG0os1PY3UvgY1Y2G06iZrZb1iVC9cyFfafwSY1IS+IQpRQ4w==", + "dev": true, + "requires": { + "css-selector-tokenizer": "^0.7.0", + "generic-names": "^1.0.2", + "icss-utils": "^3.0.1", + "lodash": "^4.17.4", + "postcss": "^6.0.2" + }, + "dependencies": { + "icss-utils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-3.0.1.tgz", + "integrity": "sha512-ANhVLoEfe0KoC9+z4yiTaXOneB49K6JIXdS+yAgH0NERELpdIT7kkj2XxUPuHafeHnn8umXnECSpsfk1RTaUew==", + "dev": true, + "requires": { + "postcss": "^6.0.2" + } + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-image-set-function": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz", + "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "requires": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-initial": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", + "requires": {} + }, + "postcss-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", + "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "requires": { + "camelcase-css": "^2.0.1" + } + }, + "postcss-lab-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz", + "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==", + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "requires": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + } + }, + "postcss-loader": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", + "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", + "requires": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.5" + }, + "dependencies": { + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "postcss-logical": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", + "requires": {} + }, + "postcss-media-minmax": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", + "requires": {} + }, + "postcss-merge-longhand": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", + "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", + "requires": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^5.1.1" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-merge-rules": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.3.tgz", + "integrity": "sha512-LbLd7uFC00vpOuMvyZop8+vvhnfRGpp2S+IMQKeuOZZapPRY4SMq5ErjQeHbHsjCUgJkRNrlU+LmxsKIqPKQlA==", + "requires": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^3.1.0", + "postcss-selector-parser": "^6.0.5" + } + }, + "postcss-minify-font-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", + "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-minify-gradients": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", + "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", + "requires": { + "colord": "^2.9.1", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-minify-params": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", + "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", + "requires": { + "browserslist": "^4.21.4", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-minify-selectors": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", + "requires": { + "postcss-selector-parser": "^6.0.5" + } + }, + "postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "requires": {} + }, + "postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "requires": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "requires": { + "icss-utils": "^5.0.0" + } + }, + "postcss-nested": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", + "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-nesting": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz", + "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==", + "requires": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-normalize": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz", + "integrity": "sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA==", + "requires": { + "@csstools/normalize.css": "*", + "postcss-browser-comments": "^4", + "sanitize.css": "*" + } + }, + "postcss-normalize-charset": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", + "requires": {} + }, + "postcss-normalize-display-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", + "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-normalize-positions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", + "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-normalize-repeat-style": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", + "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-normalize-string": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", + "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-normalize-timing-functions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", + "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-normalize-unicode": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", + "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", + "requires": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-normalize-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", + "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", + "requires": { + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-normalize-whitespace": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", + "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-opacity-percentage": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.2.tgz", + "integrity": "sha512-lyUfF7miG+yewZ8EAk9XUBIlrHyUE6fijnesuz+Mj5zrIHIEw6KcIZSOk/elVMqzLvREmXB83Zi/5QpNRYd47w==" + }, + "postcss-ordered-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", + "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", + "requires": { + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-overflow-shorthand": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz", + "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "requires": {} + }, + "postcss-place": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz", + "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-preset-env": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz", + "integrity": "sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==", + "requires": { + "@csstools/postcss-cascade-layers": "^1.1.1", + "@csstools/postcss-color-function": "^1.1.1", + "@csstools/postcss-font-format-keywords": "^1.0.1", + "@csstools/postcss-hwb-function": "^1.0.2", + "@csstools/postcss-ic-unit": "^1.0.1", + "@csstools/postcss-is-pseudo-class": "^2.0.7", + "@csstools/postcss-nested-calc": "^1.0.0", + "@csstools/postcss-normalize-display-values": "^1.0.1", + "@csstools/postcss-oklab-function": "^1.1.1", + "@csstools/postcss-progressive-custom-properties": "^1.3.0", + "@csstools/postcss-stepped-value-functions": "^1.0.1", + "@csstools/postcss-text-decoration-shorthand": "^1.0.0", + "@csstools/postcss-trigonometric-functions": "^1.0.2", + "@csstools/postcss-unset-value": "^1.0.2", + "autoprefixer": "^10.4.13", + "browserslist": "^4.21.4", + "css-blank-pseudo": "^3.0.3", + "css-has-pseudo": "^3.0.4", + "css-prefers-color-scheme": "^6.0.3", + "cssdb": "^7.1.0", + "postcss-attribute-case-insensitive": "^5.0.2", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^4.2.4", + "postcss-color-hex-alpha": "^8.0.4", + "postcss-color-rebeccapurple": "^7.1.1", + "postcss-custom-media": "^8.0.2", + "postcss-custom-properties": "^12.1.10", + "postcss-custom-selectors": "^6.0.3", + "postcss-dir-pseudo-class": "^6.0.5", + "postcss-double-position-gradients": "^3.1.2", + "postcss-env-function": "^4.0.6", + "postcss-focus-visible": "^6.0.4", + "postcss-focus-within": "^5.0.4", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^3.0.5", + "postcss-image-set-function": "^4.0.7", + "postcss-initial": "^4.0.1", + "postcss-lab-function": "^4.2.1", + "postcss-logical": "^5.0.4", + "postcss-media-minmax": "^5.0.0", + "postcss-nesting": "^10.2.0", + "postcss-opacity-percentage": "^1.1.2", + "postcss-overflow-shorthand": "^3.0.4", + "postcss-page-break": "^3.0.4", + "postcss-place": "^7.0.5", + "postcss-pseudo-class-any-link": "^7.1.6", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-pseudo-class-any-link": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz", + "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==", + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-reduce-initial": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.1.tgz", + "integrity": "sha512-//jeDqWcHPuXGZLoolFrUXBDyuEGbr9S2rMo19bkTIjBQ4PqkaO+oI8wua5BOUxpfi97i3PCoInsiFIEBfkm9w==", + "requires": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0" + } + }, + "postcss-reduce-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", + "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", + "requires": { + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "requires": {} + }, + "postcss-selector-not": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz", + "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==", + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-selector-parser": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", + "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-svgo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", + "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", + "requires": { + "postcss-value-parser": "^4.2.0", + "svgo": "^2.7.0" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + }, + "css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "requires": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + } + }, + "mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "requires": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + } + } + } + }, + "postcss-unique-selectors": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", + "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", + "requires": { + "postcss-selector-parser": "^6.0.5" + } + }, + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" + }, + "pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==" + }, + "pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "requires": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "requires": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + } + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "requires": { + "asap": "~2.0.6" + } + }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "dependencies": { + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + } + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true + }, + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==" + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + }, + "raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "requires": { + "performance-now": "^2.1.0" + } + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-app-polyfill": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz", + "integrity": "sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w==", + "requires": { + "core-js": "^3.19.2", + "object-assign": "^4.1.1", + "promise": "^8.1.0", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.9", + "whatwg-fetch": "^3.6.2" + } + }, + "react-dev-utils": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", + "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", + "requires": { + "@babel/code-frame": "^7.16.0", + "address": "^1.1.2", + "browserslist": "^4.18.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "detect-port-alt": "^1.1.6", + "escape-string-regexp": "^4.0.0", + "filesize": "^8.0.6", + "find-up": "^5.0.0", + "fork-ts-checker-webpack-plugin": "^6.5.0", + "global-modules": "^2.0.0", + "globby": "^11.0.4", + "gzip-size": "^6.0.0", + "immer": "^9.0.7", + "is-root": "^2.1.0", + "loader-utils": "^3.2.0", + "open": "^8.4.0", + "pkg-up": "^3.1.0", + "prompts": "^2.4.2", + "react-error-overlay": "^6.0.11", + "recursive-readdir": "^2.2.2", + "shell-quote": "^1.7.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + } + }, + "react-error-overlay": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", + "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "react-query": { + "version": "3.39.2", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.2.tgz", + "integrity": "sha512-F6hYDKyNgDQfQOuR1Rsp3VRzJnWHx6aRnnIZHMNGGgbL3SBgpZTDg8MQwmxOgpCAoqZJA+JSNCydF1xGJqKOCA==", + "requires": { + "@babel/runtime": "^7.5.5", + "broadcast-channel": "^3.4.1", + "match-sorter": "^6.0.2" + } + }, + "react-refresh": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", + "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==" + }, + "react-router": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.4.4.tgz", + "integrity": "sha512-SA6tSrUCRfuLWeYsTJDuriRqfFIsrSvuH7SqAJHegx9ZgxadE119rU8oOX/rG5FYEthpdEaEljdjDlnBxvfr+Q==", + "requires": { + "@remix-run/router": "1.0.4" + } + }, + "react-router-dom": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.4.4.tgz", + "integrity": "sha512-0Axverhw5d+4SBhLqLpzPhNkmv7gahUwlUVIOrRLGJ4/uwt30JVajVJXqv2Qr/LCwyvHhQc7YyK1Do8a9Jj7qA==", + "requires": { + "@remix-run/router": "1.0.4", + "react-router": "6.4.4" + } + }, + "react-scripts": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", + "integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==", + "requires": { + "@babel/core": "^7.16.0", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", + "@svgr/webpack": "^5.5.0", + "babel-jest": "^27.4.2", + "babel-loader": "^8.2.3", + "babel-plugin-named-asset-import": "^0.3.8", + "babel-preset-react-app": "^10.0.1", + "bfj": "^7.0.2", + "browserslist": "^4.18.1", + "camelcase": "^6.2.1", + "case-sensitive-paths-webpack-plugin": "^2.4.0", + "css-loader": "^6.5.1", + "css-minimizer-webpack-plugin": "^3.2.0", + "dotenv": "^10.0.0", + "dotenv-expand": "^5.1.0", + "eslint": "^8.3.0", + "eslint-config-react-app": "^7.0.1", + "eslint-webpack-plugin": "^3.1.1", + "file-loader": "^6.2.0", + "fs-extra": "^10.0.0", + "fsevents": "^2.3.2", + "html-webpack-plugin": "^5.5.0", + "identity-obj-proxy": "^3.0.0", + "jest": "^27.4.3", + "jest-resolve": "^27.4.2", + "jest-watch-typeahead": "^1.0.0", + "mini-css-extract-plugin": "^2.4.5", + "postcss": "^8.4.4", + "postcss-flexbugs-fixes": "^5.0.2", + "postcss-loader": "^6.2.1", + "postcss-normalize": "^10.0.1", + "postcss-preset-env": "^7.0.1", + "prompts": "^2.4.2", + "react-app-polyfill": "^3.0.0", + "react-dev-utils": "^12.0.1", + "react-refresh": "^0.11.0", + "resolve": "^1.20.0", + "resolve-url-loader": "^4.0.0", + "sass-loader": "^12.3.0", + "semver": "^7.3.5", + "source-map-loader": "^3.0.0", + "style-loader": "^3.3.1", + "tailwindcss": "^3.0.2", + "terser-webpack-plugin": "^5.2.5", + "webpack": "^5.64.4", + "webpack-dev-server": "^4.6.0", + "webpack-manifest-plugin": "^4.0.2", + "workbox-webpack-plugin": "^6.4.1" + }, + "dependencies": { + "dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "react-xarrows": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-xarrows/-/react-xarrows-2.0.2.tgz", + "integrity": "sha512-tDlAqaxHNmy0vegW/6NdhoWyXJq1LANX/WUAlHyzoHe9BwFVnJPPDghmDjYeVr7XWFmBrVTUrHsrW7GKYI6HtQ==", + "requires": { + "@types/prop-types": "^15.7.3", + "lodash": "^4.17.21", + "prop-types": "^15.7.2" + } + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "requires": { + "pify": "^2.3.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" + } + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "requires": { + "picomatch": "^2.2.1" + } + }, + "recursive-readdir": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "requires": { + "minimatch": "^3.0.5" + } + }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" + }, + "regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "requires": { + "regenerate": "^1.4.2" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "requires": { + "@babel/runtime": "^7.8.4" + } + }, + "regex-parser": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", + "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==" + }, + "regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==" + }, + "regexpu-core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz", + "integrity": "sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==", + "requires": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsgen": "^0.7.1", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + } + }, + "regjsgen": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", + "integrity": "sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==" + }, + "regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==" + } + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==" + }, + "remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" + }, + "renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "requires": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "reserved-words": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/reserved-words/-/reserved-words-0.1.2.tgz", + "integrity": "sha512-0S5SrIUJ9LfpbVl4Yzij6VipUdafHrOTzvmfazSw/jeZrZtQK303OPZW+obtkaw7jQlTQppy0UvZWm9872PbRw==", + "dev": true + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + }, + "resolve-url-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz", + "integrity": "sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==", + "requires": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^7.0.35", + "source-map": "0.6.1" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" + }, + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" + }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + } + } + }, + "resolve.exports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", + "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==" + }, + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==" + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "requires": { + "fsevents": "~2.3.2" + } + }, + "rollup-plugin-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", + "requires": { + "@babel/code-frame": "^7.10.4", + "jest-worker": "^26.2.1", + "serialize-javascript": "^4.0.0", + "terser": "^5.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "requires": { + "randombytes": "^2.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sanitize.css": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", + "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==" + }, + "sass": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.1.tgz", + "integrity": "sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ==", + "devOptional": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, + "sass-loader": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", + "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", + "requires": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "requires": { + "xmlchars": "^2.2.0" + } + }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==" + }, + "selfsigned": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", + "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "requires": { + "node-forge": "^1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "optional": true + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + } + } + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==" + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==" + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "shell-quote": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz", + "integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" + }, + "sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "requires": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + }, + "source-map-loader": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.2.tgz", + "integrity": "sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg==", + "requires": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.1" + } + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + }, + "spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" + }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + } + } + }, + "stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "string-natural-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", + "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + } + } + }, + "string.prototype.matchall": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", + "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.3", + "side-channel": "^1.0.4" + } + }, + "string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "requires": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==" + }, + "strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==" + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + }, + "style-loader": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", + "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", + "requires": {} + }, + "stylehacks": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", + "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "requires": { + "browserslist": "^4.21.4", + "postcss-selector-parser": "^6.0.4" + } + }, + "stylus": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz", + "integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==", + "dev": true, + "requires": { + "@adobe/css-tools": "^4.0.1", + "debug": "^4.3.2", + "glob": "^7.1.6", + "sax": "~1.2.4", + "source-map": "^0.7.3" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true + } + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" + }, + "svgo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", + "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", + "requires": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.37", + "csso": "^4.0.2", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + }, + "dependencies": { + "css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-what": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", + "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==" + }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + }, + "dependencies": { + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + } + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "requires": { + "boolbase": "~1.0.0" + } + } + } + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, + "tailwindcss": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz", + "integrity": "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==", + "requires": { + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "color-name": "^1.1.4", + "detective": "^5.2.1", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.12", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "lilconfig": "^2.0.6", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.18", + "postcss-import": "^14.1.0", + "postcss-js": "^4.0.0", + "postcss-load-config": "^3.1.4", + "postcss-nested": "6.0.0", + "postcss-selector-parser": "^6.0.10", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.22.1" + }, + "dependencies": { + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "requires": { + "is-glob": "^4.0.3" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + } + } + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" + }, + "temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==" + }, + "tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "requires": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "dependencies": { + "type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==" + } + } + }, + "terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "requires": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + } + }, + "terser": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.1.tgz", + "integrity": "sha512-xvQfyfA1ayT0qdK47zskQgRZeWLoOQ8JQ6mIgRGVNwZKdQMU+5FkCBjmv4QjcrTzyZquRw2FVtlJSRUmMKQslw==", + "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + } + } + }, + "terser-webpack-plugin": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", + "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", + "requires": { + "@jridgewell/trace-mapping": "^0.3.14", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0", + "terser": "^5.14.1" + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + }, + "throat": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", + "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==" + }, + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" + }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "tough-cookie": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", + "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "dependencies": { + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==" + } + } + }, + "tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "requires": { + "punycode": "^2.1.1" + } + }, + "tryer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", + "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" + }, + "tsconfig-paths": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.1.1.tgz", + "integrity": "sha512-VgPrtLKpRgEAJsMj5Q/I/mXouC6A/7eJ/X4Nuk6o0cRPwBtznYxTCU4FodbexbzH9somBPEXYi0ZkUViUpJ21Q==", + "dev": true, + "requires": { + "json5": "^2.2.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true + } + } + }, + "tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typescript": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", + "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==" + }, + "typescript-plugin-css-modules": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/typescript-plugin-css-modules/-/typescript-plugin-css-modules-4.1.1.tgz", + "integrity": "sha512-kpVxGkY/go9eV5TP1YUDJ6SqwBx2OIuVStMCxKyg9PhJVFXjLYR7AuItVLwoz0NCdiemH91WhtgAjb96jI34DA==", + "dev": true, + "requires": { + "dotenv": "^16.0.3", + "icss-utils": "^5.1.0", + "less": "^4.1.3", + "lodash.camelcase": "^4.3.0", + "postcss": "^8.4.19", + "postcss-filter-plugins": "^3.0.1", + "postcss-icss-keyframes": "^0.2.1", + "postcss-icss-selectors": "^2.0.3", + "postcss-load-config": "^3.1.4", + "reserved-words": "^0.1.2", + "sass": "^1.56.1", + "source-map-js": "^1.0.2", + "stylus": "^0.59.0", + "tsconfig-paths": "^4.1.1" + } + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==" + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==" + }, + "unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==" + }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "requires": { + "crypto-random-string": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + }, + "unload": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", + "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", + "requires": { + "@babel/runtime": "^7.6.2", + "detect-node": "^2.0.4" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==" + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==" + }, + "update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "util.promisify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", + "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.2", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.0" + } + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "v8-to-istanbul": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", + "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==" + } + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "requires": { + "xml-name-validator": "^3.0.0" + } + }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "requires": { + "makeerror": "1.0.12" + } + }, + "watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==" + }, + "webpack": { + "version": "5.75.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", + "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==", + "requires": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "dependencies": { + "@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==" + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + } + } + }, + "webpack-dev-middleware": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", + "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "requires": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + } + } + }, + "webpack-dev-server": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz", + "integrity": "sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==", + "requires": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.1", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.4.2" + }, + "dependencies": { + "ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + }, + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "requires": {} + } + } + }, + "webpack-manifest-plugin": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz", + "integrity": "sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow==", + "requires": { + "tapable": "^2.0.0", + "webpack-sources": "^2.2.0" + }, + "dependencies": { + "webpack-sources": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", + "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", + "requires": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + } + } + } + }, + "webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==" + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "requires": { + "iconv-lite": "0.4.24" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + }, + "whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "requires": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + }, + "workbox-background-sync": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz", + "integrity": "sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g==", + "requires": { + "idb": "^7.0.1", + "workbox-core": "6.5.4" + } + }, + "workbox-broadcast-update": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.5.4.tgz", + "integrity": "sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw==", + "requires": { + "workbox-core": "6.5.4" + } + }, + "workbox-build": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.5.4.tgz", + "integrity": "sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA==", + "requires": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.11.1", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^5.2.0", + "@rollup/plugin-node-resolve": "^11.2.1", + "@rollup/plugin-replace": "^2.4.1", + "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^7.1.6", + "lodash": "^4.17.20", + "pretty-bytes": "^5.3.0", + "rollup": "^2.43.1", + "rollup-plugin-terser": "^7.0.0", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "6.5.4", + "workbox-broadcast-update": "6.5.4", + "workbox-cacheable-response": "6.5.4", + "workbox-core": "6.5.4", + "workbox-expiration": "6.5.4", + "workbox-google-analytics": "6.5.4", + "workbox-navigation-preload": "6.5.4", + "workbox-precaching": "6.5.4", + "workbox-range-requests": "6.5.4", + "workbox-recipes": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4", + "workbox-streams": "6.5.4", + "workbox-sw": "6.5.4", + "workbox-window": "6.5.4" + }, + "dependencies": { + "@apideck/better-ajv-errors": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", + "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "requires": { + "json-schema": "^0.4.0", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + } + }, + "ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "requires": { + "whatwg-url": "^7.0.0" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "requires": { + "punycode": "^2.1.0" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + } + }, + "workbox-cacheable-response": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.5.4.tgz", + "integrity": "sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug==", + "requires": { + "workbox-core": "6.5.4" + } + }, + "workbox-core": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.5.4.tgz", + "integrity": "sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q==" + }, + "workbox-expiration": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.5.4.tgz", + "integrity": "sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ==", + "requires": { + "idb": "^7.0.1", + "workbox-core": "6.5.4" + } + }, + "workbox-google-analytics": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.5.4.tgz", + "integrity": "sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg==", + "requires": { + "workbox-background-sync": "6.5.4", + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "workbox-navigation-preload": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.5.4.tgz", + "integrity": "sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng==", + "requires": { + "workbox-core": "6.5.4" + } + }, + "workbox-precaching": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.5.4.tgz", + "integrity": "sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg==", + "requires": { + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "workbox-range-requests": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.5.4.tgz", + "integrity": "sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg==", + "requires": { + "workbox-core": "6.5.4" + } + }, + "workbox-recipes": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.5.4.tgz", + "integrity": "sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA==", + "requires": { + "workbox-cacheable-response": "6.5.4", + "workbox-core": "6.5.4", + "workbox-expiration": "6.5.4", + "workbox-precaching": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "workbox-routing": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.5.4.tgz", + "integrity": "sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg==", + "requires": { + "workbox-core": "6.5.4" + } + }, + "workbox-strategies": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.5.4.tgz", + "integrity": "sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw==", + "requires": { + "workbox-core": "6.5.4" + } + }, + "workbox-streams": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.5.4.tgz", + "integrity": "sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg==", + "requires": { + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4" + } + }, + "workbox-sw": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.5.4.tgz", + "integrity": "sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA==" + }, + "workbox-webpack-plugin": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.5.4.tgz", + "integrity": "sha512-LmWm/zoaahe0EGmMTrSLUi+BjyR3cdGEfU3fS6PN1zKFYbqAKuQ+Oy/27e4VSXsyIwAw8+QDfk1XHNGtZu9nQg==", + "requires": { + "fast-json-stable-stringify": "^2.1.0", + "pretty-bytes": "^5.4.1", + "upath": "^1.2.0", + "webpack-sources": "^1.4.3", + "workbox-build": "6.5.4" + }, + "dependencies": { + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + } + } + }, + "workbox-window": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.5.4.tgz", + "integrity": "sha512-HnLZJDwYBE+hpG25AQBO8RUWBJRaCsI9ksQJEp3aCOFCaG5kqaToAYXFRAHxzRluM2cQbGzdQF5rjKPWPA1fug==", + "requires": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "6.5.4" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "requires": {} + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + } + } +} diff --git a/tools/debug-ui/package.json b/tools/debug-ui/package.json new file mode 100644 index 00000000000..386d76b186a --- /dev/null +++ b/tools/debug-ui/package.json @@ -0,0 +1,43 @@ +{ + "name": "debug-ui", + "version": "0.1.0", + "private": true, + "dependencies": { + "@types/node": "^16.18.3", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-query": "^3.39.2", + "react-router": "^6.4.4", + "react-router-dom": "^6.4.4", + "react-scripts": "^5.0.1", + "react-xarrows": "^2.0.2", + "typescript": "^4.9.3" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "typescript-plugin-css-modules": "^4.1.1" + } +} \ No newline at end of file diff --git a/tools/debug-ui/public/index.html b/tools/debug-ui/public/index.html new file mode 100644 index 00000000000..90b2b4e89df --- /dev/null +++ b/tools/debug-ui/public/index.html @@ -0,0 +1,26 @@ + + + + + + + + React App + + + + +
+ + + + \ No newline at end of file diff --git a/tools/debug-ui/src/App.scss b/tools/debug-ui/src/App.scss new file mode 100644 index 00000000000..4eb2b53f285 --- /dev/null +++ b/tools/debug-ui/src/App.scss @@ -0,0 +1,26 @@ +.navbar { + padding: 4px; + background-color: #eee; + border-bottom: 1px solid gray; + + .nav-link { + margin: -4px 4px; + padding: 4px 8px; + text-decoration: none; + color: black; + + &:first-child { + margin-left: -4px; + } + + &.active { + background-color: rgb(53, 53, 159); + color: white; + } + + &:hover { + background-color: rgb(93, 93, 217); + color: white; + } + } +} \ No newline at end of file diff --git a/tools/debug-ui/src/App.tsx b/tools/debug-ui/src/App.tsx new file mode 100644 index 00000000000..3b7fa6ab0d2 --- /dev/null +++ b/tools/debug-ui/src/App.tsx @@ -0,0 +1,48 @@ +import { Navigate, Route, Routes, useParams } from 'react-router'; +import { NavLink } from 'react-router-dom'; +import './App.scss'; +import { ClusterView } from './ClusterView'; +import { HeaderBar } from './HeaderBar'; +import { LatestBlocksView } from './LatestBlocksView'; +import { NetworkInfoView } from './NetworkInfoView'; + +function useNodeAddr(): string { + const params = useParams<{ addr: string }>(); + const addr = params.addr || '127.0.0.1'; + return addr.includes(':') ? addr : addr + ':3030'; +} + +export const App = () => { + const addr = useNodeAddr(); + + + return ( +
+ +
+ Latest Blocks + Network Info + Epoch Info + Chain & Chunk Info + Sync Info + Validator Info + Cluster View + +
+ + } /> + } /> + } /> + TODO
} /> + TODO} /> + TODO} /> + TODO} /> + } /> + + + ); +} + +function navLinkClassName({ isActive }: { isActive: boolean }) { + return isActive ? 'nav-link active' : 'nav-link'; +} \ No newline at end of file diff --git a/tools/debug-ui/src/ClusterNodeView.scss b/tools/debug-ui/src/ClusterNodeView.scss new file mode 100644 index 00000000000..9d67a2e39a6 --- /dev/null +++ b/tools/debug-ui/src/ClusterNodeView.scss @@ -0,0 +1,49 @@ +.cluster-node-view { + display: flex; + align-items: center; + + &:hover { + background-color: lightgray; + } + + .addr { + width: 150px; + } + + .loading { + color: gray; + font-style: italic; + } + + .height { + width: 80px; + margin: 0 10px; + font-family: 'Courier New', Courier, monospace; + text-align: center; + } + + .account { + width: 300px; + margin: 0 10px; + overflow: hidden; + text-overflow: ellipsis; + } + + .error { + color: red; + } + + .old-height { + color: brown; + background-color: #fcc; + } + + a { + text-decoration: none; + } + + .sync-status { + width: 200px; + margin: 0 10px; + } +} \ No newline at end of file diff --git a/tools/debug-ui/src/ClusterNodeView.tsx b/tools/debug-ui/src/ClusterNodeView.tsx new file mode 100644 index 00000000000..f84a1195a37 --- /dev/null +++ b/tools/debug-ui/src/ClusterNodeView.tsx @@ -0,0 +1,114 @@ +import { useEffect } from 'react'; +import { useQuery } from 'react-query'; +import { fetchBasicStatus, fetchFullStatus, fetchSyncStatus, fetchTrackedShards, SyncStatusResponse, TrackedShardsResponse } from './api'; +import './ClusterNodeView.scss'; + +interface Props { + addr: string; + highestHeight: number; + basicStatusChanged: (addr: string, name: string | null, height: number) => void; + newNodesDiscovered: (nodes: string[]) => void; +} + +export const ClusterNodeView = ({ addr, highestHeight, basicStatusChanged, newNodesDiscovered }: Props) => { + const status = useQuery(['status', addr], () => fetchBasicStatus(addr)); + const fullStatus = useQuery(['fullStatus', addr], () => fetchFullStatus(addr)); + const syncStatus = useQuery(['syncStatus', addr], () => fetchSyncStatus(addr)); + const trackedShards = useQuery(['trackedShards', addr], () => fetchTrackedShards(addr)); + + useEffect(() => { + if (status.data) { + basicStatusChanged(addr, + status.data.validator_account_id, + status.data.sync_info.latest_block_height); + } + }, [addr, status.data, basicStatusChanged]); + + useEffect(() => { + const networkInfo = fullStatus.data?.detailed_debug_status?.network_info; + if (networkInfo) { + const newAddrs = []; + for (const peer of networkInfo.connected_peers) { + newAddrs.push(peer.addr); + } + if (networkInfo.tier1_connections) { + for (const peer of networkInfo.tier1_connections) { + newAddrs.push(peer.addr); + } + } + const newAddrsWithCorrectedPort = []; + for (const addr of newAddrs) { + newAddrsWithCorrectedPort.push(addr.split(':')[0] + ':3030'); + } + + newNodesDiscovered(newAddrsWithCorrectedPort); + } + }, [addr, fullStatus.data, newNodesDiscovered]); + + const anyError = status.error || fullStatus.error || syncStatus.error || trackedShards.error; + const anyLoading = status.isLoading || fullStatus.isLoading || syncStatus.isLoading || trackedShards.isLoading; + return ( +
+ + {status.data &&
{status.data.sync_info.latest_block_height}
} + {status.data && status.data.validator_account_id && +
{status.data.validator_account_id}
} + {syncStatus.data &&
{syncStatusToText(syncStatus.data)}
} + {trackedShards.data && +
{trackedShardsToText(trackedShards.data)}
} + {!!anyError &&
{'' + anyError}
} + {anyLoading && !anyError &&
Loading...
} +
+ ); +} + +function syncStatusToText(syncStatus: SyncStatusResponse): string { + const status = syncStatus.status_response.SyncStatus; + if (status == null) { + return 'No sync status??'; + } + if (status === 'AwaitingPeers') { + return "Awaiting peers"; + } + if (status === 'NoSync') { + return ''; + } + if (status === 'StateSyncDone') { + return "State sync done"; + } + if ("EpochSync" in status) { + return "Epoch sync"; + } + if ("HeaderSync" in status) { + return "Header sync"; + } + if ("StateSync" in status) { + return "State sync"; + } + return `Body sync ${status.BodySync.start_height} -> ${status.BodySync.highest_height}`; +} + +function booleanArrayToIndexList(array: boolean[]): number[] { + const result = []; + for (let i = 0; i < array.length; i++) { + if (array[i]) { + result.push(i); + } + } + return result; +} + +function trackedShardsToText(trackedShards: TrackedShardsResponse): string { + const { shards_tracked_this_epoch, shards_tracked_next_epoch } = trackedShards.status_response.TrackedShards; + const thisShards = booleanArrayToIndexList(shards_tracked_this_epoch).join(', '); + const nextShards = booleanArrayToIndexList(shards_tracked_next_epoch).join(', '); + return `[${thisShards}] next: [${nextShards}]`; +} \ No newline at end of file diff --git a/tools/debug-ui/src/ClusterView.scss b/tools/debug-ui/src/ClusterView.scss new file mode 100644 index 00000000000..5a2502ed18d --- /dev/null +++ b/tools/debug-ui/src/ClusterView.scss @@ -0,0 +1,8 @@ +.cluster-view { + padding: 10px; + + .title { + font-weight: bold; + margin-bottom: 10px; + } +} \ No newline at end of file diff --git a/tools/debug-ui/src/ClusterView.tsx b/tools/debug-ui/src/ClusterView.tsx new file mode 100644 index 00000000000..62090994c31 --- /dev/null +++ b/tools/debug-ui/src/ClusterView.tsx @@ -0,0 +1,74 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { ClusterNodeView } from './ClusterNodeView'; +import './ClusterView.scss'; + +interface Props { + initialAddr: string; +} + +function sortingKeyForNode(addr: string, addrToName: Map): string { + if (addrToName.has(addr)) { + return '0' + addrToName.get(addr); + } else { + return '1' + addr; + } +} + +export class DiscoveryNodes { + nodes: Set = new Set(); + addrToName: Map = new Map(); + + reset(initialAddr: string) { + this.nodes.clear(); + this.addrToName.clear(); + this.nodes.add(initialAddr); + } + + sorted(): string[] { + return Array.from(this.nodes).sort((a, b) => { + return sortingKeyForNode(a, this.addrToName).localeCompare(sortingKeyForNode(b, this.addrToName)); + }); + } +} + +export const ClusterView = ({ initialAddr }: Props) => { + const [sortedNodes, setSortedNodes] = useState([]); + const [highestHeight, setHighestHeight] = useState(0); + const nodes = useRef(new DiscoveryNodes()); + + useEffect(() => { + nodes.current.reset(initialAddr); + setHighestHeight(0); + setSortedNodes(nodes.current.sorted()); + }, [initialAddr]); + + const basicStatusCallback = useCallback((addr: string, name: string | null, height: number) => { + setHighestHeight((prev) => Math.max(prev, height)); + if (name) { + nodes.current.addrToName.set(addr, name); + } + setSortedNodes(nodes.current.sorted()); + }, []); + + const newNodesDiscoveredCallback = useCallback((newIps: string[]) => { + for (const addr of newIps) { + nodes.current.nodes.add(addr); + } + setSortedNodes(nodes.current.sorted()); + }, []); + + return ( +
+
Discovered {sortedNodes.length} nodes in cluster
+ {sortedNodes.map((addr) => { + return ; + })} +
+ ); +}; \ No newline at end of file diff --git a/tools/debug-ui/src/HeaderBar.scss b/tools/debug-ui/src/HeaderBar.scss new file mode 100644 index 00000000000..524719a6b43 --- /dev/null +++ b/tools/debug-ui/src/HeaderBar.scss @@ -0,0 +1,37 @@ +.header-bar { + width: 100%; + display: flex; + align-items: center; + background-color: #eee; + border-bottom: 1px solid gray; + padding: 4px; + box-sizing: border-box; + + .loading { + color: gray; + font-style: italic; + } + + .error { + color: red; + } + + .label { + margin: -4px 4px -4px 4px; + padding: 4px 8px; + border-left: 1px solid gray; + border-right: 1px solid gray; + background-color: #666; + color: white; + } + + .label:first-child { + border-left: none; + margin-left: -4px; + } + + .value { + margin-right: 20px; + margin-left: 4px; + } +} \ No newline at end of file diff --git a/tools/debug-ui/src/HeaderBar.tsx b/tools/debug-ui/src/HeaderBar.tsx new file mode 100644 index 00000000000..9fe5f697ac4 --- /dev/null +++ b/tools/debug-ui/src/HeaderBar.tsx @@ -0,0 +1,69 @@ +import { useQuery } from "react-query"; +import { fetchBasicStatus } from "./api"; +import './HeaderBar.scss'; + +type Props = { + addr: string; +} + +export const HeaderBar = ({ addr }: Props) => { + const { data: nodeStatus, error, isLoading } = useQuery(['status', addr], () => fetchBasicStatus(addr)); + if (isLoading) { + return
+
Loading...
+
; + } + if (error) { + return
+
{'' + error}
+
; + } + if (!nodeStatus) { + return
+
No node status
+
; + } + const version = nodeStatus.version; + return
+
Chain
+
{nodeStatus.chain_id}
+ {nodeStatus.validator_account_id &&
Validator
} + {nodeStatus.validator_account_id &&
{nodeStatus.validator_account_id}
} +
Protocol
+
{nodeStatus.protocol_version}
+
Build
+
+ {version.version === 'trunk' && 'Nightly build '} + {version.version === version.build && 'Release '} + {version.version === 'trunk' || version.version === version.build + ? {version.build} + : `Release ${version.version} (build ${version.build})`} +
+
rustc
+
{version.rustc_version}
+
Uptime
+
{formatDurationInSec(nodeStatus.uptime_sec)}
+
Address
+
{addr}
+
+} + +function formatDurationInSec(totalSeconds: number): string { + let seconds = totalSeconds % 60; + let total_minutes = (totalSeconds - seconds) / 60; + let minutes = total_minutes % 60; + let total_hours = (total_minutes - minutes) / 60; + let hours = total_hours % 24; + let days = (total_hours - hours) / 24; + return `${days}d ${addZeros(hours)}:${addZeros(minutes)}:${addZeros(seconds)}`; +} + +function addZeros(x: number): string { + if (x >= 10) { + return '' + x; + } else { + return '0' + x; + } +} \ No newline at end of file diff --git a/tools/debug-ui/src/LatestBlocksView.scss b/tools/debug-ui/src/LatestBlocksView.scss new file mode 100644 index 00000000000..4fb3724a708 --- /dev/null +++ b/tools/debug-ui/src/LatestBlocksView.scss @@ -0,0 +1,126 @@ +.latest-blocks-view { + padding: 10px; + + .height-controller { + margin-bottom: 10px; + + .prompt { + font-weight: bold; + margin-right: 20px; + } + } + + .explanation { + color: gray; + } + + .missed-blocks, + .missed-chunks { + margin-top: 10px; + margin-bottom: 10px; + } + + button { + margin-right: 8px; + } + + table { + width: 100%; + border-collapse: collapse; + margin-top: 10px; + } + + table, + th, + td { + border: 1px solid black; + } + + td { + text-align: left; + padding: 8px; + vertical-align: middle; + } + + .graph-node-cell { + border: none; + text-align: center; + } + + th { + text-align: center; + vertical-align: middle; + padding: 8px; + background-color: lightgrey; + } + + .skipped-chunk { + background-color: lightgray; + } + + .hash-element { + font-family: monospace; + cursor: pointer; + } + + .validator-unavailable { + color: red; + } + + .error { + color: red; + white-space: pre; + } + + .not-on-canonical-chain { + color: gray; + } + + .missed-height { + color: gray; + } + + .hidden { + display: none; + } + + .graph-dot { + width: 12px; + height: 12px; + background-color: black; + border-radius: 100%; + display: inline-block; + } + + .graph-dot-col-1 { + margin-left: 24px; + } + + .graph-dot-col-0.graph-dot-total-2 { + margin-right: 24px; + } + + .not-on-canonical-chain .graph-dot { + background-color: gray; + } + + .head-label, + .header-head-label { + font-size: 10px; + font-family: Arial, Helvetica, sans-serif; + font-weight: bold; + color: white; + border-radius: 4px; + text-align: center; + padding: 4px; + margin-top: 2px; + } + + .head-label { + background-color: rgb(76, 208, 0); + } + + .header-head-label { + background-color: rgb(235, 215, 0); + } +} \ No newline at end of file diff --git a/tools/debug-ui/src/LatestBlocksView.tsx b/tools/debug-ui/src/LatestBlocksView.tsx new file mode 100644 index 00000000000..f9bd6bfd20e --- /dev/null +++ b/tools/debug-ui/src/LatestBlocksView.tsx @@ -0,0 +1,376 @@ +import React, { ReactElement, useCallback, useMemo, useState } from "react"; +import { useQuery } from "react-query"; +import Xarrow, { useXarrow, Xwrapper } from "react-xarrows"; +import { DebugBlockStatus, fetchBlockStatus, fetchFullStatus, MissedHeightInfo } from "./api"; +import './LatestBlocksView.scss'; + +function ellipsify(str: string, maxLen: number): string { + if (str.length > maxLen) { + return str.substring(0, maxLen - 3) + '...'; + } + return str; +} + +type HashElementProps = { + hashValue: string; + creator: string; + expandAll: boolean; + knownProducers: Set; +}; + +// Makes an element that when clicked, expands or ellipsifies the hash and creator. +const HashElement = ({ hashValue, creator, expandAll, knownProducers }: HashElementProps) => { + let [expanded, setExpanded] = useState(false); + let updateXarrow = useXarrow(); + return { + setExpanded((value) => !value); + // xarrows need to be updated whenever graph dot positions may change. + updateXarrow(); + }}> + {expanded || expandAll + ? `${hashValue} ${creator}` + : `${ellipsify(hashValue, 8)} ${ellipsify(creator, 13)}`} + ; +} + +type BlockTableRowBlock = { + block: DebugBlockStatus, + parentIndex: number | null, // the index of the parent block, or null if parent not included in the data + graphColumn: number | null, // the column to display the graph node in + blockDelay: number | null, // number of seconds since parent's block timestamp, or null if parent not included in the data + chunkSkipped: boolean[], // for each chunk, whether the chunk is the same as that chunk of parent block + isHead: boolean, + isHeaderHead: boolean, +}; +type BlockTableRow = BlockTableRowBlock | { missedHeight: MissedHeightInfo }; + +// Sorts the API response into easily displayable rows, and computes the graph layout. +function sortBlocksAndDetermineBlockGraphLayout( + blocks: DebugBlockStatus[], + missedHeights: MissedHeightInfo[], + head: string, + headerHead: string): BlockTableRow[] { + const rows: BlockTableRow[] = []; + for (let block of blocks) { + rows.push({ + block, + parentIndex: null, + graphColumn: -1, + blockDelay: null, + chunkSkipped: block.chunks.map(() => false), + isHead: head === block.block_hash, + isHeaderHead: headerHead === block.block_hash, + }); + } + for (let missedHeight of missedHeights) { + rows.push({ missedHeight }); + } + + function sortingKey(row: BlockTableRow) { + if ('block' in row) { + // some lousy tie-breaking for same-height rows. + return row.block.block_height + (row.block.block_timestamp / 1e12 % 1); + } else { + return row.missedHeight.block_height; + } + } + + rows.sort((a, b) => sortingKey(b) - sortingKey(a)); + + const rowIndexByHash = new Map(); + rows.forEach((row, rowIndex) => { + if ('block' in row) { + rowIndexByHash.set(row.block.block_hash, rowIndex); + } + }); + + let highestNodeOnFirstColumn = rows.length; + for (let i = rows.length - 1; i >= 0; i--) { + let row = rows[i]; + if ('missedHeight' in row) { + continue; + } + const block = row.block; + + // Look up parent index, and also compute things that depend on the parent block. + if (rowIndexByHash.has(block.prev_block_hash)) { + row.parentIndex = rowIndexByHash.get(block.prev_block_hash)!; + const parentBlock = (rows[row.parentIndex] as BlockTableRowBlock).block; + row.blockDelay = (block.block_timestamp - parentBlock.block_timestamp) / 1e9; + for (let j = 0; + j < Math.min(block.chunks.length, parentBlock.chunks.length); + j++) { + row.chunkSkipped[j] = + block.chunks[j].chunk_hash === parentBlock.chunks[j].chunk_hash; + } + } + // We'll use a two-column layout for the block graph. We traverse from bottom + // up (oldest block to latest), and for each row we pick the first column unless + // that would make us draw a line (from the parent to this node) through another + // node; in which case we would pick the second column. To do that we just need + // to keep track of the highest node we've seen so far for the first column. + // + // Not the best layout for a graph, but it's sufficient since we rarely have forks. + let column = 0; + if (row.parentIndex !== null && + (rows[row.parentIndex] as BlockTableRowBlock).graphColumn === 0 && + row.parentIndex > highestNodeOnFirstColumn) { + column = 1; + } else { + highestNodeOnFirstColumn = i; + } + row.graphColumn = column; + } + return rows; +} + +type BlocksTableProps = { + rows: BlockTableRow[], + knownProducers: Set, + expandAll: boolean, + hideMissingHeights: boolean, +} + +const BlocksTable = ({ rows, knownProducers, expandAll, hideMissingHeights }: BlocksTableProps) => { + let numGraphColumns = 1; // either 1 or 2; determines the width of leftmost td + let numShards = 0; + for (let row of rows) { + if ('block' in row) { + numGraphColumns = Math.max(numGraphColumns, (row.graphColumn || 0) + 1); + for (let chunk of row.block.chunks) { + numShards = Math.max(numShards, chunk.shard_id + 1); + } + } + } + const header = + Chain + Height + {'Hash & creator'} + Processing Time (ms) + Block Delay (s) + Gas price ratio + {[...Array(numShards).keys()].map(i => + Shard {i} (hash/gas(Tgas)/time(ms)))} + ; + + // One xarrow element per arrow (from block to block). + const graphArrows = [] as ReactElement[]; + + // One 'tr' element per row. + const tableRows = [] as ReactElement[]; + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if ('missedHeight' in row) { + if (!hideMissingHeights) { + tableRows.push( + + {row.missedHeight.block_height} + {row.missedHeight.block_producer} missed block + ); + } + continue; + } + let block = row.block; + + const chunkCells = [] as ReactElement[]; + block.chunks.forEach((chunk, shardId) => { + chunkCells.push( + + + + {(chunk.gas_used / (1024 * 1024 * 1024 * 1024)).toFixed(1)} + {chunk.processing_time_ms} + ); + }); + + tableRows.push( + + +
+
+ + + {block.block_height} + {row.isHead &&
HEAD
} + {row.isHeaderHead &&
HEADER HEAD
} + + + + + {block.processing_time_ms} + {row.blockDelay ?? ''} + {block.gas_price_ratio} + {block.full_block_missing && header only} + {chunkCells} + ); + if (row.parentIndex != null) { + graphArrows.push(); + } + } + return
+ {graphArrows} + + + {header} + {tableRows} + +
+
+} + +type LatestBlockViewProps = { + addr: string, +} + +export const LatestBlocksView = ({ addr }: LatestBlockViewProps) => { + const [height, setHeight] = React.useState(null); + const [heightInInput, setHeightInInput] = React.useState(''); + const [expandAll, setExpandAll] = React.useState(false); + const [hideMissingHeights, setHideMissingHeights] = React.useState(false); + const [showMissingChunksStats, setShowMissingChunksStats] = React.useState(false); + const updateXarrow = useXarrow(); + + const { data: status } = + useQuery(['fullStatus', addr], async () => await fetchFullStatus(addr)); + const { data: blockData, error, isLoading } = + useQuery(['latestBlocks', addr, height], async () => await fetchBlockStatus(addr, height)); + + const { rows, knownProducerSet } = useMemo(() => { + if (status && blockData) { + const knownProducerSet = new Set(); + for (const producer of status.detailed_debug_status!.network_info.known_producers) { + knownProducerSet.add(producer.account_id); + } + + const data = blockData.status_response.BlockStatus; + const rows = sortBlocksAndDetermineBlockGraphLayout( + data.blocks, + data.missed_heights, + data.head, + data.header_head); + return { rows, knownProducerSet }; + } + return { rows: [], knownProducerSet: new Set() }; + }, [status, blockData]); + + // Compute missing blocks and chunks statistics (whenever rows changes). + const { numCanonicalBlocks, canonicalHeightCount, numChunksSkipped } = React.useMemo(() => { + let firstCanonicalHeight = 0; + let lastCanonicalHeight = 0; + let numCanonicalBlocks = 0; + const numChunksSkipped = []; + for (const row of rows) { + if (!('block' in row)) { + continue; + } + const block = row.block; + if (!block.is_on_canonical_chain) { + continue; + } + if (firstCanonicalHeight === 0) { + firstCanonicalHeight = block.block_height; + } + lastCanonicalHeight = block.block_height; + numCanonicalBlocks++; + for (let i = 0; i < row.chunkSkipped.length; i++) { + while (numChunksSkipped.length < i + 1) { + numChunksSkipped.push(0); + } + if (row.chunkSkipped[i]) { + numChunksSkipped[i]++; + } + } + } + return { + numCanonicalBlocks, + canonicalHeightCount: firstCanonicalHeight - lastCanonicalHeight + 1, + numChunksSkipped, + }; + }, [rows]); + + const goToHeightCallback = useCallback(() => { + const height = parseInt(heightInInput); + setHeight(height); + }, [heightInInput]); + + return +
+
+ + {height == null ? 'Displaying most recent blocks' : `Displaying blocks from height ${height}`} + + setHeightInInput(e.target.value)} /> + + +
+
Skipped chunks have grey background.
+
+ Red text means that we don't know this producer + (it's not present in our announce account list). +
+ {!!error &&
{(error as Error).stack}
} +
+ Missing blocks: {canonicalHeightCount - numCanonicalBlocks} { } + Produced: {numCanonicalBlocks} { } + Missing Rate: {((canonicalHeightCount - numCanonicalBlocks) / canonicalHeightCount * 100).toFixed(2)}% +
+ + + + {showMissingChunksStats &&
+ {numChunksSkipped.map((numSkipped, shardId) => +
+ Shard {shardId}: Missing chunks: {numSkipped} { } + Produced: {numCanonicalBlocks - numSkipped} { } + Missing Rate: {(numSkipped / numCanonicalBlocks * 100).toFixed(2)}% +
)} +
} + {isLoading ? +
Loading...
: + + } +
+
; +}; diff --git a/tools/debug-ui/src/NetworkInfoView.scss b/tools/debug-ui/src/NetworkInfoView.scss new file mode 100644 index 00000000000..f05ba8d6d4d --- /dev/null +++ b/tools/debug-ui/src/NetworkInfoView.scss @@ -0,0 +1,7 @@ +.network-info-view { + padding: 10px; + + .error { + color: red; + } +} \ No newline at end of file diff --git a/tools/debug-ui/src/NetworkInfoView.tsx b/tools/debug-ui/src/NetworkInfoView.tsx new file mode 100644 index 00000000000..303234cf16c --- /dev/null +++ b/tools/debug-ui/src/NetworkInfoView.tsx @@ -0,0 +1,155 @@ +import { ReactElement, useMemo } from "react"; +import { useQuery } from "react-query"; +import { fetchEpochInfo, fetchFullStatus } from "./api"; +import './NetworkInfoView.scss'; + +function formatDurationInMillis(millis: number): string { + if (millis == null) { + return '(null)'; + } + let total_seconds = Math.floor(millis / 1000); + let hours = Math.floor(total_seconds / 3600) + let minutes = Math.floor((total_seconds - (hours * 3600)) / 60) + let seconds = total_seconds - (hours * 3600) - (minutes * 60) + if (hours > 0) { + if (minutes > 0) { + return `${hours}h ${minutes}m ${seconds}s` + } else { + return `${hours}h ${seconds}s` + } + } + if (minutes > 0) { + return `${minutes}m ${seconds}s` + } + return `${seconds}s` +} + +function formatBytesPerSecond(bytes_per_second: number): string { + if (bytes_per_second == null) { + return '-'; + } + if (bytes_per_second < 3000) { + return `${bytes_per_second} bps`; + } + let kilobytes_per_second = bytes_per_second / 1024; + if (kilobytes_per_second < 3000) { + return `${kilobytes_per_second.toFixed(1)} Kbps`; + } + let megabytes_per_second = kilobytes_per_second / 1024; + return `${megabytes_per_second.toFixed(1)} Mbps`; +} + +function formatTraffic(bytes_received: number, bytes_sent: number): ReactElement { + return
+
{"⬇ " + formatBytesPerSecond(bytes_received)}
+
{"⬆ " + formatBytesPerSecond(bytes_sent)}
+
; +} + +function addDebugPortLink(peer_addr: string): ReactElement { + // TODO: use new UI + return + {peer_addr} + ; +} + +function peerClass(current_height: number, peer_height: number): string { + if (peer_height > current_height + 5) { + return 'peer_ahead_alot'; + } + if (peer_height > current_height + 2) { + return 'peer_ahead'; + } + + if (peer_height < current_height - 100) { + return 'peer_far_behind'; + } + if (peer_height < current_height - 10) { + return 'peer_behind'; + } + if (peer_height < current_height - 3) { + return 'peer_behind_a_little'; + } + return 'peer_in_sync'; +} + +function getIntersection(setA: Set, setB: Set): Set { + const intersection = new Set( + [...setA].filter(element => setB.has(element)) + ); + + return intersection; +} + +function getDifference(setA: Set, setB: Set): Set { + return new Set( + [...setA].filter(element => !setB.has(element)) + ); +} + +type NetworkInfoViewProps = { + addr: string, +}; + +export const NetworkInfoView = ({ addr }: NetworkInfoViewProps) => { + const { data: fullStatus, error: fullStatusError, isLoading: fullStatusLoading } = + useQuery(['full_status', addr], () => fetchFullStatus(addr)); + const { data: epochInfo, error: epochInfoError, isLoading: epochInfoLoading } = + useQuery(['epoch_info', addr], () => fetchEpochInfo(addr)); + + const epochId = fullStatus?.sync_info.epoch_id; + const { blockProducers, chunkProducers, knownSet, reachableSet } = useMemo(() => { + if (fullStatus && epochInfo) { + const knownSet = new Set( + fullStatus.detailed_debug_status!.network_info.known_producers + .map((p) => p.account_id)); + const reachableSet = new Set( + fullStatus.detailed_debug_status!.network_info.known_producers + .filter((p) => (p.next_hops?.length ?? 0) > 0) + .map((p) => p.account_id)); + for (const oneEpoch of epochInfo.status_response.EpochInfo) { + if (oneEpoch.epoch_id === epochId) { + return { + blockProducers: new Set( + oneEpoch.block_producers.map(bp => bp.account_id)), + chunkProducers: new Set(oneEpoch.chunk_only_producers), + knownSet, + reachableSet, + } + } + } + } + return { + blockProducers: new Set(), + chunkProducers: new Set(), + knownSet: new Set(), + reachableSet: new Set(), + }; + }, [epochId, fullStatus, epochInfo]); + + if (fullStatusLoading || epochInfoLoading) { + return
Loading...
; + } + if (fullStatusError || epochInfoError) { + return
+
+ {((fullStatusError || epochInfoError) as Error).stack} +
+
; + } + if (!fullStatus || !epochInfo) { + return
+
No Data
+
; + } + + const detailedDebugStatus = fullStatus.detailed_debug_status!; + + return
+

Current Sync Status: {detailedDebugStatus.sync_status}

+

Number of peers: {detailedDebugStatus.network_info.num_connected_peers} / {detailedDebugStatus.network_info.peer_max_count}

+ +

TODO: migrate the rest

+
; +}; \ No newline at end of file diff --git a/tools/debug-ui/src/api.tsx b/tools/debug-ui/src/api.tsx new file mode 100644 index 00000000000..6eaaca0b008 --- /dev/null +++ b/tools/debug-ui/src/api.tsx @@ -0,0 +1,289 @@ + +export interface StatusResponse { + chain_id: string; + latest_protocol_version: number; + node_key: string; + node_public_key: string; + protocol_version: number; + rpc_addr?: string; + sync_info: StatusSyncInfo; + uptime_sec: number; + validator_account_id: string | null; + validator_public_key: string | null; + validators: ValidatorInfo[]; + version: BuildInfo; + detailed_debug_status: DetailedDebugStatus | null; +} + +export interface StatusSyncInfo { + earliest_block_hash: string; + earliest_block_height: number; + earliest_block_time: string; + epoch_id: string; + epoch_start_height: number; + latest_block_hash: string; + latest_block_height: number; + latest_block_time: string; + latest_state_root: string; + syncing: boolean; +} + +export interface ValidatorInfo { + account_id: string; + is_slashed: boolean; +} + +export interface BuildInfo { + build: string; + rustc_version: string; + version: string; +} + +export interface DetailedDebugStatus { + network_info: NetworkInfoView, + sync_status: String, + catchup_status: CatchupStatusView[], + current_head_status: BlockStatusView, + current_header_head_status: BlockStatusView, + block_production_delay_millis: number, +} + +export interface NetworkInfoView { + peer_max_count: number, + num_connected_peers: number, + connected_peers: PeerInfoView[], + known_producers: KnownProducerView[], + tier1_accounts_data?: AccountData[], + tier1_connections?: PeerInfoView[], +} + +export interface CatchupStatusView { + sync_block_hash: string, + sync_block_height: number, + shard_sync_status: { [shard_id: number]: string }, + blocks_to_catchup: BlockStatusView[], +} + +export interface BlockStatusView { + height: number, + hash: string, +} + +export interface PeerInfoView { + addr: string, + account_id: string | null, + height: number | null, + block_hash: string | null, + is_highest_block_invalid: boolean, + tracked_shards: number[], + archival: boolean, + peer_id: string, + received_bytes_per_sec: number, + sent_bytes_per_sec: number, + last_time_peer_requested_millis: number, + last_time_received_message_millis: number, + connection_established_time_millis: number, + is_outbound_peer: boolean, + nonce: number, +} + +export interface KnownProducerView { + account_id: string, + peer_id: string, + next_hops: string[] | null, +} + +export interface PeerAddr { + addr: string, + peer_id: string, +} + +export interface AccountData { + peer_id: string, + proxies: PeerAddr[], + account_key: string, + version: number, + timestamp: string, +} + +export type SyncStatusView = + 'AwaitingPeers' | + 'NoSync' | { + EpochSync: { epoch_ord: number } + } | { + HeaderSync: { + start_height: number, + current_height: number, + highest_height: number, + } + } | { + StateSync: [ + string, + { [shard_id: number]: ShardSyncDownloadView }, + ] + } | + 'StateSyncDone' | { + BodySync: { + start_height: number, + current_height: number, + highest_height: number, + } + }; + +export interface ShardSyncDownloadView { + downloads: { error: boolean, done: boolean }[]; + status: string; +} + +export interface DebugBlockStatusData { + blocks: DebugBlockStatus[], + missed_heights: MissedHeightInfo[], + head: string, + header_head: string, +} + +export interface DebugBlockStatus { + block_hash: string, + prev_block_hash: string, + block_height: number, + block_timestamp: number, + block_producer: string | null, + full_block_missing: boolean, + is_on_canonical_chain: boolean, + chunks: DebugChunkStatus[], + processing_time_ms?: number, + gas_price_ratio: number, +} + +export interface MissedHeightInfo { + block_height: number, + block_producer: string | null, +} + +export interface DebugChunkStatus { + shard_id: number, + chunk_hash: string, + chunk_producer: string | null, + gas_used: number, + processing_time_ms?: number, +} + +export interface EpochInfoView { + epoch_id: string, + height: number, + first_block: null | [string, string], + block_producers: ValidatorInfo[], + chunk_only_producers: string[], + validator_info: EpochValidatorInfo[], + protocol_version: number, + shard_size_and_parts: [number, number, boolean][], +} + +export interface EpochValidatorInfo { + current_validators: CurrentEpochValidatorInfo[], + next_validators: NextEpochValidatorInfo[], + current_fishermen: ValidatorStakeView[], + next_fishermen: ValidatorStakeView[], + current_proposals: ValidatorStakeView[], + prev_epoch_kickout: ValidatorKickoutView[], + epoch_start_height: number, + epoch_height: number, +} + +export interface CurrentEpochValidatorInfo { + account_id: string, + public_key: string, + is_slashed: boolean, + stake: string, + shards: number[], + num_produced_blocks: number, + num_expected_blocks: number, + num_produced_chunks: number, + num_expected_chunks: number, +} + +export interface NextEpochValidatorInfo { + account_id: string, + public_key: string, + stake: string, + shards: number[], +} + +export interface ValidatorStakeView { + V1: { + account_id: string, + public_key: string, + stake: string, + } +} + +export interface ValidatorKickoutView { + account_id: string, + reason: ValidatorKickoutReason, +} + +export type ValidatorKickoutReason = + 'Slashed' | + { NotEnoughBlocks: { produced: number, expected: number } } | + { NotEnoughChunks: { produced: number, expected: number } } | + 'Unstaked' | + { NotEnoughStake: { stake: string, threshold: string } } | + 'DidNotGetASeat'; + +export interface SyncStatusResponse { + status_response: { + SyncStatus: SyncStatusView, + }; +} + +export interface TrackedShardsResponse { + status_response: { + TrackedShards: { + shards_tracked_this_epoch: boolean[], + shards_tracked_next_epoch: boolean[], + }, + }; +} + +export interface BlockStatusResponse { + status_response: { + BlockStatus: DebugBlockStatusData, + } +} + +export interface EpochInfoResponse { + status_response: { + EpochInfo: EpochInfoView[], + } +} + +export async function fetchBasicStatus(addr: string): Promise { + const response = await fetch(`http://${addr}/status`); + return await response.json(); +} + +export async function fetchFullStatus(addr: string): Promise { + const response = await fetch(`http://${addr}/debug/api/status`); + return await response.json(); +} + +export async function fetchSyncStatus(addr: string): Promise { + const response = await fetch(`http://${addr}/debug/api/sync_status`); + return await response.json(); +} + +export async function fetchTrackedShards(addr: string): Promise { + const response = await fetch(`http://${addr}/debug/api/tracked_shards`); + return await response.json(); +} + +export async function fetchBlockStatus(addr: string, height: number | null): Promise { + const trailing = height ? `/${height}` : ''; + const response = await fetch(`http://${addr}/debug/api/block_status${trailing}`); + return await response.json(); +} + +export async function fetchEpochInfo(addr: string): Promise { + const response = await fetch(`http://${addr}/debug/api/epoch_info`); + return await response.json(); +} diff --git a/tools/debug-ui/src/index.css b/tools/debug-ui/src/index.css new file mode 100644 index 00000000000..3e3b6a19055 --- /dev/null +++ b/tools/debug-ui/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} \ No newline at end of file diff --git a/tools/debug-ui/src/index.tsx b/tools/debug-ui/src/index.tsx new file mode 100644 index 00000000000..278c7e10819 --- /dev/null +++ b/tools/debug-ui/src/index.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { App } from './App'; + +const root = ReactDOM.createRoot( + document.getElementById('root') as HTMLElement +); + +const queryClient = new QueryClient(); + +const router = createBrowserRouter([{ + // Root path redirects to the current host - this is useful if we serve the app from the node + // itself. + path: '/', + element: +}, { + path: '/:addr/*', + element: +}]); + +root.render( + + + + + +); diff --git a/tools/debug-ui/src/react-app-env.d.ts b/tools/debug-ui/src/react-app-env.d.ts new file mode 100644 index 00000000000..6431bc5fc6b --- /dev/null +++ b/tools/debug-ui/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tools/debug-ui/tsconfig.json b/tools/debug-ui/tsconfig.json new file mode 100644 index 00000000000..5086fb4cdc6 --- /dev/null +++ b/tools/debug-ui/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2015", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + }, + "include": [ + "src" + ] +} \ No newline at end of file From ae89cef6992be4490a81ca22caf5e3f97b24cafe Mon Sep 17 00:00:00 2001 From: mm-near <91919554+mm-near@users.noreply.github.com> Date: Mon, 19 Dec 2022 12:53:46 +0100 Subject: [PATCH 111/188] Migrate tests to use the new TestBlockBuilder (#8173) * updated store test * migrate remaining block test cases to use the new builder * cleanup warning * code fixes Co-authored-by: near-bulldozer[bot] <73298989+near-bulldozer[bot]@users.noreply.github.com> --- chain/chain/src/store.rs | 43 +++---- chain/chain/src/tests/challenges.rs | 20 ++-- chain/chain/src/tests/gc.rs | 2 +- chain/chain/src/tests/simple_chain.rs | 40 ++++--- chain/client/src/sync/header.rs | 22 +++- chain/client/src/sync/state.rs | 20 ++-- core/primitives/src/test_utils.rs | 111 +----------------- docs/practices/testing/test_utils.md | 8 ++ .../src/tests/client/process_blocks.rs | 23 ++-- 9 files changed, 97 insertions(+), 192 deletions(-) diff --git a/chain/chain/src/store.rs b/chain/chain/src/store.rs index 600ada7b166..261086d6fb5 100644 --- a/chain/chain/src/store.rs +++ b/chain/chain/src/store.rs @@ -2973,14 +2973,13 @@ impl<'a> ChainStoreUpdate<'a> { mod tests { use std::sync::Arc; - use near_primitives::merkle::PartialMerkleTree; - use near_chain_configs::{GCConfig, GenesisConfig}; use near_primitives::block::{Block, Tip}; use near_primitives::epoch_manager::block_info::BlockInfo; use near_primitives::errors::InvalidTxError; use near_primitives::hash::hash; use near_primitives::test_utils::create_test_signer; + use near_primitives::test_utils::TestBlockBuilder; use near_primitives::types::{BlockHeight, EpochId, NumBlocks}; use near_primitives::utils::index_to_bytes; use near_primitives::validator_signer::InMemoryValidatorSigner; @@ -3019,7 +3018,7 @@ mod tests { let mut chain = get_chain(); let genesis = chain.get_block_by_height(0).unwrap(); let signer = Arc::new(create_test_signer("test1")); - let short_fork = vec![Block::empty_with_height(&genesis, 1, &*signer)]; + let short_fork = vec![TestBlockBuilder::new(&genesis, signer.clone()).build()]; let mut store_update = chain.mut_store().store_update(); store_update.save_block_header(short_fork[0].header().clone()).unwrap(); store_update.commit().unwrap(); @@ -3037,7 +3036,7 @@ mod tests { let mut prev_block = genesis; for i in 1..(transaction_validity_period + 3) { let mut store_update = chain.mut_store().store_update(); - let block = Block::empty_with_height(&prev_block, i, &*signer.clone()); + let block = TestBlockBuilder::new(&prev_block, signer.clone()).height(i).build(); prev_block = block.clone(); store_update.save_block_header(block.header().clone()).unwrap(); store_update @@ -3077,7 +3076,7 @@ mod tests { let mut prev_block = genesis; for i in 1..(transaction_validity_period + 2) { let mut store_update = chain.mut_store().store_update(); - let block = Block::empty_with_height(&prev_block, i, &*signer.clone()); + let block = TestBlockBuilder::new(&prev_block, signer.clone()).height(i).build(); prev_block = block.clone(); store_update.save_block_header(block.header().clone()).unwrap(); store_update @@ -3096,11 +3095,10 @@ mod tests { transaction_validity_period ) .is_ok()); - let new_block = Block::empty_with_height( - blocks.last().unwrap(), - transaction_validity_period + 3, - &*signer, - ); + let new_block = TestBlockBuilder::new(&blocks.last().unwrap(), signer.clone()) + .height(transaction_validity_period + 3) + .build(); + let mut store_update = chain.mut_store().store_update(); store_update.save_block_header(new_block.header().clone()).unwrap(); store_update @@ -3127,7 +3125,7 @@ mod tests { let mut prev_block = genesis.clone(); for i in 1..(transaction_validity_period + 2) { let mut store_update = chain.mut_store().store_update(); - let block = Block::empty_with_height(&prev_block, i, &*signer.clone()); + let block = TestBlockBuilder::new(&prev_block, signer.clone()).height(i).build(); prev_block = block.clone(); store_update.save_block_header(block.header().clone()).unwrap(); short_fork.push(block); @@ -3147,7 +3145,7 @@ mod tests { let mut prev_block = genesis.clone(); for i in 1..(transaction_validity_period * 5) { let mut store_update = chain.mut_store().store_update(); - let block = Block::empty_with_height(&prev_block, i, &*signer.clone()); + let block = TestBlockBuilder::new(&prev_block, signer.clone()).height(i).build(); prev_block = block.clone(); store_update.save_block_header(block.header().clone()).unwrap(); long_fork.push(block); @@ -3169,7 +3167,7 @@ mod tests { let mut chain = get_chain(); let genesis = chain.get_block_by_height(0).unwrap(); let signer = Arc::new(create_test_signer("test1")); - let block1 = Block::empty_with_height(&genesis, 1, &*signer); + let block1 = TestBlockBuilder::new(&genesis, signer.clone()).build(); let mut block2 = block1.clone(); block2.mut_header().get_mut().inner_lite.epoch_id = EpochId(hash(&[1, 2, 3])); block2.mut_header().resign(&*signer); @@ -3257,7 +3255,7 @@ mod tests { let mut store_update = chain.mut_store().store_update(); let block = if next_epoch_id == *prev_block.header().next_epoch_id() { - Block::empty_with_height(&prev_block, height, &*signer) + TestBlockBuilder::new(&prev_block, signer.clone()).height(height).build() } else { let prev_hash = prev_block.hash(); let epoch_id = prev_block.header().next_epoch_id().clone(); @@ -3268,15 +3266,12 @@ mod tests { &prev_hash, ) .unwrap(); - Block::empty_with_epoch( - &prev_block, - height, - epoch_id, - next_epoch_id, - next_bp_hash, - &*signer, - &mut PartialMerkleTree::default(), - ) + TestBlockBuilder::new(&prev_block, signer.clone()) + .height(height) + .epoch_id(epoch_id) + .next_epoch_id(next_epoch_id) + .next_bp_hash(next_bp_hash) + .build() }; blocks.push(block.clone()); store_update.save_block(block.clone()); @@ -3377,7 +3372,7 @@ mod tests { store_update.commit().unwrap(); } for i in 1..1000 { - let block = Block::empty_with_height(&prev_block, i, &*signer.clone()); + let block = TestBlockBuilder::new(&prev_block, signer.clone()).height(i).build(); blocks.push(block.clone()); let mut store_update = chain.mut_store().store_update(); diff --git a/chain/chain/src/tests/challenges.rs b/chain/chain/src/tests/challenges.rs index 885eac89c76..5c4b4a6ea8a 100644 --- a/chain/chain/src/tests/challenges.rs +++ b/chain/chain/src/tests/challenges.rs @@ -1,7 +1,8 @@ use crate::test_utils::setup; -use crate::{Block, Error, Provenance}; +use crate::{Error, Provenance}; use assert_matches::assert_matches; use near_o11y::testonly::init_test_logger; +use near_primitives::test_utils::TestBlockBuilder; use near_primitives::time::Clock; use near_primitives::utils::MaybeValidated; @@ -15,7 +16,7 @@ fn challenges_new_head_prev() { for i in 0..5 { let prev_hash = *chain.head_header().unwrap().hash(); let prev = chain.get_block(&prev_hash).unwrap(); - let block = Block::empty(&prev, &*signer); + let block = TestBlockBuilder::new(&prev, signer.clone()).build(); hashes.push(*block.hash()); chain.process_block_test(&None, block).unwrap(); assert_eq!(chain.head().unwrap().height, i + 1); @@ -24,11 +25,12 @@ fn challenges_new_head_prev() { assert_eq!(chain.head().unwrap().height, 5); // The block to be added below after we invalidated fourth block. - let last_block = Block::empty(&chain.get_block(&hashes[3]).unwrap(), &*signer); + let last_block = + TestBlockBuilder::new(&chain.get_block(&hashes[3]).unwrap(), signer.clone()).build(); assert_eq!(last_block.header().height(), 5); let prev = chain.get_block(&hashes[1]).unwrap(); - let challenger_block = Block::empty_with_height(&prev, 3, &*signer); + let challenger_block = TestBlockBuilder::new(&prev, signer.clone()).height(3).build(); let challenger_hash = *challenger_block.hash(); let _ = chain.process_block_test(&None, challenger_block).unwrap(); @@ -64,11 +66,11 @@ fn challenges_new_head_prev() { assert_eq!(chain.head_header().unwrap().hash(), &hashes[2]); // Add two more blocks - let b3 = Block::empty(&chain.get_block(&hashes[2]).unwrap(), &*signer); + let b3 = TestBlockBuilder::new(&chain.get_block(&hashes[2]).unwrap(), signer.clone()).build(); let _ = chain.process_block_test(&None, b3.clone()).unwrap(); assert_eq!(&chain.head().unwrap().last_block_hash, b3.hash()); - let b4 = Block::empty(&b3, &*signer); + let b4 = TestBlockBuilder::new(&b3, signer.clone()).build(); chain.process_block_test(&None, b4.clone()).unwrap(); let new_head = chain.head().unwrap().last_block_hash; @@ -76,10 +78,10 @@ fn challenges_new_head_prev() { assert_eq!(chain.head_header().unwrap().hash(), &new_head); // Add two more blocks on an alternative chain - let b3 = Block::empty(&chain.get_block(&hashes[2]).unwrap(), &*signer); + let b3 = TestBlockBuilder::new(&chain.get_block(&hashes[2]).unwrap(), signer.clone()).build(); let _ = chain.process_block_test(&None, b3.clone()).unwrap(); - let b4 = Block::empty(&b3, &*signer); + let b4 = TestBlockBuilder::new(&b3, signer.clone()).build(); let _ = chain.process_block_test(&None, b4.clone()).unwrap(); let challenger_hash = b4.hash(); @@ -96,7 +98,7 @@ fn test_no_challenge_on_same_header() { let (mut chain, _, signer) = setup(); let prev_hash = *chain.head_header().unwrap().hash(); let prev = chain.get_block(&prev_hash).unwrap(); - let block = Block::empty(&prev, &*signer); + let block = TestBlockBuilder::new(&prev, signer.clone()).build(); chain.process_block_test(&None, block.clone()).unwrap(); assert_eq!(chain.head().unwrap().height, 1); let mut challenges = vec![]; diff --git a/chain/chain/src/tests/gc.rs b/chain/chain/src/tests/gc.rs index 65ac41102cb..5af11fe2c41 100644 --- a/chain/chain/src/tests/gc.rs +++ b/chain/chain/src/tests/gc.rs @@ -67,7 +67,7 @@ fn do_fork( .get_next_epoch_id_from_prev_block(prev_block.hash()) .expect("block must exist"); let block = if next_epoch_id == *prev_block.header().next_epoch_id() { - Block::empty(&prev_block, &*signer) + TestBlockBuilder::new(&prev_block, signer.clone()).build() } else { let prev_hash = prev_block.hash(); let epoch_id = prev_block.header().next_epoch_id().clone(); diff --git a/chain/chain/src/tests/simple_chain.rs b/chain/chain/src/tests/simple_chain.rs index 25d3110e8e6..a337d76d5e3 100644 --- a/chain/chain/src/tests/simple_chain.rs +++ b/chain/chain/src/tests/simple_chain.rs @@ -62,7 +62,7 @@ fn build_chain() { let prev_hash = *chain.head_header().unwrap().hash(); let prev = chain.get_block(&prev_hash).unwrap(); - let block = Block::empty(&prev, &*signer); + let block = TestBlockBuilder::new(&prev, signer.clone()).build(); chain.process_block_test(&None, block).unwrap(); assert_eq!(chain.head().unwrap().height, i as u64); } @@ -85,7 +85,7 @@ fn build_chain_with_orphans() { let (mut chain, _, signer) = setup(); let mut blocks = vec![chain.get_block(&chain.genesis().hash().clone()).unwrap()]; for i in 1..4 { - let block = Block::empty(&blocks[i - 1], &*signer); + let block = TestBlockBuilder::new(&blocks[i - 1], signer.clone()).build(); blocks.push(block); } let last_block = &blocks[blocks.len() - 1]; @@ -145,9 +145,9 @@ fn build_chain_with_skips_and_forks() { let b1 = TestBlockBuilder::new(&genesis, signer.clone()).build(); let b2 = TestBlockBuilder::new(&genesis, signer.clone()).height(2).build(); let b3 = TestBlockBuilder::new(&b1, signer.clone()).height(3).build(); - let b4 = Block::empty_with_height(&b2, 4, &*signer); - let b5 = Block::empty(&b4, &*signer); - let b6 = Block::empty(&b5, &*signer); + let b4 = TestBlockBuilder::new(&b2, signer.clone()).height(4).build(); + let b5 = TestBlockBuilder::new(&b4, signer.clone()).build(); + let b6 = TestBlockBuilder::new(&b5, signer.clone()).build(); assert!(chain.process_block_test(&None, b1).is_ok()); assert!(chain.process_block_test(&None, b2).is_ok()); assert!(chain.process_block_test(&None, b3.clone()).is_ok()); @@ -158,7 +158,7 @@ fn build_chain_with_skips_and_forks() { assert_eq!(chain.get_block_header_by_height(5).unwrap().height(), 5); assert_eq!(chain.get_block_header_by_height(6).unwrap().height(), 6); - let c4 = Block::empty_with_height(&b3, 4, &*signer); + let c4 = TestBlockBuilder::new(&b3, signer.clone()).height(4).build(); assert_eq!(chain.final_head().unwrap().height, 4); assert_matches!(chain.process_block_test(&None, c4), Err(Error::CannotBeFinalized)); } @@ -182,18 +182,20 @@ fn blocks_at_height() { init_test_logger(); let (mut chain, _, signer) = setup(); let genesis = chain.get_block_by_height(0).unwrap(); - let b_1 = Block::empty_with_height(&genesis, 1, &*signer); - let b_2 = Block::empty_with_height(&b_1, 2, &*signer); + let b_1 = TestBlockBuilder::new(&genesis, signer.clone()).height(1).build(); - let c_1 = Block::empty_with_height(&genesis, 1, &*signer); - let c_3 = Block::empty_with_height(&c_1, 3, &*signer); - let c_4 = Block::empty_with_height(&c_3, 4, &*signer); + let b_2 = TestBlockBuilder::new(&b_1, signer.clone()).height(2).build(); - let d_3 = Block::empty_with_height(&b_2, 3, &*signer); - let d_5 = Block::empty_with_height(&d_3, 5, &*signer); - let d_6 = Block::empty_with_height(&d_5, 6, &*signer); + let c_1 = TestBlockBuilder::new(&genesis, signer.clone()).height(1).build(); + let c_3 = TestBlockBuilder::new(&c_1, signer.clone()).height(3).build(); + let c_4 = TestBlockBuilder::new(&c_3, signer.clone()).height(4).build(); - let e_7 = Block::empty_with_height(&b_1, 7, &*signer); + let d_3 = TestBlockBuilder::new(&b_2, signer.clone()).height(3).build(); + + let d_5 = TestBlockBuilder::new(&d_3, signer.clone()).height(5).build(); + let d_6 = TestBlockBuilder::new(&d_5, signer.clone()).height(6).build(); + + let e_7 = TestBlockBuilder::new(&b_1, signer.clone()).height(7).build(); let b_1_hash = *b_1.hash(); let b_2_hash = *b_2.hash(); @@ -253,10 +255,10 @@ fn next_blocks() { init_test_logger(); let (mut chain, _, signer) = setup(); let genesis = chain.get_block(&chain.genesis().hash().clone()).unwrap(); - let b1 = Block::empty(&genesis, &*signer); - let b2 = Block::empty_with_height(&b1, 2, &*signer); - let b3 = Block::empty_with_height(&b1, 3, &*signer); - let b4 = Block::empty_with_height(&b3, 4, &*signer); + let b1 = TestBlockBuilder::new(&genesis, signer.clone()).build(); + let b2 = TestBlockBuilder::new(&b1, signer.clone()).height(2).build(); + let b3 = TestBlockBuilder::new(&b1, signer.clone()).height(3).build(); + let b4 = TestBlockBuilder::new(&b3, signer.clone()).height(4).build(); let b1_hash = *b1.hash(); let b2_hash = *b2.hash(); let b3_hash = *b3.hash(); diff --git a/chain/client/src/sync/header.rs b/chain/client/src/sync/header.rs index 07b9050eeb8..2d3e3f3330d 100644 --- a/chain/client/src/sync/header.rs +++ b/chain/client/src/sync/header.rs @@ -295,6 +295,7 @@ mod test { use near_network::test_utils::MockPeerManagerAdapter; use near_primitives::block::{Approval, Block, GenesisId}; use near_primitives::network::PeerId; + use near_primitives::test_utils::TestBlockBuilder; use super::*; use near_network::types::{BlockInfo, FullPeerInfo, PeerInfo}; @@ -358,7 +359,9 @@ mod test { let prev = chain.get_block(&chain.head().unwrap().last_block_hash).unwrap(); // Have gaps in the chain, so we don't have final blocks (i.e. last final block is // genesis). Otherwise we violate consensus invariants. - let block = Block::empty_with_height(&prev, prev.header().height() + 2, &*signer); + let block = TestBlockBuilder::new(&prev, signer.clone()) + .height(prev.header().height() + 2) + .build(); process_block_sync( &mut chain, &None, @@ -373,7 +376,9 @@ mod test { let prev = chain2.get_block(&chain2.head().unwrap().last_block_hash).unwrap(); // Have gaps in the chain, so we don't have final blocks (i.e. last final block is // genesis). Otherwise we violate consensus invariants. - let block = Block::empty_with_height(&prev, prev.header().height() + 2, &*signer2); + let block = TestBlockBuilder::new(&prev, signer2.clone()) + .height(prev.header().height() + 2) + .build(); process_block_sync( &mut chain2, &None, @@ -441,7 +446,7 @@ mod test { // Both chains share a common final block at height 3. for _ in 0..5 { let prev = chain.get_block(&chain.head().unwrap().last_block_hash).unwrap(); - let block = Block::empty(&prev, &*signer); + let block = TestBlockBuilder::new(&prev, signer.clone()).build(); process_block_sync( chain, &None, @@ -455,7 +460,9 @@ mod test { for _ in 0..7 { let prev = chain.get_block(&chain.head().unwrap().last_block_hash).unwrap(); // Test with huge gaps to make sure we are still able to find locators. - let block = Block::empty_with_height(&prev, prev.header().height() + 1000, &*signer); + let block = TestBlockBuilder::new(&prev, signer.clone()) + .height(prev.header().height() + 1000) + .build(); process_block_sync( &mut chain, &None, @@ -469,7 +476,9 @@ mod test { let prev = chain2.get_block(&chain2.head().unwrap().last_block_hash).unwrap(); // Test with huge gaps, but 3 blocks here produce a higher height than the 7 blocks // above. - let block = Block::empty_with_height(&prev, prev.header().height() + 3100, &*signer2); + let block = TestBlockBuilder::new(&prev, signer2.clone()) + .height(prev.header().height() + 3100) + .build(); process_block_sync( &mut chain2, &None, @@ -566,7 +575,8 @@ mod test { let mut all_blocks = vec![]; for i in 0..61 { let current_height = 3 + i * 5; - let block = Block::empty_with_height(last_block, current_height, &*signer); + let block = + TestBlockBuilder::new(last_block, signer.clone()).height(current_height).build(); all_blocks.push(block); last_block = &all_blocks[all_blocks.len() - 1]; } diff --git a/chain/client/src/sync/state.rs b/chain/client/src/sync/state.rs index cf46fda3818..1d53b2df8ec 100644 --- a/chain/client/src/sync/state.rs +++ b/chain/client/src/sync/state.rs @@ -892,13 +892,13 @@ mod test { use actix::System; use near_actix_test_utils::run_actix; - use near_chain::{test_utils::process_block_sync, Block, BlockProcessingArtifact, Provenance}; + use near_chain::{test_utils::process_block_sync, BlockProcessingArtifact, Provenance}; use near_epoch_manager::EpochManagerAdapter; use near_network::test_utils::MockPeerManagerAdapter; use near_primitives::{ - merkle::PartialMerkleTree, syncing::{ShardStateSyncResponseHeader, ShardStateSyncResponseV2}, + test_utils::TestBlockBuilder, types::EpochId, }; @@ -919,17 +919,13 @@ mod test { for _ in 0..(chain.epoch_length + 1) { let prev = chain.get_block(&chain.head().unwrap().last_block_hash).unwrap(); let block = if kv.is_next_block_epoch_start(prev.hash()).unwrap() { - Block::empty_with_epoch( - &prev, - prev.header().height() + 1, - prev.header().next_epoch_id().clone(), - EpochId { 0: *prev.hash() }, - *prev.header().next_bp_hash(), - &*signer, - &mut PartialMerkleTree::default(), - ) + TestBlockBuilder::new(&prev, signer.clone()) + .epoch_id(prev.header().next_epoch_id().clone()) + .next_epoch_id(EpochId { 0: *prev.hash() }) + .next_bp_hash(*prev.header().next_bp_hash()) + .build() } else { - Block::empty(&prev, &*signer) + TestBlockBuilder::new(&prev, signer.clone()).build() }; process_block_sync( diff --git a/core/primitives/src/test_utils.rs b/core/primitives/src/test_utils.rs index 0a6879fcfe8..718b3ff736e 100644 --- a/core/primitives/src/test_utils.rs +++ b/core/primitives/src/test_utils.rs @@ -17,7 +17,7 @@ use crate::transaction::{ DeployContractAction, FunctionCallAction, SignedTransaction, StakeAction, Transaction, TransferAction, }; -use crate::types::{AccountId, Balance, BlockHeight, EpochId, EpochInfoProvider, Gas, Nonce}; +use crate::types::{AccountId, Balance, EpochId, EpochInfoProvider, Gas, Nonce}; use crate::validator_signer::{InMemoryValidatorSigner, ValidatorSigner}; use crate::version::PROTOCOL_VERSION; @@ -437,115 +437,6 @@ impl Block { } } } - - pub fn empty_with_epoch( - prev: &Block, - height: BlockHeight, - epoch_id: EpochId, - next_epoch_id: EpochId, - next_bp_hash: CryptoHash, - signer: &dyn ValidatorSigner, - block_merkle_tree: &mut PartialMerkleTree, - ) -> Self { - block_merkle_tree.insert(*prev.hash()); - Self::empty_with_approvals( - prev, - height, - epoch_id, - next_epoch_id, - vec![], - signer, - next_bp_hash, - block_merkle_tree.root(), - ) - } - - pub fn empty_with_height( - prev: &Block, - height: BlockHeight, - signer: &dyn ValidatorSigner, - ) -> Self { - Self::empty_with_height_and_block_merkle_tree( - prev, - height, - signer, - &mut PartialMerkleTree::default(), - ) - } - - pub fn empty_with_height_and_block_merkle_tree( - prev: &Block, - height: BlockHeight, - signer: &dyn ValidatorSigner, - block_merkle_tree: &mut PartialMerkleTree, - ) -> Self { - Self::empty_with_epoch( - prev, - height, - prev.header().epoch_id().clone(), - if prev.header().prev_hash() == &CryptoHash::default() { - EpochId(*prev.hash()) - } else { - prev.header().next_epoch_id().clone() - }, - *prev.header().next_bp_hash(), - signer, - block_merkle_tree, - ) - } - - pub fn empty_with_block_merkle_tree( - prev: &Block, - signer: &dyn ValidatorSigner, - block_merkle_tree: &mut PartialMerkleTree, - ) -> Self { - Self::empty_with_height_and_block_merkle_tree( - prev, - prev.header().height() + 1, - signer, - block_merkle_tree, - ) - } - - pub fn empty(prev: &Block, signer: &dyn ValidatorSigner) -> Self { - Self::empty_with_block_merkle_tree(prev, signer, &mut PartialMerkleTree::default()) - } - - /// This is not suppose to be used outside of chain tests, because this doesn't refer to correct chunks. - /// Done because chain tests don't have a good way to store chunks right now. - pub fn empty_with_approvals( - prev: &Block, - height: BlockHeight, - epoch_id: EpochId, - next_epoch_id: EpochId, - approvals: Vec>, - signer: &dyn ValidatorSigner, - next_bp_hash: CryptoHash, - block_merkle_root: CryptoHash, - ) -> Self { - Block::produce( - PROTOCOL_VERSION, - PROTOCOL_VERSION, - prev.header(), - height, - prev.header().block_ordinal() + 1, - prev.chunks().iter().cloned().collect(), - epoch_id, - next_epoch_id, - None, - approvals, - Ratio::new(0, 1), - 0, - 0, - Some(0), - vec![], - vec![], - signer, - next_bp_hash, - block_merkle_root, - None, - ) - } } #[derive(Default)] diff --git a/docs/practices/testing/test_utils.md b/docs/practices/testing/test_utils.md index fe7932f49e5..6c7e86e97f2 100644 --- a/docs/practices/testing/test_utils.md +++ b/docs/practices/testing/test_utils.md @@ -26,6 +26,14 @@ This will create a signer for account 'test' using 'test' as a seed. let signer: InMemoryValidatorSigner = create_test_signer("test"); ``` +### Block + +Use ``TestBlockBuilder`` to create the block that you need. This class allows you to set custom values for most of the fields. + +```rust +let test_block = test_utils::TestBlockBuilder::new(prev, signer).height(33).build(); +``` + ## Store Use the in memory test store in tests: ```rust diff --git a/integration-tests/src/tests/client/process_blocks.rs b/integration-tests/src/tests/client/process_blocks.rs index 85574d81a8b..d56f1c06713 100644 --- a/integration-tests/src/tests/client/process_blocks.rs +++ b/integration-tests/src/tests/client/process_blocks.rs @@ -57,6 +57,7 @@ use near_primitives::sharding::{ use near_primitives::state_part::PartId; use near_primitives::syncing::{get_num_state_parts, ShardStateSyncResponseHeader, StatePartKey}; use near_primitives::test_utils::create_test_signer; +use near_primitives::test_utils::TestBlockBuilder; use near_primitives::transaction::{ Action, DeployContractAction, ExecutionStatus, FunctionCallAction, SignedTransaction, Transaction, @@ -1115,12 +1116,12 @@ fn test_time_attack() { chain_genesis, TEST_SEED, ); - let signer = create_test_signer("test1"); + let signer = Arc::new(create_test_signer("test1")); let genesis = client.chain.get_block_by_height(0).unwrap(); - let mut b1 = Block::empty_with_height(&genesis, 1, &signer); + let mut b1 = TestBlockBuilder::new(&genesis, signer.clone()).build(); b1.mut_header().get_mut().inner_lite.timestamp = to_timestamp(b1.header().timestamp() + chrono::Duration::seconds(60)); - b1.mut_header().resign(&signer); + b1.mut_header().resign(&*signer); let _ = client.process_block_test(b1.into(), Provenance::NONE).unwrap(); @@ -1149,9 +1150,9 @@ fn test_invalid_approvals() { chain_genesis, TEST_SEED, ); - let signer = create_test_signer("test1"); + let signer = Arc::new(create_test_signer("test1")); let genesis = client.chain.get_block_by_height(0).unwrap(); - let mut b1 = Block::empty_with_height(&genesis, 1, &signer); + let mut b1 = TestBlockBuilder::new(&genesis, signer.clone()).build(); b1.mut_header().get_mut().inner_rest.approvals = (0..100) .map(|i| { let account_id = AccountId::try_from(format!("test{}", i)).unwrap(); @@ -1161,7 +1162,7 @@ fn test_invalid_approvals() { ) }) .collect(); - b1.mut_header().resign(&signer); + b1.mut_header().resign(&*signer); let result = client.process_block_test(b1.into(), Provenance::NONE); assert_matches!(result.unwrap_err(), Error::InvalidApprovals); @@ -1195,11 +1196,11 @@ fn test_invalid_gas_price() { chain_genesis, TEST_SEED, ); - let signer = create_test_signer("test1"); + let signer = Arc::new(create_test_signer("test1")); let genesis = client.chain.get_block_by_height(0).unwrap(); - let mut b1 = Block::empty_with_height(&genesis, 1, &signer); + let mut b1 = TestBlockBuilder::new(&genesis, signer.clone()).build(); b1.mut_header().get_mut().inner_rest.gas_price = 0; - b1.mut_header().resign(&signer); + b1.mut_header().resign(&*signer); let res = client.process_block_test(b1.into(), Provenance::NONE); assert_matches!(res.unwrap_err(), Error::InvalidGasPrice); @@ -1210,8 +1211,8 @@ fn test_invalid_height_too_large() { let mut env = TestEnv::builder(ChainGenesis::test()).build(); let b1 = env.clients[0].produce_block(1).unwrap().unwrap(); let _ = env.clients[0].process_block_test(b1.clone().into(), Provenance::PRODUCED).unwrap(); - let signer = create_test_signer("test0"); - let b2 = Block::empty_with_height(&b1, u64::MAX, &signer); + let signer = Arc::new(create_test_signer("test0")); + let b2 = TestBlockBuilder::new(&b1, signer.clone()).height(u64::MAX).build(); let res = env.clients[0].process_block_test(b2.into(), Provenance::NONE); assert_matches!(res.unwrap_err(), Error::InvalidBlockHeight(_)); } From c630181cd1918c0bf741efef5a776300e1607ceb Mon Sep 17 00:00:00 2001 From: Andrei Kashin Date: Tue, 20 Dec 2022 09:08:13 +0000 Subject: [PATCH 112/188] refactor: Merge `Testbed` and `RuntimeTestbed` (#8242) As the separation was not bringing much value and made existing code more complex. See #8201 for full motivation. --- .../src/estimator_context.rs | 175 ++++++++++++++++-- runtime/runtime-params-estimator/src/lib.rs | 3 +- .../runtime-params-estimator/src/testbed.rs | 164 ---------------- 3 files changed, 156 insertions(+), 186 deletions(-) delete mode 100644 runtime/runtime-params-estimator/src/testbed.rs diff --git a/runtime/runtime-params-estimator/src/estimator_context.rs b/runtime/runtime-params-estimator/src/estimator_context.rs index cb34b7b04c8..47f0f8426f3 100644 --- a/runtime/runtime-params-estimator/src/estimator_context.rs +++ b/runtime/runtime-params-estimator/src/estimator_context.rs @@ -1,16 +1,21 @@ -use near_primitives::shard_layout::ShardUId; -use std::collections::HashMap; - -use near_primitives::transaction::SignedTransaction; -use near_store::{TrieCache, TrieCachingStorage, TrieConfig}; -use near_vm_logic::ExtCosts; - +use super::transaction_builder::TransactionBuilder; use crate::config::{Config, GasMetric}; use crate::gas_cost::GasCost; -use crate::testbed::RuntimeTestbed; use genesis_populate::get_account_id; - -use super::transaction_builder::TransactionBuilder; +use genesis_populate::state_dump::StateDump; +use near_primitives::receipt::Receipt; +use near_primitives::runtime::config_store::RuntimeConfigStore; +use near_primitives::runtime::migration_data::{MigrationData, MigrationFlags}; +use near_primitives::test_utils::MockEpochInfoProvider; +use near_primitives::transaction::{ExecutionStatus, SignedTransaction}; +use near_primitives::types::{Gas, MerkleHash}; +use near_primitives::version::PROTOCOL_VERSION; +use near_store::{ShardTries, ShardUId, Store, StoreCompiledContractCache}; +use near_store::{TrieCache, TrieCachingStorage, TrieConfig}; +use near_vm_logic::{ExtCosts, VMLimitConfig}; +use node_runtime::{ApplyState, Runtime}; +use std::collections::HashMap; +use std::sync::Arc; /// Global context shared by all cost calculating functions. pub(crate) struct EstimatorContext<'c> { @@ -44,11 +49,40 @@ impl<'c> EstimatorContext<'c> { } pub(crate) fn testbed(&mut self) -> Testbed<'_> { - let inner = - RuntimeTestbed::from_state_dump(&self.config.state_dump_path, self.config.in_memory_db); + // Copies dump from another directory and loads the state from it. + let workdir = tempfile::Builder::new().prefix("runtime_testbed").tempdir().unwrap(); + let StateDump { store, roots } = StateDump::from_dir( + &self.config.state_dump_path, + workdir.path(), + self.config.in_memory_db, + ); + // Ensure decent RocksDB SST file layout. + store.compact().expect("compaction failed"); + + assert!(roots.len() <= 1, "Parameter estimation works with one shard only."); + assert!(!roots.is_empty(), "No state roots found."); + let root = roots[0]; + + // Create ShardTries with relevant settings adjusted for estimator. + let shard_uids = [ShardUId { shard_id: 0, version: 0 }]; + let mut trie_config = near_store::TrieConfig::default(); + trie_config.enable_receipt_prefetching = true; + let tries = ShardTries::new( + store.clone(), + trie_config, + &shard_uids, + near_store::flat_state::FlatStateFactory::new(store.clone()), + ); + Testbed { config: self.config, - inner, + _workdir: workdir, + tries, + root, + runtime: Runtime::new(), + prev_receipts: Vec::new(), + apply_state: Self::make_apply_state(store.clone()), + epoch_info_provider: MockEpochInfoProvider::default(), transaction_builder: TransactionBuilder::new( (0..self.config.active_accounts) .map(|index| get_account_id(index as u64)) @@ -56,6 +90,49 @@ impl<'c> EstimatorContext<'c> { ), } } + + fn make_apply_state(store: Store) -> ApplyState { + let mut runtime_config = + 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 { + max_total_log_length: u64::MAX, + max_number_registers: u64::MAX, + max_gas_burnt: u64::MAX, + max_register_size: u64::MAX, + max_number_logs: u64::MAX, + + max_actions_per_receipt: u64::MAX, + max_promises_per_function_call_action: u64::MAX, + max_number_input_data_dependencies: u64::MAX, + + max_total_prepaid_gas: u64::MAX, + + ..VMLimitConfig::test() + }; + runtime_config.account_creation_config.min_allowed_top_level_account_length = 0; + + ApplyState { + // Put each runtime into a separate shard. + block_height: 1, + // Epoch length is long enough to avoid corner cases. + prev_block_hash: Default::default(), + block_hash: Default::default(), + epoch_id: Default::default(), + epoch_height: 0, + gas_price: 0, + block_timestamp: 0, + gas_limit: None, + random_seed: Default::default(), + current_protocol_version: PROTOCOL_VERSION, + config: Arc::new(runtime_config), + cache: Some(Box::new(StoreCompiledContractCache::new(&store))), + is_new_chunk: true, + migration_data: Arc::new(MigrationData::default()), + migration_flags: MigrationFlags::default(), + } + } } /// A single isolated instance of runtime. @@ -63,7 +140,14 @@ impl<'c> EstimatorContext<'c> { /// We use it to time processing a bunch of blocks. pub(crate) struct Testbed<'c> { pub(crate) config: &'c Config, - inner: RuntimeTestbed, + /// Directory where we temporarily keep the storage. + _workdir: tempfile::TempDir, + tries: ShardTries, + root: MerkleHash, + runtime: Runtime, + prev_receipts: Vec, + apply_state: ApplyState, + epoch_info_provider: MockEpochInfoProvider, transaction_builder: TransactionBuilder, } @@ -95,8 +179,8 @@ impl Testbed<'_> { let gas_cost = { self.clear_caches(); let start = GasCost::measure(self.config.metric); - self.inner.process_block(&block, allow_failures); - extra_blocks = self.inner.process_blocks_until_no_receipts(allow_failures); + self.process_block_impl(&block, allow_failures); + extra_blocks = self.process_blocks_until_no_receipts(allow_failures); start.elapsed() }; assert_eq!(block_latency, extra_blocks); @@ -115,13 +199,13 @@ impl Testbed<'_> { pub(crate) fn process_block(&mut self, block: Vec, block_latency: usize) { let allow_failures = false; - self.inner.process_block(&block, allow_failures); - let extra_blocks = self.inner.process_blocks_until_no_receipts(allow_failures); + self.process_block_impl(&block, allow_failures); + let extra_blocks = self.process_blocks_until_no_receipts(allow_failures); assert_eq!(block_latency, extra_blocks); } pub(crate) fn trie_caching_storage(&mut self) -> TrieCachingStorage { - let store = self.inner.store(); + let store = self.tries.get_store(); let is_view = false; let prefetcher = None; let caching_storage = TrieCachingStorage::new( @@ -136,7 +220,7 @@ impl Testbed<'_> { pub(crate) fn clear_caches(&mut self) { // Flush out writes hanging in memtable - self.inner.flush_db_write_buffer(); + self.tries.get_store().flush().unwrap(); // OS caches: // - only required in time based measurements, since ICount looks at syscalls directly. @@ -150,4 +234,55 @@ impl Testbed<'_> { panic!("Cannot drop OS caches on non-linux systems."); } } + + fn process_block_impl( + &mut self, + transactions: &[SignedTransaction], + allow_failures: bool, + ) -> Gas { + let apply_result = self + .runtime + .apply( + self.tries.get_trie_for_shard(ShardUId::single_shard(), self.root.clone()), + &None, + &self.apply_state, + &self.prev_receipts, + transactions, + &self.epoch_info_provider, + Default::default(), + ) + .unwrap(); + + let mut store_update = self.tries.store_update(); + self.root = self.tries.apply_all( + &apply_result.trie_changes, + ShardUId::single_shard(), + &mut store_update, + ); + store_update.commit().unwrap(); + self.apply_state.block_height += 1; + + let mut total_burnt_gas = 0; + if !allow_failures { + for outcome in &apply_result.outcomes { + total_burnt_gas += outcome.outcome.gas_burnt; + match &outcome.outcome.status { + ExecutionStatus::Failure(e) => panic!("Execution failed {:#?}", e), + _ => (), + } + } + } + self.prev_receipts = apply_result.outgoing_receipts; + total_burnt_gas + } + + /// Returns the number of blocks required to reach quiescence + fn process_blocks_until_no_receipts(&mut self, allow_failures: bool) -> usize { + let mut n = 0; + while !self.prev_receipts.is_empty() { + self.process_block_impl(&[], allow_failures); + n += 1; + } + n + } } diff --git a/runtime/runtime-params-estimator/src/lib.rs b/runtime/runtime-params-estimator/src/lib.rs index 14da7ea9144..a5335c6e67b 100644 --- a/runtime/runtime-params-estimator/src/lib.rs +++ b/runtime/runtime-params-estimator/src/lib.rs @@ -60,6 +60,7 @@ mod cost; mod cost_table; mod costs_to_runtime_config; +// Encapsulates the runtime so that it can be run separately from the rest of the node. mod estimator_context; mod gas_cost; mod qemu; @@ -74,8 +75,6 @@ pub mod utils; // Runs a VM (Default: Wasmer) on the given contract and measures the time it takes to do a single operation. pub mod vm_estimator; -// Encapsulates the runtime so that it can be run separately from the rest of the node. -pub mod testbed; // Prepares transactions and feeds them to the testbed in batches. Performs the warm up, takes care // of nonces. pub mod config; diff --git a/runtime/runtime-params-estimator/src/testbed.rs b/runtime/runtime-params-estimator/src/testbed.rs deleted file mode 100644 index ff21786b8ea..00000000000 --- a/runtime/runtime-params-estimator/src/testbed.rs +++ /dev/null @@ -1,164 +0,0 @@ -use genesis_populate::state_dump::StateDump; -use near_primitives::receipt::Receipt; -use near_primitives::runtime::config_store::RuntimeConfigStore; -use near_primitives::runtime::migration_data::{MigrationData, MigrationFlags}; -use near_primitives::test_utils::MockEpochInfoProvider; -use near_primitives::transaction::{ExecutionStatus, SignedTransaction}; -use near_primitives::types::{Gas, MerkleHash}; -use near_primitives::version::PROTOCOL_VERSION; -use near_store::{ShardTries, ShardUId, Store, StoreCompiledContractCache}; -use near_vm_logic::VMLimitConfig; -use node_runtime::{ApplyState, Runtime}; -use std::path::Path; -use std::sync::Arc; - -pub struct RuntimeTestbed { - /// Directory where we temporarily keep the storage. - _workdir: tempfile::TempDir, - tries: ShardTries, - root: MerkleHash, - runtime: Runtime, - prev_receipts: Vec, - apply_state: ApplyState, - epoch_info_provider: MockEpochInfoProvider, -} - -impl RuntimeTestbed { - /// Copies dump from another directory and loads the state from it. - pub fn from_state_dump(dump_dir: &Path, in_memory_db: bool) -> Self { - let workdir = tempfile::Builder::new().prefix("runtime_testbed").tempdir().unwrap(); - let StateDump { store, roots } = - StateDump::from_dir(dump_dir, workdir.path(), in_memory_db); - // Ensure decent RocksDB SST file layout. - store.compact().expect("compaction failed"); - - // Create ShardTries with relevant settings adjusted for estimator. - let shard_uids = [ShardUId { shard_id: 0, version: 0 }]; - let mut trie_config = near_store::TrieConfig::default(); - trie_config.enable_receipt_prefetching = true; - let tries = ShardTries::new( - store.clone(), - trie_config, - &shard_uids, - near_store::flat_state::FlatStateFactory::new(store.clone()), - ); - - assert!(roots.len() <= 1, "Parameter estimation works with one shard only."); - assert!(!roots.is_empty(), "No state roots found."); - let root = roots[0]; - - let mut runtime_config = - 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 { - max_total_log_length: u64::MAX, - max_number_registers: u64::MAX, - max_gas_burnt: u64::MAX, - max_register_size: u64::MAX, - max_number_logs: u64::MAX, - - max_actions_per_receipt: u64::MAX, - max_promises_per_function_call_action: u64::MAX, - max_number_input_data_dependencies: u64::MAX, - - max_total_prepaid_gas: u64::MAX, - - ..VMLimitConfig::test() - }; - runtime_config.account_creation_config.min_allowed_top_level_account_length = 0; - - let runtime = Runtime::new(); - let prev_receipts = vec![]; - - let apply_state = ApplyState { - // Put each runtime into a separate shard. - block_height: 1, - // Epoch length is long enough to avoid corner cases. - prev_block_hash: Default::default(), - block_hash: Default::default(), - epoch_id: Default::default(), - epoch_height: 0, - gas_price: 0, - block_timestamp: 0, - gas_limit: None, - random_seed: Default::default(), - current_protocol_version: PROTOCOL_VERSION, - config: Arc::new(runtime_config), - cache: Some(Box::new(StoreCompiledContractCache::new(&tries.get_store()))), - is_new_chunk: true, - migration_data: Arc::new(MigrationData::default()), - migration_flags: MigrationFlags::default(), - }; - - Self { - _workdir: workdir, - tries, - root, - runtime, - prev_receipts, - apply_state, - epoch_info_provider: MockEpochInfoProvider::default(), - } - } - - pub fn process_block( - &mut self, - transactions: &[SignedTransaction], - allow_failures: bool, - ) -> Gas { - let apply_result = self - .runtime - .apply( - self.tries.get_trie_for_shard(ShardUId::single_shard(), self.root.clone()), - &None, - &self.apply_state, - &self.prev_receipts, - transactions, - &self.epoch_info_provider, - Default::default(), - ) - .unwrap(); - - let mut store_update = self.tries.store_update(); - self.root = self.tries.apply_all( - &apply_result.trie_changes, - ShardUId::single_shard(), - &mut store_update, - ); - store_update.commit().unwrap(); - self.apply_state.block_height += 1; - - let mut total_burnt_gas = 0; - if !allow_failures { - for outcome in &apply_result.outcomes { - total_burnt_gas += outcome.outcome.gas_burnt; - match &outcome.outcome.status { - ExecutionStatus::Failure(e) => panic!("Execution failed {:#?}", e), - _ => (), - } - } - } - self.prev_receipts = apply_result.outgoing_receipts; - total_burnt_gas - } - - /// Returns the number of blocks required to reach quiescence - pub fn process_blocks_until_no_receipts(&mut self, allow_failures: bool) -> usize { - let mut n = 0; - while !self.prev_receipts.is_empty() { - self.process_block(&[], allow_failures); - n += 1; - } - n - } - - /// Flushes RocksDB memtable - pub fn flush_db_write_buffer(&mut self) { - self.tries.get_store().flush().unwrap(); - } - - pub fn store(&mut self) -> Store { - self.tries.get_store() - } -} From 263839d38dc0644caa06617df08240a83ed26e45 Mon Sep 17 00:00:00 2001 From: Andrei Kashin Date: Tue, 20 Dec 2022 12:36:01 +0000 Subject: [PATCH 113/188] doc: Fix typo in parameter estimator docs (#8248) Adds a missing "to" word. --- docs/architecture/gas/estimator.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/architecture/gas/estimator.md b/docs/architecture/gas/estimator.md index ea3b751c78a..70015e0a76b 100644 --- a/docs/architecture/gas/estimator.md +++ b/docs/architecture/gas/estimator.md @@ -5,7 +5,7 @@ benchmarking is not really commonly used term but I feel it describes it quite well. It measures the performance assuming that up to a third of validators and all users collude to make the system as slow as possible. -This benchmarking suite is used check that the gas parameters defined in the +This benchmarking suite is used to check that the gas parameters defined in the protocol are correct. Correct in this context means, a chunk filled with 1 Pgas will take at most 1 second to be applied. Or more generally, per 1 Tgas of execution, we spend no more than 1ms wall-clock time. @@ -125,4 +125,4 @@ in due time. - \ No newline at end of file + From 782ad6a390da123118e70cbb3eb111fe9155c679 Mon Sep 17 00:00:00 2001 From: Aleksandr Logunov Date: Tue, 20 Dec 2022 18:01:00 +0400 Subject: [PATCH 114/188] refactor: constructor for ShardSyncDownload (#8206) Co-authored-by: Akhilesh Singhania --- chain/client-primitives/src/types.rs | 23 +++++++++++++++++++++++ chain/client/src/sync/state.rs | 21 +++------------------ 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/chain/client-primitives/src/types.rs b/chain/client-primitives/src/types.rs index 6395829bc9e..f5023d8e2f8 100644 --- a/chain/client-primitives/src/types.rs +++ b/chain/client-primitives/src/types.rs @@ -150,12 +150,35 @@ impl StateSplitApplyingStatus { } } +/// Stores status of shard sync and statuses of downloading shards. #[derive(Clone, Debug)] pub struct ShardSyncDownload { + /// Stores all download statuses. If we are downloading state parts, its length equals the number of state parts. + /// Otherwise it is 1, since we have only one piece of data to download, like shard state header. pub downloads: Vec, pub status: ShardSyncStatus, } +impl ShardSyncDownload { + /// Creates a instance of self which includes initial statuses for shard sync and download at the given time. + pub fn new(now: DateTime) -> Self { + Self { + downloads: vec![ + DownloadStatus { + start_time: now, + prev_update_time: now, + run_me: Arc::new(AtomicBool::new(true)), + error: false, + done: false, + state_requests_count: 0, + last_target: None, + }; + 1 + ], + status: ShardSyncStatus::StateDownloadHeader, + } + } +} /// Various status sync can be in, whether it's fast sync or archival. #[derive(Clone, Debug, strum::AsRefStr)] pub enum SyncStatus { diff --git a/chain/client/src/sync/state.rs b/chain/client/src/sync/state.rs index 1d53b2df8ec..f65522745e3 100644 --- a/chain/client/src/sync/state.rs +++ b/chain/client/src/sync/state.rs @@ -179,21 +179,6 @@ impl StateSync { ) -> Result<(bool, bool), near_chain::Error> { let mut all_done = true; let mut update_sync_status = false; - let init_sync_download = ShardSyncDownload { - downloads: vec![ - DownloadStatus { - start_time: now, - prev_update_time: now, - run_me: Arc::new(AtomicBool::new(true)), - error: false, - done: false, - state_requests_count: 0, - last_target: None, - }; - 1 - ], - status: ShardSyncStatus::StateDownloadHeader, - }; let prev_hash = *chain.get_block_header(&sync_hash)?.prev_hash(); let prev_epoch_id = chain.get_block_header(&prev_hash)?.epoch_id().clone(); @@ -212,7 +197,7 @@ impl StateSync { let shard_sync_download = new_shard_sync.entry(shard_id).or_insert_with(|| { need_shard = true; update_sync_status = true; - init_sync_download.clone() + ShardSyncDownload::new(now) }); let old_status = shard_sync_download.status.clone(); let mut this_done = false; @@ -308,7 +293,7 @@ impl StateSync { // Cannot finalize the downloaded state. // The reasonable behavior here is to start from the very beginning. error!(target: "sync", "State sync finalizing error, shard = {}, hash = {}: {:?}", shard_id, sync_hash, e); - *shard_sync_download = init_sync_download.clone(); + *shard_sync_download = ShardSyncDownload::new(now); chain.clear_downloaded_parts(shard_id, sync_hash, state_num_parts)?; } } @@ -329,7 +314,7 @@ impl StateSync { // Cannot finalize the downloaded state. // The reasonable behavior here is to start from the very beginning. error!(target: "sync", "State sync finalizing error, shard = {}, hash = {}: {:?}", shard_id, sync_hash, e); - *shard_sync_download = init_sync_download.clone(); + *shard_sync_download = ShardSyncDownload::new(now); let shard_state_header = chain.get_state_header(shard_id, sync_hash)?; let state_num_parts = get_num_state_parts( From 250a27203effceac5adb5051fd044e8ee3fcc19f Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Tue, 20 Dec 2022 15:34:56 +0100 Subject: [PATCH 115/188] runtime: track registers memory usage rather than recomputing each time (#8237) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rather than recomputing registers memory usage from scratch each time register is set, track it in a Registers.usage field which is updated when needed. This changes an O(N) recalculation operation after each register write by an O(1) update. Note that there’s one observable change in behaviour. Previously, the register was inserted before memory usage limit has been checked. Even if the insert resulted in limit violation, the new value would be kept. With this commit, registers aren’t changed on any kind of error. Since errors result in smart contract termination, this change is fine since it won’t be obsarvable by the contract. --- runtime/near-vm-logic/src/vmstate.rs | 81 ++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/runtime/near-vm-logic/src/vmstate.rs b/runtime/near-vm-logic/src/vmstate.rs index 43a0317881c..c45deacf63a 100644 --- a/runtime/near-vm-logic/src/vmstate.rs +++ b/runtime/near-vm-logic/src/vmstate.rs @@ -6,6 +6,7 @@ use near_primitives_core::config::VMLimitConfig; use near_vm_errors::{HostError, VMLogicError}; use core::mem::size_of; +use std::collections::hash_map::Entry; type Result = ::std::result::Result; @@ -110,7 +111,18 @@ impl<'a> Memory<'a> { /// /// See documentation of [`Memory`] for more motivation for this struct. #[derive(Default, Clone)] -pub(super) struct Registers(std::collections::HashMap>); +pub(super) struct Registers { + /// Values of each existing register. + registers: std::collections::HashMap>, + + /// Total memory usage as counted for the purposes of the contract + /// execution. + /// + /// Usage of each register is counted as its value’s length plus eight + /// (i.e. size of `u64`). Total usage is sum over all registers. This only + /// approximates actual usage in memory. + total_memory_usage: u64, +} impl Registers { /// Returns register with given index. @@ -122,7 +134,7 @@ impl Registers { gas_counter: &mut GasCounter, register_id: u64, ) -> Result<&'s [u8]> { - if let Some(data) = self.0.get(®ister_id) { + if let Some(data) = self.registers.get(®ister_id) { gas_counter.pay_base(read_register_base)?; let len = u64::try_from(data.len()).map_err(|_| HostError::MemoryAccessViolation)?; gas_counter.pay_per(read_register_byte, len)?; @@ -134,7 +146,7 @@ impl Registers { /// Returns length of register with given index or None if no such register. pub(super) fn get_len(&self, register_id: u64) -> Option { - self.0.get(®ister_id).map(|data| data.len() as u64) + self.registers.get(®ister_id).map(|data| data.len() as u64) } /// Sets register with given index. @@ -155,30 +167,57 @@ impl Registers { u64::try_from(data.as_ref().len()).map_err(|_| HostError::MemoryAccessViolation)?; gas_counter.pay_base(write_register_base)?; gas_counter.pay_per(write_register_byte, data_len)?; + let entry = self.check_set_register(config, register_id, data_len)?; + let data = data.into(); + match entry { + Entry::Occupied(mut entry) => { + entry.insert(data); + } + Entry::Vacant(entry) => { + entry.insert(data); + } + }; + Ok(()) + } + + /// Checks and updates registers usage limits before setting given register + /// to value with given length. + /// + /// On success, returns Entry which must be used to insert the new value + /// into the registers. + fn check_set_register<'a>( + &'a mut self, + config: &VMLimitConfig, + register_id: u64, + data_len: u64, + ) -> Result>> { + if data_len > config.max_register_size { + return Err(HostError::MemoryAccessViolation.into()); + } // Fun fact: if we are at the limit and we replace a register, we’ll // fail even though we should be succeeding. This bug is now part of // the protocol so we can’t change it. - if data_len > config.max_register_size || self.0.len() as u64 >= config.max_number_registers - { + if self.registers.len() as u64 >= config.max_number_registers { return Err(HostError::MemoryAccessViolation.into()); } - match self.0.insert(register_id, data.into()) { - Some(old_value) if old_value.len() as u64 >= data_len => { - // If there was old value and it was no shorter than the new - // one, there’s no need to check new memory usage since it - // didn’t increase. - } - _ => { - // Calculate and check the new memory usage. - // TODO(mina86): Memorise usage in a field so we don’t have to - // go through all registers each time. - let usage: usize = self.0.values().map(|v| size_of::() + v.len()).sum(); - if usage as u64 > config.registers_memory_limit { - return Err(HostError::MemoryAccessViolation.into()); - } - } + + let entry = self.registers.entry(register_id); + let calc_usage = |len: u64| len + size_of::() as u64; + let old_mem_usage = match &entry { + Entry::Occupied(entry) => calc_usage(entry.get().len() as u64), + Entry::Vacant(_) => 0, + }; + let usage = self + .total_memory_usage + .checked_sub(old_mem_usage) + .unwrap() + .checked_add(calc_usage(data_len)) + .ok_or(HostError::MemoryAccessViolation)?; + if usage > config.registers_memory_limit { + return Err(HostError::MemoryAccessViolation.into()); } - Ok(()) + self.total_memory_usage = usage; + Ok(entry) } } From de7ef2731590ca2d36d237dc031c9a4f49facbbe Mon Sep 17 00:00:00 2001 From: Aleksandr Logunov Date: Tue, 20 Dec 2022 20:15:56 +0400 Subject: [PATCH 116/188] docs: more details about catchup (#8253) Here we describe how catchup works in more details - mention `DBCol::StateDlInfos`, `DBCol::BlocksToCatchup` and process of extracting blocks to catch up. --- docs/architecture/how/sync.md | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/architecture/how/sync.md b/docs/architecture/how/sync.md index 14dd8630780..f8eb72c9569 100644 --- a/docs/architecture/how/sync.md +++ b/docs/architecture/how/sync.md @@ -178,7 +178,9 @@ shard states for epoch T+1 are ready, processing new blocks only applies chunks for the shards that the node is tracking in epoch T. When the shard states for epoch T+1 finish downloading, the catchup process needs to reprocess the blocks that have already been processed in epoch T to apply the chunks for the -shards in epoch T+1. +shards in epoch T+1. We assume that it will be faster than regular block +processing, because blocks are not full and block production has its own delays, +so catchup can finish within an epoch. In other words, there are three modes for applying chunks and two code paths, either through the normal `process_block` (blue) or through `catchup_blocks` @@ -210,23 +212,27 @@ is caught up and if we need to download states. The logic works as follows: happen, because the node will appear stalled until the blocks in the previous epoch are catching up. * Otherwise, we start processing blocks for the new epoch T. For the first - block, we always consider it as not caught up and will initiate the process + block, we always mark it as not caught up and will initiate the process for downloading states for shards that we are going to care about in epoch - T+1. -* For other blocks, we consider it caught up if the previous block is caught up. -* `process_block` will apply chunks differently depending on whether the block - is caught up or not, as we discussed in `ApplyChunksMode`. + T+1. Info about downloading states is persisted in `DBCol::StateDlInfos`. +* For other blocks, we mark them as not caught up if the previous block is not + caught up. This info is persisted in `DBCol::BlocksToCatchup` which stores + mapping from previous block to vector of all child blocks to catch up. +* Chunks for already tracked shards will be applied during `process_block`, as + we said before mentioning `ApplyChunksMode`. +* Once we downloaded state, we start catchup. It will take blocks from + `DBCol::BlocksToCatchup` in breadth-first search order and apply chunks for + shards which have to be tracked in the next epoch. +* When catchup doesn't see any more blocks to process, `DBCol::BlocksToCatchup` + is cleared, which means that catchup process is finished. -The information of which blocks need to catch up (`add_block_to_catch_up`) and -which new states need to be downloaded (`add_state_dl_info`) are stored in -storage to be persisted across different runs. The catchup process is implemented through the function `Client::run_catchup`. `ClientActor` schedules a call to `run_catchup` every 100ms. However, the call can be delayed if ClientActor has a lot of messages in its actix queue. -Every time `run_catchup` is called, it checks the store to see if there are any -shard states that should be downloaded (`iterate_state_sync_infos`). If so, it +Every time `run_catchup` is called, it checks `DBCol::StateDlInfos` to see +if there are any shard states that should be downloaded. If so, it initiates the syncing process for these shards. After the state is downloaded, `run_catchup` will start to apply blocks that need to be caught up. From 7919144431e12758e3d24e48d39686e7d12cc2a6 Mon Sep 17 00:00:00 2001 From: wacban Date: Tue, 20 Dec 2022 19:06:56 +0000 Subject: [PATCH 117/188] [feat] Adding save_trie_changes config (#8218) * [feat] Adding save_trie_changes config --- chain/chain/src/store.rs | 4 +- chain/client/src/client.rs | 2 +- chain/client/src/info.rs | 2 +- chain/client/src/test_utils.rs | 40 +++++++++- chain/client/src/view_client.rs | 2 +- core/chain-configs/src/client_config.rs | 12 +++ .../src/tests/client/process_blocks.rs | 76 ++++++++++++++++++- integration-tests/src/tests/network/runner.rs | 2 +- nearcore/src/config.rs | 12 ++- tools/mirror/src/offline.rs | 2 +- tools/mock-node/src/setup.rs | 7 +- tools/speedy_sync/src/main.rs | 2 +- tools/state-viewer/src/commands.rs | 25 +++--- tools/state-viewer/src/dump_state_parts.rs | 2 +- 14 files changed, 159 insertions(+), 31 deletions(-) diff --git a/chain/chain/src/store.rs b/chain/chain/src/store.rs index 261086d6fb5..2b3e64df95d 100644 --- a/chain/chain/src/store.rs +++ b/chain/chain/src/store.rs @@ -361,7 +361,9 @@ pub struct ChainStore { block_ordinal_to_hash: CellLruCache, CryptoHash>, /// Processed block heights. processed_block_heights: CellLruCache, ()>, - /// Is this a non-archival node that needs to store to DBCol::TrieChanges? + /// Should this node store trie changes? Must be set to true if either of the following is true + /// - archive is false - non archival nodes need trie changes for garbage collection + /// - the node will be migrated to split storage in the near future - split storage nodes need trie changes for hot storage garbage collection save_trie_changes: bool, } diff --git a/chain/client/src/client.rs b/chain/client/src/client.rs index 81b9cf02c7e..618fb20de3d 100644 --- a/chain/client/src/client.rs +++ b/chain/client/src/client.rs @@ -192,7 +192,7 @@ impl Client { &chain_genesis, doomslug_threshold_mode, ChainConfig { - save_trie_changes: !config.archive, + save_trie_changes: config.save_trie_changes, background_migration_threads: config.client_background_migration_threads, }, )?; diff --git a/chain/client/src/info.rs b/chain/client/src/info.rs index 64e822b539b..53ba1237ec7 100644 --- a/chain/client/src/info.rs +++ b/chain/client/src/info.rs @@ -519,7 +519,7 @@ mod tests { #[test] fn telemetry_info() { - let config = ClientConfig::test(false, 1230, 2340, 50, false, true); + let config = ClientConfig::test(false, 1230, 2340, 50, false, true, true); let info_helper = InfoHelper::new(None, &config, None); let store = near_store::test_utils::create_test_store(); diff --git a/chain/client/src/test_utils.rs b/chain/client/src/test_utils.rs index 957e9c354e7..3282fe85ff4 100644 --- a/chain/client/src/test_utils.rs +++ b/chain/client/src/test_utils.rs @@ -223,7 +223,7 @@ pub fn setup( runtime.clone(), &chain_genesis, doomslug_threshold_mode, - ChainConfig { save_trie_changes: !archive, background_migration_threads: 1 }, + ChainConfig { save_trie_changes: true, background_migration_threads: 1 }, ) .unwrap(); let genesis_block = chain.get_block(&chain.genesis().hash().clone()).unwrap(); @@ -236,6 +236,7 @@ pub fn setup( max_block_prod_time, num_validator_seats, archive, + true, epoch_sync_enabled, ); @@ -309,7 +310,7 @@ pub fn setup_only_view( runtime.clone(), &chain_genesis, doomslug_threshold_mode, - ChainConfig { save_trie_changes: !archive, background_migration_threads: 1 }, + ChainConfig { save_trie_changes: true, background_migration_threads: 1 }, ) .unwrap(); @@ -321,6 +322,7 @@ pub fn setup_only_view( max_block_prod_time, num_validator_seats, archive, + true, epoch_sync_enabled, ); @@ -1090,10 +1092,13 @@ pub fn setup_client_with_runtime( chain_genesis: ChainGenesis, runtime_adapter: Arc, rng_seed: RngSeed, + archive: bool, + save_trie_changes: bool, ) -> Client { let validator_signer = account_id.map(|x| Arc::new(create_test_signer(x.as_str())) as Arc); - let mut config = ClientConfig::test(true, 10, 20, num_validator_seats, false, true); + let mut config = + ClientConfig::test(true, 10, 20, num_validator_seats, archive, save_trie_changes, true); config.epoch_length = chain_genesis.epoch_length; let mut client = Client::new( config, @@ -1119,6 +1124,8 @@ pub fn setup_client( client_adapter: Arc, chain_genesis: ChainGenesis, rng_seed: RngSeed, + archive: bool, + save_trie_changes: bool, ) -> Client { let num_validator_seats = vs.all_block_producers().count() as NumSeats; let runtime_adapter = @@ -1132,6 +1139,8 @@ pub fn setup_client( chain_genesis, runtime_adapter, rng_seed, + archive, + save_trie_changes, ) } @@ -1148,6 +1157,8 @@ pub struct TestEnv { // random seed to be inject in each client according to AccountId // if not set, a default constant TEST_SEED will be injected seeds: HashMap, + archive: bool, + save_trie_changes: bool, } /// A builder for the TestEnv structure. @@ -1160,6 +1171,8 @@ pub struct TestEnvBuilder { // random seed to be inject in each client according to AccountId // if not set, a default constant TEST_SEED will be injected seeds: HashMap, + archive: bool, + save_trie_changes: bool, } /// Builder for the [`TestEnv`] structure. @@ -1176,6 +1189,8 @@ impl TestEnvBuilder { runtime_adapters: None, network_adapters: None, seeds, + archive: false, + save_trie_changes: true, } } @@ -1238,6 +1253,16 @@ impl TestEnvBuilder { self } + pub fn archive(mut self, archive: bool) -> Self { + self.archive = archive; + self + } + + pub fn save_trie_changes(mut self, save_trie_changes: bool) -> Self { + self.save_trie_changes = save_trie_changes; + self + } + /// Constructs new `TestEnv` structure. /// /// If no clients were configured (either through count or vector) one @@ -1282,11 +1307,14 @@ impl TestEnvBuilder { client_adapter.clone(), chain_genesis.clone(), rng_seed, + self.archive, + self.save_trie_changes, ) }) .collect(), Some(runtime_adapters) => { assert!(clients.len() == runtime_adapters.len()); + clients .into_iter() .zip((&network_adapters).iter()) @@ -1305,6 +1333,8 @@ impl TestEnvBuilder { chain_genesis.clone(), runtime_adapter, rng_seed, + self.archive, + self.save_trie_changes, ) }) .collect() @@ -1325,6 +1355,8 @@ impl TestEnvBuilder { .collect(), paused_blocks: Default::default(), seeds, + archive: self.archive, + save_trie_changes: self.save_trie_changes, } } @@ -1624,6 +1656,8 @@ impl TestEnv { self.client_adapters[idx].clone(), self.chain_genesis.clone(), rng_seed, + self.archive, + self.save_trie_changes, ) } diff --git a/chain/client/src/view_client.rs b/chain/client/src/view_client.rs index bcf4e661088..7e743dcf5fd 100644 --- a/chain/client/src/view_client.rs +++ b/chain/client/src/view_client.rs @@ -130,7 +130,7 @@ impl ViewClientActor { runtime_adapter.clone(), chain_genesis, DoomslugThresholdMode::TwoThirds, - !config.archive, + config.save_trie_changes, )?; Ok(ViewClientActor { adv, diff --git a/core/chain-configs/src/client_config.rs b/core/chain-configs/src/client_config.rs index dee06c4b7d8..e4f198ba304 100644 --- a/core/chain-configs/src/client_config.rs +++ b/core/chain-configs/src/client_config.rs @@ -142,6 +142,10 @@ pub struct ClientConfig { pub tracked_shards: Vec, /// Not clear old data, set `true` for archive nodes. pub archive: bool, + /// Save trie changes. Must be set to true if either of the following is true + /// - archive is false - non archival nodes need trie changes for garbage collection + /// - the node will be migrated to split storage in the near future - split storage nodes need trie changes for hot storage garbage collection + pub save_trie_changes: bool, /// Number of threads for ViewClientActor pool. pub view_client_threads: usize, /// Run Epoch Sync on the start. @@ -167,8 +171,15 @@ impl ClientConfig { max_block_prod_time: u64, num_block_producer_seats: NumSeats, archive: bool, + save_trie_changes: bool, epoch_sync_enabled: bool, ) -> Self { + assert!( + archive || save_trie_changes, + "Configuration with archive = false and save_trie_changes = false is not supported \ + because non-archival nodes must save trie changes in order to do do garbage collection." + ); + ClientConfig { version: Default::default(), chain_id: "unittest".to_string(), @@ -210,6 +221,7 @@ impl ClientConfig { tracked_accounts: vec![], tracked_shards: vec![], archive, + save_trie_changes, log_summary_style: LogSummaryStyle::Colored, view_client_threads: 1, epoch_sync_enabled, diff --git a/integration-tests/src/tests/client/process_blocks.rs b/integration-tests/src/tests/client/process_blocks.rs index d56f1c06713..5aa59bd3b12 100644 --- a/integration-tests/src/tests/client/process_blocks.rs +++ b/integration-tests/src/tests/client/process_blocks.rs @@ -49,7 +49,7 @@ use near_primitives::merkle::{verify_hash, PartialMerkleTree}; use near_primitives::receipt::DelayedReceiptIndices; use near_primitives::runtime::config::RuntimeConfig; use near_primitives::runtime::config_store::RuntimeConfigStore; -use near_primitives::shard_layout::ShardUId; +use near_primitives::shard_layout::{get_block_shard_uid, ShardUId}; use near_primitives::sharding::{ EncodedShardChunk, ReedSolomonWrapper, ShardChunkHeader, ShardChunkHeaderInner, ShardChunkHeaderV3, @@ -72,7 +72,7 @@ use near_primitives::views::{ BlockHeaderView, FinalExecutionStatus, QueryRequest, QueryResponseKind, }; use near_store::test_utils::create_test_store; -use near_store::{get, DBCol}; +use near_store::{get, DBCol, TrieChanges}; use nearcore::config::{GenesisExt, TESTING_INIT_BALANCE, TESTING_INIT_STAKE}; use nearcore::NEAR_BASE; use rand::prelude::StdRng; @@ -1115,6 +1115,8 @@ fn test_time_attack() { client_adapter, chain_genesis, TEST_SEED, + false, + true, ); let signer = Arc::new(create_test_signer("test1")); let genesis = client.chain.get_block_by_height(0).unwrap(); @@ -1149,6 +1151,8 @@ fn test_invalid_approvals() { client_adapter, chain_genesis, TEST_SEED, + false, + true, ); let signer = Arc::new(create_test_signer("test1")); let genesis = client.chain.get_block_by_height(0).unwrap(); @@ -1195,6 +1199,8 @@ fn test_invalid_gas_price() { client_adapter, chain_genesis, TEST_SEED, + false, + true, ); let signer = Arc::new(create_test_signer("test1")); let genesis = client.chain.get_block_by_height(0).unwrap(); @@ -1354,6 +1360,8 @@ fn test_bad_chunk_mask() { Arc::new(MockClientAdapterForShardsManager::default()), chain_genesis.clone(), TEST_SEED, + false, + true, ) }) .collect(); @@ -1478,6 +1486,68 @@ fn test_gc_with_epoch_length() { } } +/// Test that producing blocks works in archival mode with save_trie_changes enabled. +/// In that case garbage collection should not happen but trie changes should be saved to the store. +#[test] +fn test_archival_save_trie_changes() { + let epoch_length = 10; + + 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) + .runtime_adapters(create_nightshade_runtimes(&genesis, 1)) + .archive(true) + .save_trie_changes(true) + .build(); + let mut blocks = vec![]; + let genesis_block = env.clients[0].chain.get_block_by_height(0).unwrap(); + blocks.push(genesis_block); + for i in 1..=epoch_length * (DEFAULT_GC_NUM_EPOCHS_TO_KEEP + 1) { + let block = env.clients[0].produce_block(i).unwrap().unwrap(); + env.process_block(0, block.clone(), Provenance::PRODUCED); + blocks.push(block); + } + // Go through all of the blocks and verify that the block are stored and that the trie changes were stored too. + for i in 0..=epoch_length * (DEFAULT_GC_NUM_EPOCHS_TO_KEEP + 1) { + let client = &env.clients[0]; + let chain = &client.chain; + let store = chain.store(); + let block = &blocks[i as usize]; + let header = block.header(); + let epoch_id = header.epoch_id(); + let runtime_adapter = &client.runtime_adapter; + let shard_layout = runtime_adapter.get_shard_layout(epoch_id).unwrap(); + + assert!(chain.get_block(block.hash()).is_ok()); + assert!(chain.get_block_by_height(i).is_ok()); + assert!(chain.store().get_all_block_hashes_by_height(i as BlockHeight).is_ok()); + + // The genesis block does not contain trie changes. + if i == 0 { + continue; + } + + // Go through chunks and test that trie changes were correctly saved to the store. + let chunks = block.chunks(); + for chunk in chunks.iter() { + let shard_id = chunk.shard_id() as u32; + let version = shard_layout.version(); + + let shard_uid = ShardUId { version, shard_id }; + let key = get_block_shard_uid(&block.hash(), &shard_uid); + let trie_changes: Option = + store.store().get_ser(DBCol::TrieChanges, &key).unwrap(); + + if let Some(trie_changes) = trie_changes { + // We don't do any transactions in this test so the root should remain unchanged. + assert_eq!(trie_changes.old_root, trie_changes.new_root); + } + } + } +} + /// When an epoch is very long there should not be anything garbage collected unexpectedly #[test] fn test_gc_long_epoch() { @@ -1944,7 +2014,7 @@ fn test_incorrect_validator_key_produce_block() { KeyType::ED25519, "seed", )); - let mut config = ClientConfig::test(true, 10, 20, 2, false, true); + let mut config = ClientConfig::test(true, 10, 20, 2, false, true, true); config.epoch_length = chain_genesis.epoch_length; let mut client = Client::new( config, diff --git a/integration-tests/src/tests/network/runner.rs b/integration-tests/src/tests/network/runner.rs index e759a2a9633..622948edcff 100644 --- a/integration-tests/src/tests/network/runner.rs +++ b/integration-tests/src/tests/network/runner.rs @@ -56,7 +56,7 @@ fn setup_network_node( let telemetry_actor = TelemetryActor::new(TelemetryConfig::default()).start(); let db = store.into_inner(near_store::Temperature::Hot); - let mut client_config = ClientConfig::test(false, 100, 200, num_validators, false, true); + let mut client_config = ClientConfig::test(false, 100, 200, num_validators, false, true, true); client_config.archive = config.archive; client_config.ttl_account_id_router = config.ttl_account_id_router.try_into().unwrap(); let genesis_block = Chain::make_genesis_block(&*runtime, &chain_genesis).unwrap(); diff --git a/nearcore/src/config.rs b/nearcore/src/config.rs index 1dc88a5ccd3..93582c8d1dd 100644 --- a/nearcore/src/config.rs +++ b/nearcore/src/config.rs @@ -308,6 +308,7 @@ pub struct Config { pub tracked_shards: Vec, #[serde(skip_serializing_if = "is_false")] pub archive: bool, + pub save_trie_changes: bool, pub log_summary_style: LogSummaryStyle, /// Garbage collection configuration. #[serde(default, flatten)] @@ -363,6 +364,7 @@ impl Default for Config { tracked_accounts: vec![], tracked_shards: vec![], archive: false, + save_trie_changes: true, log_summary_style: LogSummaryStyle::Colored, gc: GCConfig::default(), epoch_sync_enabled: true, @@ -384,7 +386,7 @@ impl Config { let contents = std::fs::read_to_string(path) .with_context(|| format!("Failed to read config from {}", path.display()))?; let mut unrecognised_fields = Vec::new(); - let config = serde_ignored::deserialize( + let config: Config = serde_ignored::deserialize( &mut serde_json::Deserializer::from_str(&contents), |field| { let field = field.to_string(); @@ -411,6 +413,13 @@ impl Config { path.display(), ); } + + assert!( + config.archive || config.save_trie_changes, + "Configuration with archive = false and save_trie_changes = false is not supported \ + because non-archival nodes must save trie changes in order to do do garbage collection." + ); + Ok(config) } @@ -598,6 +607,7 @@ impl NearConfig { tracked_accounts: config.tracked_accounts, tracked_shards: config.tracked_shards, archive: config.archive, + save_trie_changes: config.save_trie_changes, log_summary_style: config.log_summary_style, gc: config.gc, view_client_threads: config.view_client_threads, diff --git a/tools/mirror/src/offline.rs b/tools/mirror/src/offline.rs index 0eb7d543abf..b805314e9ec 100644 --- a/tools/mirror/src/offline.rs +++ b/tools/mirror/src/offline.rs @@ -45,7 +45,7 @@ impl ChainAccess { let chain = ChainStore::new( store.clone(), config.genesis.config.genesis_height, - !config.client_config.archive, + config.client_config.save_trie_changes, ); let runtime = NightshadeRuntime::from_config(home.as_ref(), store, &config); Ok(Self { chain, runtime }) diff --git a/tools/mock-node/src/setup.rs b/tools/mock-node/src/setup.rs index b2e31af10e6..f95fbd1a3a3 100644 --- a/tools/mock-node/src/setup.rs +++ b/tools/mock-node/src/setup.rs @@ -120,12 +120,12 @@ pub fn setup_mock_node( let mut chain_store = ChainStore::new( client_runtime.store().clone(), config.genesis.config.genesis_height, - !config.client_config.archive, + config.client_config.save_trie_changes, ); let mut network_chain_store = ChainStore::new( mock_network_runtime.store().clone(), config.genesis.config.genesis_height, - !config.client_config.archive, + config.client_config.save_trie_changes, ); let network_tail_height = network_chain_store.tail().unwrap(); @@ -261,14 +261,13 @@ pub fn setup_mock_node( let client1 = client.clone(); let view_client1 = view_client.clone(); let genesis_config = config.genesis.config.clone(); - let archival = config.client_config.archive; let network_config = network_config.clone(); let chain = Chain::new_for_view_client( mock_network_runtime, &chain_genesis, DoomslugThresholdMode::NoApprovals, - !archival, + config.client_config.save_trie_changes, ) .unwrap(); let chain_height = chain.head().unwrap().height; diff --git a/tools/speedy_sync/src/main.rs b/tools/speedy_sync/src/main.rs index 4a16dcc4bc0..2072539f665 100644 --- a/tools/speedy_sync/src/main.rs +++ b/tools/speedy_sync/src/main.rs @@ -238,7 +238,7 @@ fn load_snapshot(load_cmd: LoadCmd) { &chain_genesis, DoomslugThresholdMode::TwoThirds, ChainConfig { - save_trie_changes: !config.client_config.archive, + save_trie_changes: config.client_config.save_trie_changes, background_migration_threads: 1, }, ) diff --git a/tools/state-viewer/src/commands.rs b/tools/state-viewer/src/commands.rs index 1de72efd356..5f83eaa922b 100644 --- a/tools/state-viewer/src/commands.rs +++ b/tools/state-viewer/src/commands.rs @@ -129,7 +129,7 @@ pub(crate) fn dump_tx( let chain_store = ChainStore::new( store.clone(), near_config.genesis.config.genesis_height, - !near_config.client_config.archive, + near_config.client_config.save_trie_changes, ); let mut txs = vec![]; for height in start_height..=end_height { @@ -266,7 +266,7 @@ pub(crate) fn print_chain( let chain_store = ChainStore::new( store.clone(), near_config.genesis.config.genesis_height, - !near_config.client_config.archive, + near_config.client_config.save_trie_changes, ); let runtime = NightshadeRuntime::from_config(home_dir, store, &near_config); let mut account_id_to_blocks = HashMap::new(); @@ -367,7 +367,7 @@ pub(crate) fn replay_chain( let chain_store = ChainStore::new( store, near_config.genesis.config.genesis_height, - !near_config.client_config.archive, + near_config.client_config.save_trie_changes, ); let new_store = create_test_store(); let runtime = NightshadeRuntime::from_config(home_dir, new_store, &near_config); @@ -517,7 +517,7 @@ pub(crate) fn apply_block_at_height( let mut chain_store = ChainStore::new( store.clone(), near_config.genesis.config.genesis_height, - !near_config.client_config.archive, + near_config.client_config.save_trie_changes, ); let runtime_adapter: Arc = Arc::new(NightshadeRuntime::from_config(home_dir, store, &near_config)); @@ -543,7 +543,7 @@ pub(crate) fn view_chain( let chain_store = ChainStore::new( store.clone(), near_config.genesis.config.genesis_height, - !near_config.client_config.archive, + near_config.client_config.save_trie_changes, ); let block = { match height { @@ -608,7 +608,8 @@ pub(crate) fn view_chain( pub(crate) fn check_block_chunk_existence(near_config: NearConfig, store: Store) { let genesis_height = near_config.genesis.config.genesis_height; - let chain_store = ChainStore::new(store, genesis_height, !near_config.client_config.archive); + let chain_store = + ChainStore::new(store, genesis_height, near_config.client_config.save_trie_changes); let head = chain_store.head().unwrap(); let mut cur_block = chain_store.get_block(&head.last_block_hash).unwrap(); while cur_block.header().height() > genesis_height { @@ -641,7 +642,7 @@ pub(crate) fn print_epoch_info( ) { let genesis_height = near_config.genesis.config.genesis_height; let mut chain_store = - ChainStore::new(store.clone(), genesis_height, !near_config.client_config.archive); + ChainStore::new(store.clone(), genesis_height, near_config.client_config.save_trie_changes); let mut epoch_manager = EpochManager::new_from_genesis_config(store.clone(), &near_config.genesis.config) .expect("Failed to start Epoch Manager"); @@ -662,7 +663,7 @@ pub(crate) fn get_receipt(receipt_id: CryptoHash, near_config: NearConfig, store let chain_store = ChainStore::new( store, near_config.genesis.config.genesis_height, - !near_config.client_config.archive, + near_config.client_config.save_trie_changes, ); let receipt = chain_store.get_receipt(&receipt_id); println!("Receipt: {:#?}", receipt); @@ -672,7 +673,7 @@ pub(crate) fn get_chunk(chunk_hash: ChunkHash, near_config: NearConfig, store: S let chain_store = ChainStore::new( store, near_config.genesis.config.genesis_height, - !near_config.client_config.archive, + near_config.client_config.save_trie_changes, ); let chunk = chain_store.get_chunk(&chunk_hash); println!("Chunk: {:#?}", chunk); @@ -686,7 +687,7 @@ pub(crate) fn get_partial_chunk( let chain_store = ChainStore::new( store, near_config.genesis.config.genesis_height, - !near_config.client_config.archive, + near_config.client_config.save_trie_changes, ); let partial_chunk = chain_store.get_partial_chunk(&partial_chunk_hash); println!("Partial chunk: {:#?}", partial_chunk); @@ -719,7 +720,7 @@ fn load_trie_stop_at_height( let chain_store = ChainStore::new( store.clone(), near_config.genesis.config.genesis_height, - !near_config.client_config.archive, + near_config.client_config.save_trie_changes, ); let runtime = NightshadeRuntime::from_config(home_dir, store, near_config); @@ -783,7 +784,7 @@ pub(crate) fn apply_chunk( let mut chain_store = ChainStore::new( store, near_config.genesis.config.genesis_height, - !near_config.client_config.archive, + near_config.client_config.save_trie_changes, ); let (apply_result, gas_limit) = apply_chunk::apply_chunk(&runtime, &mut chain_store, chunk_hash, target_height, None)?; diff --git a/tools/state-viewer/src/dump_state_parts.rs b/tools/state-viewer/src/dump_state_parts.rs index 8457e42f083..1dbad67dc4c 100644 --- a/tools/state-viewer/src/dump_state_parts.rs +++ b/tools/state-viewer/src/dump_state_parts.rs @@ -118,7 +118,7 @@ pub(crate) fn dump_state_parts( let mut chain_store = ChainStore::new( store.clone(), near_config.genesis.config.genesis_height, - !near_config.client_config.archive, + near_config.client_config.save_trie_changes, ); let epoch_id = epoch_selection.to_epoch_id(store, &mut chain_store, &mut epoch_manager); From b6f2bd9c6a772a08f76604a4dcc5e14bcb8e0a45 Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Tue, 20 Dec 2022 20:41:26 +0100 Subject: [PATCH 118/188] refactor: fix clippy issues in near-crypto (#8241) This PR fixes the following issues in `near-crypto` as part of #8145: * `clippy::needless_borrow` * `clippy::redundant_static_lifetimes` * `clippy::redundant_clone` * `clippy::derive_partial_eq_without_eq` * `clippy::useless_vec` * `clippy::from_over_into` --- core/crypto/src/hash.rs | 4 ++-- core/crypto/src/key_file.rs | 9 ++++----- core/crypto/src/signature.rs | 9 +++++++-- core/crypto/src/signer.rs | 2 +- core/crypto/src/traits.rs | 24 ++++++++++++------------ 5 files changed, 26 insertions(+), 22 deletions(-) diff --git a/core/crypto/src/hash.rs b/core/crypto/src/hash.rs index 04870848986..13f15b0cea6 100644 --- a/core/crypto/src/hash.rs +++ b/core/crypto/src/hash.rs @@ -62,13 +62,13 @@ impl + ?Sized> Hashable for &T { impl Hashable for Point { fn hash_into(self, digest: D) -> D { - digest.chain(&self.pack()) + digest.chain(self.pack()) } } impl Hashable for Scalar { fn hash_into(self, digest: D) -> D { - digest.chain(&self.pack()) + digest.chain(self.pack()) } } diff --git a/core/crypto/src/key_file.rs b/core/crypto/src/key_file.rs index b169dcb9395..6d570fd9985 100644 --- a/core/crypto/src/key_file.rs +++ b/core/crypto/src/key_file.rs @@ -48,9 +48,9 @@ impl KeyFile { mod test { use super::*; - const ACCOUNT_ID: &'static str = "example"; - const SECRET_KEY: &'static str = "ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr"; - const KEY_FILE_CONTENTS: &'static str = r#"{ + const ACCOUNT_ID: &str = "example"; + const SECRET_KEY: &str = "ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr"; + const KEY_FILE_CONTENTS: &str = r#"{ "account_id": "example", "public_key": "ed25519:6DSjZ8mvsRZDvFqFxo8tCKePG96omXW7eVYVSySmDk8e", "secret_key": "ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr" @@ -63,7 +63,7 @@ mod test { let account_id = ACCOUNT_ID.parse().unwrap(); let secret_key: SecretKey = SECRET_KEY.parse().unwrap(); - let public_key = secret_key.public_key().clone(); + let public_key = secret_key.public_key(); let key = KeyFile { account_id, public_key, secret_key }; key.write_to_file(&path).unwrap(); @@ -90,7 +90,6 @@ mod test { let secret_key: SecretKey = SECRET_KEY.parse().unwrap(); assert_eq!(secret_key, key.secret_key); assert_eq!(secret_key.public_key(), key.public_key); - () }) } diff --git a/core/crypto/src/signature.rs b/core/crypto/src/signature.rs index 13b7b2a3aa8..3293c82b88f 100644 --- a/core/crypto/src/signature.rs +++ b/core/crypto/src/signature.rs @@ -119,6 +119,8 @@ pub enum PublicKey { } impl PublicKey { + // `is_empty` always returns false, so there is no point in adding it + #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> usize { match self { Self::ED25519(_) => ed25519_dalek::PUBLIC_KEY_LENGTH + 1, @@ -490,6 +492,9 @@ pub enum Signature { SECP256K1(Secp256K1Signature), } +// This `Hash` implementation is safe since it retains the property +// `k1 == k2 ⇒ hash(k1) == hash(k2)`. +#[allow(clippy::derive_hash_xor_eq)] impl Hash for Signature { fn hash(&self, state: &mut H) { match self { @@ -745,7 +750,7 @@ mod tests { #[test] fn test_sign_verify() { - for key_type in vec![KeyType::ED25519, KeyType::SECP256K1] { + for key_type in [KeyType::ED25519, KeyType::SECP256K1] { let secret_key = SecretKey::from_random(key_type); let public_key = secret_key.public_key(); use sha2::Digest; @@ -812,7 +817,7 @@ mod tests { fn test_borsh_serialization() { use sha2::Digest; let data = sha2::Sha256::digest(b"123").to_vec(); - for key_type in vec![KeyType::ED25519, KeyType::SECP256K1] { + for key_type in [KeyType::ED25519, KeyType::SECP256K1] { let sk = SecretKey::from_seed(key_type, "test"); let pk = sk.public_key(); let bytes = pk.try_to_vec().unwrap(); diff --git a/core/crypto/src/signer.rs b/core/crypto/src/signer.rs index 57b63d823af..4f2fee030a9 100644 --- a/core/crypto/src/signer.rs +++ b/core/crypto/src/signer.rs @@ -44,7 +44,7 @@ impl Signer for EmptySigner { } /// Signer that keeps secret key in memory. -#[derive(Clone, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct InMemorySigner { pub account_id: AccountId, pub public_key: PublicKey, diff --git a/core/crypto/src/traits.rs b/core/crypto/src/traits.rs index 8b07fe4a391..f5345165c4c 100644 --- a/core/crypto/src/traits.rs +++ b/core/crypto/src/traits.rs @@ -14,15 +14,15 @@ macro_rules! common_conversions { } } - impl Into for $ty { - fn into(self) -> String { - bs58::encode(self).into_string() + impl From<$ty> for String { + fn from(v: $ty) -> String { + bs58::encode(v).into_string() } } - impl Into for &$ty { - fn into(self) -> String { - bs58::encode(self).into_string() + impl From<&$ty> for String { + fn from(v: &$ty) -> String { + bs58::encode(v).into_string() } } @@ -74,15 +74,15 @@ macro_rules! common_conversions_fixed { } } - impl Into<[u8; $l]> for $ty { - fn into(self) -> [u8; $l] { - *self.as_ref() + impl From<$ty> for [u8; $l] { + fn from(v: $ty) -> [u8; $l] { + *v.as_ref() } } - impl Into<[u8; $l]> for &$ty { - fn into(self) -> [u8; $l] { - *self.as_ref() + impl From<&$ty> for [u8; $l] { + fn from(v: &$ty) -> [u8; $l] { + *v.as_ref() } } From b4edb966451cba0b7a0b015951a3355f71384f2e Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Wed, 21 Dec 2022 09:29:35 +0100 Subject: [PATCH 119/188] refactor: fix clippy warnings in near-account-id (#8247) Part of #8145 --- core/account-id/src/borsh.rs | 4 ++-- core/account-id/src/lib.rs | 2 +- core/account-id/src/serde.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/account-id/src/borsh.rs b/core/account-id/src/borsh.rs index d6ec09be70f..aa216655b7c 100644 --- a/core/account-id/src/borsh.rs +++ b/core/account-id/src/borsh.rs @@ -32,7 +32,7 @@ mod tests { #[test] fn test_is_valid_account_id() { - for account_id in OK_ACCOUNT_IDS.iter().cloned() { + for account_id in OK_ACCOUNT_IDS.iter() { let parsed_account_id = account_id.parse::().unwrap_or_else(|err| { panic!("Valid account id {:?} marked invalid: {}", account_id, err) }); @@ -52,7 +52,7 @@ mod tests { assert_eq!(serialized_account_id, str_serialized_account_id); } - for account_id in BAD_ACCOUNT_IDS.iter().cloned() { + for account_id in BAD_ACCOUNT_IDS.iter() { let str_serialized_account_id = account_id.try_to_vec().unwrap(); assert!( diff --git a/core/account-id/src/lib.rs b/core/account-id/src/lib.rs index d040d5fa285..3603ff988e8 100644 --- a/core/account-id/src/lib.rs +++ b/core/account-id/src/lib.rs @@ -439,7 +439,7 @@ mod tests { } for account_id in BAD_ACCOUNT_IDS.iter().cloned() { - if let Ok(_) = AccountId::validate(account_id) { + if AccountId::validate(account_id).is_ok() { panic!("Invalid account id {:?} marked valid", account_id); } } diff --git a/core/account-id/src/serde.rs b/core/account-id/src/serde.rs index a785b7aef4e..fb1d7ca7d38 100644 --- a/core/account-id/src/serde.rs +++ b/core/account-id/src/serde.rs @@ -35,7 +35,7 @@ mod tests { #[test] fn test_is_valid_account_id() { - for account_id in OK_ACCOUNT_IDS.iter().cloned() { + for account_id in OK_ACCOUNT_IDS.iter() { let parsed_account_id = account_id.parse::().unwrap_or_else(|err| { panic!("Valid account id {:?} marked invalid: {}", account_id, err) }); @@ -53,7 +53,7 @@ mod tests { assert_eq!(serialized_account_id, json!(account_id)); } - for account_id in BAD_ACCOUNT_IDS.iter().cloned() { + for account_id in BAD_ACCOUNT_IDS.iter() { assert!( serde_json::from_value::(json!(account_id)).is_err(), "successfully deserialized invalid account ID {:?}", From ff1ff054dff5675020e6b1f33a1b3d5bb8a787e5 Mon Sep 17 00:00:00 2001 From: Aleksandr Logunov Date: Wed, 21 Dec 2022 16:09:50 +0400 Subject: [PATCH 120/188] feat: support block catchups for flat storage (#8193) This PR enables flat storage on nightly, which adds it to the CI. We also add block catchup support here, because it is implicitly required by our nayduck tests. ## Idea You can read about catchups here: https://github.com/near/nearcore/blob/master/docs/architecture/how/sync.md * First, we remove obsolete flat storage data. This is critical, because any leftover key in flat storage may lead to incorrect transaction processing, which can lead to kickout or being slashed in the future. So far we just remove keys naively to pass nayduck tests, but when we enable catchups on mainnet/testnet, we have to optimize this part. * During applying state parts and setting KV trie items we set KV flat storage items as well. * When we finalize prepared shard state, we set flat storage head and create `FlatStorageState`. * During block catchups we check is tracked shard is new, and if yes, we update flat storage head for it. cc @mzhangmzz @pugachAG ## Testing * couple of existing catchup tests, e.g. `test_catchup_gas_price_change` * Manual check of affected nayduck tests. `python3 pytest/tests/sanity/block_production.py` passes with this change and doesn't pass without it. * Note that some nayduck tests still fail after that: https://nayduck.near.org/#/run/2785. I would merge the PR as is and introduce fixes later, because they seem to be orthogonal to catchups, but catchups are necessary to support to have a chance to pass these tests. --- chain/chain/Cargo.toml | 3 +- chain/chain/src/chain.rs | 217 +++++++++++++++++------ chain/chain/src/flat_storage_creator.rs | 2 +- chain/chain/src/test_utils/kv_runtime.rs | 8 + chain/chain/src/types.rs | 8 + chain/client/Cargo.toml | 2 + chain/client/src/sync/state.rs | 1 + core/store/Cargo.toml | 1 + core/store/src/flat_state.rs | 98 +++++++++- core/store/src/trie/mod.rs | 4 +- core/store/src/trie/state_parts.rs | 8 +- integration-tests/Cargo.toml | 3 +- nearcore/Cargo.toml | 3 +- nearcore/src/runtime/mod.rs | 17 +- 14 files changed, 306 insertions(+), 69 deletions(-) diff --git a/chain/chain/Cargo.toml b/chain/chain/Cargo.toml index 81873d0ba75..a454ed405ab 100644 --- a/chain/chain/Cargo.toml +++ b/chain/chain/Cargo.toml @@ -56,7 +56,8 @@ nightly = [ nightly_protocol = [ "near-store/nightly_protocol", "near-primitives/nightly_protocol", - "protocol_feature_reject_blocks_with_outdated_protocol_version" + "protocol_feature_reject_blocks_with_outdated_protocol_version", + "protocol_feature_flat_state", ] mock_node = [] sandbox = ["near-primitives/sandbox"] diff --git a/chain/chain/src/chain.rs b/chain/chain/src/chain.rs index e15e75170b9..6dc7eaa9572 100644 --- a/chain/chain/src/chain.rs +++ b/chain/chain/src/chain.rs @@ -51,8 +51,8 @@ use near_primitives::views::{ LightClientBlockView, SignedTransactionView, }; #[cfg(feature = "protocol_feature_flat_state")] -use near_store::{flat_state, StorageError}; -use near_store::{DBCol, ShardTries, StoreUpdate, WrappedTrieChanges}; +use near_store::flat_state; +use near_store::{DBCol, ShardTries, StorageError, StoreUpdate, WrappedTrieChanges}; use crate::block_processing_utils::{ BlockPreprocessInfo, BlockProcessingArtifact, BlocksInProcessing, DoneApplyChunkCallback, @@ -85,9 +85,9 @@ use near_primitives::shard_layout::{ account_id_to_shard_id, account_id_to_shard_uid, ShardLayout, ShardUId, }; use near_primitives::version::PROTOCOL_VERSION; -use near_store::flat_state::FlatStorageError; #[cfg(feature = "protocol_feature_flat_state")] use near_store::flat_state::{store_helper, FlatStateDelta}; +use near_store::flat_state::{FlatStorageError, FlatStorageStateStatus}; use once_cell::sync::OnceCell; use rayon::iter::{IntoParallelIterator, ParallelIterator}; @@ -2106,6 +2106,64 @@ 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_id: ShardId, + ) -> Result<(), Error> { + if let Some(flat_storage_state) = + self.runtime_adapter.get_flat_storage_state_for_shard(shard_id) + { + 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_state.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 { + // If background flat storage creation was initiated, update its creation status, which means executing + // some work related to current creation step and possibly moving status forward until flat storage is + // finally created. + // Note that it doesn't work with state sync / catchup logic. + match &mut self.flat_storage_creator { + Some(flat_storage_creator) => { + flat_storage_creator.update_status(shard_id, &self.store)?; + } + None => { + // TODO (#8250): enable this assertion. Currently it doesn't work because runtime may be implemented + // with KeyValueRuntime which doesn't support flat storage. + // #[cfg(feature = "protocol_feature_flat_state")] + // 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. @@ -2132,6 +2190,7 @@ impl Chain { .entered(); let prev_head = self.store.head()?; + let is_caught_up = block_preprocess_info.is_caught_up; let provenance = block_preprocess_info.provenance.clone(); let block_start_processing_time = block_preprocess_info.block_start_processing_time.clone(); // TODO(#8055): this zip relies on the ordering of the apply_results. @@ -2162,51 +2221,34 @@ impl Chain { // apply chunks processes. This means, the flat head is not always the same as // the last final block on chain, which is OK, because in the flat storage implementation // we don't assume that. - // Also note, for now, we only update flat storage for the shards that we care about in this epoch - // TODO (#7327): support flat storage for state sync and block catchups for shard_id in 0..self.runtime_adapter.num_shards(block.header().epoch_id())? { - if self.runtime_adapter.cares_about_shard( - me.as_ref(), - block.header().prev_hash(), - shard_id, - true, - ) { - if let Some(flat_storage_state) = - self.runtime_adapter.get_flat_storage_state_for_shard(shard_id) - { - 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_state.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)`. 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 { - match &mut self.flat_storage_creator { - Some(flat_storage_creator) => { - flat_storage_creator.update_status(shard_id, &self.store)?; - } - None => {} - } - } + let need_flat_storage_update = if is_caught_up { + // If we already caught up this epoch, then flat storage exists for both shards which we already track + // and shards which will be tracked in next epoch, so we can update them. + self.runtime_adapter.cares_about_shard( + me.as_ref(), + block.header().prev_hash(), + shard_id, + true, + ) || self.runtime_adapter.will_care_about_shard( + me.as_ref(), + block.header().prev_hash(), + shard_id, + true, + ) + } else { + // If we didn't catch up, we can update only shards tracked right now. Remaining shards will be updated + // during catchup of this block. + self.runtime_adapter.cares_about_shard( + me.as_ref(), + block.header().prev_hash(), + shard_id, + true, + ) + }; + + if need_flat_storage_update { + self.update_flat_storage_for_block(&block, shard_id)?; } } @@ -3119,6 +3161,10 @@ impl Chain { num_parts: u64, state_parts_task_scheduler: &dyn Fn(ApplyStatePartsRequest), ) -> Result<(), Error> { + // Before working with state parts, remove existing flat storage data. + let epoch_id = self.get_block_header(&sync_hash)?.epoch_id().clone(); + self.runtime_adapter.remove_flat_storage_state_for_shard(shard_id, &epoch_id)?; + let shard_state_header = self.get_state_header(shard_id, sync_hash)?; let state_root = shard_state_header.chunk_prev_state_root(); let epoch_id = self.get_block_header(&sync_hash)?.epoch_id().clone(); @@ -3145,6 +3191,34 @@ impl Chain { apply_result?; let shard_state_header = self.get_state_header(shard_id, sync_hash)?; + let chunk = shard_state_header.cloned_chunk(); + + let block_hash = chunk.prev_block(); + let block_height = self.get_block_header(block_hash)?.height(); + + // Flat storage must not exist at this point because leftover keys corrupt its state. + assert!(self.runtime_adapter.get_flat_storage_state_for_shard(shard_id).is_none()); + + // We synced shard state on top of _previous_ block for chunk in shard state header and applied state parts to + // flat storage. Now we can set flat head to hash of this block and create flat storage. + #[cfg(feature = "protocol_feature_flat_state")] + { + let mut store_update = self.runtime_adapter.store().store_update(); + store_helper::set_flat_head(&mut store_update, shard_id, block_hash); + store_update.commit()?; + } + + match self.runtime_adapter.try_create_flat_storage_state_for_shard( + shard_id, + block_height, + self.store(), + ) { + FlatStorageStateStatus::Ready | FlatStorageStateStatus::DontCreate => {} + status @ _ => { + return Err(Error::StorageError(StorageError::FlatStorageError(format!("Unable to create flat storage during syncing shard {shard_id}, got status {status:?}")))); + } + } + let mut height = shard_state_header.chunk_height_included(); let mut chain_update = self.chain_update(); chain_update.set_state_finalize(shard_id, sync_hash, shard_state_header)?; @@ -3307,12 +3381,30 @@ impl Chain { let prev_block = self.store.get_block(block.header().prev_hash())?; let mut chain_update = self.chain_update(); chain_update.apply_chunk_postprocessing( - me, &block, &prev_block, results.into_iter().collect::, Error>>()?, )?; chain_update.commit()?; + + for shard_id in 0..self.runtime_adapter.num_shards(block.header().epoch_id())? { + // Update flat storage for each shard being caught up. We catch up a shard if it is tracked in the next + // epoch. If it is tracked in this epoch as well, it was updated during regular block processing. + if !self.runtime_adapter.cares_about_shard( + me.as_ref(), + block.header().prev_hash(), + shard_id, + true, + ) && self.runtime_adapter.will_care_about_shard( + me.as_ref(), + block.header().prev_hash(), + shard_id, + true, + ) { + self.update_flat_storage_for_block(&block, shard_id)?; + } + } + Ok(()) } @@ -4692,7 +4784,6 @@ impl<'a> ChainUpdate<'a> { fn apply_chunk_postprocessing( &mut self, - me: &Option, block: &Block, prev_block: &Block, apply_results: Vec, @@ -4700,7 +4791,6 @@ impl<'a> ChainUpdate<'a> { let _span = tracing::debug_span!(target: "chain", "apply_chunk_postprocessing").entered(); for result in apply_results { self.process_apply_chunk_result( - me, result, *block.hash(), block.header().height(), @@ -4839,28 +4929,27 @@ impl<'a> ChainUpdate<'a> { #[allow(unused_variables)] fn save_flat_state_changes( &mut self, - me: &Option, block_hash: CryptoHash, prev_hash: CryptoHash, height: BlockHeight, shard_id: ShardId, trie_changes: &WrappedTrieChanges, ) -> Result<(), Error> { - // Right now, we don't implement flat storage for catchup, so we only store - // the delta for the shards that we are tracking this epoch #[cfg(feature = "protocol_feature_flat_state")] - if self.runtime_adapter.cares_about_shard(me.as_ref(), &prev_hash, shard_id, true) { + { let delta = FlatStateDelta::from_state_changes(&trie_changes.state_changes()); if let Some(chain_flat_storage) = self.runtime_adapter.get_flat_storage_state_for_shard(shard_id) { + // If flat storage exists, we add a block to it. let block_info = flat_state::BlockInfo { hash: block_hash, height, prev_hash }; let store_update = chain_flat_storage .add_block(&block_hash, delta, block_info) .map_err(|e| StorageError::from(e))?; self.chain_store_update.merge(store_update); } else { + // Otherwise, save delta to disk so it will be used for flat storage creation later. info!(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_id, block_hash.clone(), &delta) @@ -4874,7 +4963,6 @@ impl<'a> ChainUpdate<'a> { /// Processed results of applying chunk fn process_apply_chunk_result( &mut self, - me: &Option, result: ApplyChunkResult, block_hash: CryptoHash, height: BlockHeight, @@ -4905,7 +4993,6 @@ impl<'a> ChainUpdate<'a> { ), ); self.save_flat_state_changes( - me, block_hash, prev_block_hash, height, @@ -4946,7 +5033,6 @@ impl<'a> ChainUpdate<'a> { *new_extra.state_root_mut() = apply_result.new_root; self.save_flat_state_changes( - me, block_hash, prev_block_hash, height, @@ -4998,7 +5084,7 @@ impl<'a> ChainUpdate<'a> { } x }).collect::, Error>>()?; - self.apply_chunk_postprocessing(me, block, &prev_block, results)?; + self.apply_chunk_postprocessing(block, &prev_block, results)?; let BlockPreprocessInfo { is_caught_up, @@ -5318,6 +5404,13 @@ impl<'a> ChainUpdate<'a> { self.chain_store_update.save_chunk(chunk); + self.save_flat_state_changes( + block_header.hash().clone(), + chunk_header.prev_block_hash().clone(), + chunk_header.height_included(), + shard_id, + &apply_result.trie_changes, + )?; self.chain_store_update.save_trie_changes(apply_result.trie_changes); let chunk_extra = ChunkExtra::new( &apply_result.new_root, @@ -5397,7 +5490,13 @@ impl<'a> ChainUpdate<'a> { Default::default(), false, )?; - + self.save_flat_state_changes( + block_header.hash().clone(), + prev_block_header.hash().clone(), + height, + shard_id, + &apply_result.trie_changes, + )?; self.chain_store_update.save_trie_changes(apply_result.trie_changes); let mut new_chunk_extra = ChunkExtra::clone(&chunk_extra); diff --git a/chain/chain/src/flat_storage_creator.rs b/chain/chain/src/flat_storage_creator.rs index c5db36333cb..897f3e7a06f 100644 --- a/chain/chain/src/flat_storage_creator.rs +++ b/chain/chain/src/flat_storage_creator.rs @@ -33,6 +33,7 @@ use near_store::{Trie, TrieDBStorage, TrieTraversalItem}; use std::sync::atomic::AtomicU64; use std::sync::Arc; use tracing::debug; +#[cfg(feature = "protocol_feature_flat_state")] use tracing::info; /// If we launched a node with enabled flat storage but it doesn't have flat storage data on disk, we have to create it. @@ -382,7 +383,6 @@ impl FlatStorageCreator { chain_store.head().unwrap().height, chain_store, ); - info!(target: "chain", %shard_id, "Flat storage creation status: {:?}", status); match status { FlatStorageStateStatus::Ready | FlatStorageStateStatus::DontCreate => {} _ => { diff --git a/chain/chain/src/test_utils/kv_runtime.rs b/chain/chain/src/test_utils/kv_runtime.rs index a9770b9f1bb..e24eeb63033 100644 --- a/chain/chain/src/test_utils/kv_runtime.rs +++ b/chain/chain/src/test_utils/kv_runtime.rs @@ -837,6 +837,14 @@ impl RuntimeAdapter for KeyValueRuntime { FlatStorageStateStatus::DontCreate } + fn remove_flat_storage_state_for_shard( + &self, + _shard_id: ShardId, + _epoch_id: &EpochId, + ) -> Result<(), Error> { + Ok(()) + } + fn set_flat_storage_state_for_genesis( &self, _genesis_block: &CryptoHash, diff --git a/chain/chain/src/types.rs b/chain/chain/src/types.rs index 75518493fe6..1d804e4a203 100644 --- a/chain/chain/src/types.rs +++ b/chain/chain/src/types.rs @@ -307,6 +307,14 @@ pub trait RuntimeAdapter: EpochManagerAdapter + Send + Sync { chain_access: &dyn ChainAccessForFlatStorage, ) -> FlatStorageStateStatus; + /// Removes flat storage state for shard, if it exists. + /// Used to clear old flat storage data from disk and memory before syncing to newer state. + fn remove_flat_storage_state_for_shard( + &self, + shard_id: ShardId, + epoch_id: &EpochId, + ) -> Result<(), Error>; + fn set_flat_storage_state_for_genesis( &self, genesis_block: &CryptoHash, diff --git a/chain/client/Cargo.toml b/chain/client/Cargo.toml index 1debd8665ad..bf34d630623 100644 --- a/chain/client/Cargo.toml +++ b/chain/client/Cargo.toml @@ -65,9 +65,11 @@ delay_detector = [ nightly_protocol = [] nightly = [ "nightly_protocol", + "protocol_feature_flat_state", "near-chain/nightly", ] sandbox = [ "near-client-primitives/sandbox", "near-chain/sandbox", ] +protocol_feature_flat_state = ["near-store/protocol_feature_flat_state", "near-chain/protocol_feature_flat_state"] diff --git a/chain/client/src/sync/state.rs b/chain/client/src/sync/state.rs index f65522745e3..68af141955e 100644 --- a/chain/client/src/sync/state.rs +++ b/chain/client/src/sync/state.rs @@ -199,6 +199,7 @@ impl StateSync { update_sync_status = true; ShardSyncDownload::new(now) }); + let old_status = shard_sync_download.status.clone(); let mut this_done = false; match &shard_sync_download.status { diff --git a/core/store/Cargo.toml b/core/store/Cargo.toml index ae6f2a0c88b..dd8f4c3f4f9 100644 --- a/core/store/Cargo.toml +++ b/core/store/Cargo.toml @@ -61,4 +61,5 @@ cold_store = [] nightly_protocol = [] nightly = [ "nightly_protocol", + "protocol_feature_flat_state", ] diff --git a/core/store/src/flat_state.rs b/core/store/src/flat_state.rs index f56010c398f..5399edcff17 100644 --- a/core/store/src/flat_state.rs +++ b/core/store/src/flat_state.rs @@ -54,7 +54,9 @@ impl From for StorageError { #[cfg(feature = "protocol_feature_flat_state")] mod imp { use crate::flat_state::{store_helper, FlatStorageState, POISONED_LOCK_ERR}; + use near_primitives::errors::StorageError; use near_primitives::hash::CryptoHash; + use near_primitives::shard_layout::ShardLayout; use near_primitives::state::ValueRef; use near_primitives::types::ShardId; use std::collections::HashMap; @@ -155,7 +157,6 @@ mod imp { /// Note that this function is different from `add_flat_storage_state_for_shard`, /// it must be called before `add_flat_storage_state_for_shard` if the node starts from /// an empty database. - #[cfg(feature = "protocol_feature_flat_state")] pub fn set_flat_storage_state_for_genesis( &self, store_update: &mut StoreUpdate, @@ -249,6 +250,24 @@ mod imp { let flat_storage_states = self.0.flat_storage_states.lock().expect(POISONED_LOCK_ERR); flat_storage_states.get(&shard_id).cloned() } + + pub fn remove_flat_storage_state_for_shard( + &self, + shard_id: ShardId, + shard_layout: ShardLayout, + ) -> Result<(), StorageError> { + let mut flat_storage_states = + self.0.flat_storage_states.lock().expect(POISONED_LOCK_ERR); + + match flat_storage_states.remove(&shard_id) { + None => {} + Some(flat_storage_state) => { + flat_storage_state.clear_state(shard_layout)?; + } + } + + Ok(()) + } } } @@ -256,7 +275,9 @@ mod imp { mod imp { use crate::flat_state::FlatStorageState; use crate::{Store, StoreUpdate}; + use near_primitives::errors::StorageError; use near_primitives::hash::CryptoHash; + use near_primitives::shard_layout::ShardLayout; use near_primitives::types::ShardId; /// Since this has no variants it can never be instantiated. @@ -302,6 +323,14 @@ mod imp { ) { } + pub fn remove_flat_storage_state_for_shard( + &self, + _shard_id: ShardId, + _shard_layout: ShardLayout, + ) -> Result<(), StorageError> { + Ok(()) + } + pub fn set_flat_storage_state_for_genesis( &self, _store_update: &mut StoreUpdate, @@ -343,6 +372,15 @@ impl FlatStateDelta { self.0.get(key).cloned() } + /// Inserts a key-value pair to delta. + pub fn insert(&mut self, key: Vec, value: Option) -> Option> { + self.0.insert(key, value) + } + + pub fn len(&self) -> usize { + self.0.len() + } + /// Merge two deltas. Values from `other` should override values from `self`. pub fn merge(&mut self, other: &Self) { self.0.extend(other.0.iter().map(|(k, v)| (k.clone(), v.clone()))) @@ -387,7 +425,14 @@ impl FlatStateDelta { } use near_primitives::errors::StorageError; +#[cfg(feature = "protocol_feature_flat_state")] +use near_primitives::shard_layout::account_id_to_shard_id; +use near_primitives::shard_layout::ShardLayout; +#[cfg(feature = "protocol_feature_flat_state")] +use near_primitives::trie_key::trie_key_parsers::parse_account_id_from_raw_key; use std::sync::{Arc, RwLock}; +#[cfg(feature = "protocol_feature_flat_state")] +use tracing::info; /// FlatStorageState 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, @@ -712,7 +757,7 @@ impl FlatStorageStateInner { } /// Get deltas between blocks `target_block_hash`(inclusive) to flat head(exclusive), - /// in backwards chain order. Returns an error if there is no path between these two them. + /// in backwards chain order. Returns an error if there is no path between them. fn get_deltas_between_blocks( &self, target_block_hash: &CryptoHash, @@ -829,7 +874,6 @@ impl FlatStorageState { /// 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. - // TODO (#7327): implement garbage collection of old deltas. #[cfg(feature = "protocol_feature_flat_state")] pub fn update_flat_head(&self, new_head: &CryptoHash) -> Result<(), FlatStorageError> { let mut guard = self.0.write().expect(POISONED_LOCK_ERR); @@ -840,6 +884,8 @@ impl FlatStorageState { } // Update flat state on disk. + let shard_id = guard.shard_id; + let new_height = guard.blocks.get(new_head).unwrap().height; guard.flat_head = *new_head; let mut store_update = StoreUpdate::new(guard.store.storage.clone()); store_helper::set_flat_head(&mut store_update, guard.shard_id, new_head); @@ -867,6 +913,8 @@ impl FlatStorageState { } store_update.commit().expect(BORSH_ERR); + info!(target: "chain", %shard_id, %new_head, %new_height, "Moved flat storage head"); + Ok(()) } @@ -888,6 +936,9 @@ impl FlatStorageState { block: BlockInfo, ) -> Result { let mut guard = self.0.write().expect(POISONED_LOCK_ERR); + let shard_id = guard.shard_id; + let block_height = block.height; + info!(target: "chain", %shard_id, %block_hash, %block_height, "Adding block to flat storage"); if !guard.blocks.contains_key(&block.prev_hash) { return Err(guard.create_block_not_supported_error(block_hash)); } @@ -907,6 +958,47 @@ impl FlatStorageState { ) -> Result { panic!("not implemented") } + + /// Clears all State key-value pairs from flat storage. + #[cfg(feature = "protocol_feature_flat_state")] + pub fn clear_state(&self, shard_layout: ShardLayout) -> Result<(), StorageError> { + let guard = self.0.write().expect(POISONED_LOCK_ERR); + let shard_id = guard.shard_id; + + // Removes all items belonging to the shard one by one. + // Note that it does not work for resharding. + // TODO (#7327): call it just after we stopped tracking a shard. + // TODO (#7327): remove FlatStateDeltas. Consider custom serialization of keys to remove them by + // prefix. + // TODO (#7327): support range deletions which are much faster than naive deletions. For that, we + // can delete ranges of keys like + // [ [0]+boundary_accounts(shard_id) .. [0]+boundary_accounts(shard_id+1) ), etc. + // We should also take fixed accounts into account. + let mut store_update = guard.store.store_update(); + let mut removed_items = 0; + for item in guard.store.iter(crate::DBCol::FlatState) { + let (key, _) = + item.map_err(|e| StorageError::StorageInconsistentState(e.to_string()))?; + let account_id = parse_account_id_from_raw_key(&key) + .map_err(|e| StorageError::StorageInconsistentState(e.to_string()))? + .ok_or(StorageError::FlatStorageError(format!( + "Failed to find account id in flat storage key {:?}", + key + )))?; + if account_id_to_shard_id(&account_id, &shard_layout) == shard_id { + removed_items += 1; + store_update.delete(crate::DBCol::FlatState, &key); + } + } + info!(target: "chain", %shard_id, %removed_items, "Removing old items from flat storage"); + + store_helper::remove_flat_head(&mut store_update, shard_id); + store_update.commit().map_err(|_| StorageError::StorageInternalError)?; + Ok(()) + } + + #[cfg(not(feature = "protocol_feature_flat_state"))] + pub fn clear_state(&self, _shard_layout: ShardLayout) {} } #[cfg(test)] diff --git a/core/store/src/trie/mod.rs b/core/store/src/trie/mod.rs index 4134ea22f57..461066f7ad3 100644 --- a/core/store/src/trie/mod.rs +++ b/core/store/src/trie/mod.rs @@ -25,7 +25,7 @@ pub use crate::trie::prefetching_trie_storage::{PrefetchApi, PrefetchError}; pub use crate::trie::shard_tries::{KeyForStateChanges, ShardTries, WrappedTrieChanges}; pub use crate::trie::trie_storage::{TrieCache, TrieCachingStorage, TrieDBStorage, TrieStorage}; use crate::trie::trie_storage::{TrieMemoryPartialStorage, TrieRecordingStorage}; -use crate::StorageError; +use crate::{FlatStateDelta, StorageError}; pub use near_primitives::types::TrieNodesCount; use std::fmt::Write; @@ -550,6 +550,8 @@ impl TrieChanges { pub struct ApplyStatePartResult { /// Trie changes after applying state part. pub trie_changes: TrieChanges, + /// Flat state changes after applying state part, stored as delta. + pub flat_state_delta: FlatStateDelta, /// Contract codes belonging to the state part. pub contract_codes: Vec, } diff --git a/core/store/src/trie/state_parts.rs b/core/store/src/trie/state_parts.rs index 414327a2236..da73a95a4f5 100644 --- a/core/store/src/trie/state_parts.rs +++ b/core/store/src/trie/state_parts.rs @@ -10,8 +10,9 @@ use crate::trie::nibble_slice::NibbleSlice; use crate::trie::{ ApplyStatePartResult, NodeHandle, RawTrieNodeWithSize, TrieNode, TrieNodeWithSize, }; -use crate::{PartialStorage, StorageError, Trie, TrieChanges}; +use crate::{FlatStateDelta, PartialStorage, StorageError, Trie, TrieChanges}; use near_primitives::contract::ContractCode; +use near_primitives::state::ValueRef; use near_primitives::state_record::is_contract_code_key; impl Trie { @@ -199,6 +200,7 @@ impl Trie { if state_root == &Trie::EMPTY_ROOT { return Ok(ApplyStatePartResult { trie_changes: TrieChanges::empty(Trie::EMPTY_ROOT), + flat_state_delta: Default::default(), contract_codes: vec![], }); } @@ -211,11 +213,14 @@ impl Trie { let mut iterator = trie.iter()?; let trie_traversal_items = iterator.visit_nodes_interval(&path_begin, &path_end)?; let mut map = HashMap::new(); + let mut flat_state_delta = FlatStateDelta::default(); let mut contract_codes = Vec::new(); for TrieTraversalItem { hash, key } in trie_traversal_items { let value = trie.storage.retrieve_raw_bytes(&hash)?; map.entry(hash).or_insert_with(|| (value.to_vec(), 0)).1 += 1; if let Some(trie_key) = key { + let value_ref = ValueRef::new(&value); + flat_state_delta.insert(trie_key.clone(), Some(value_ref)); if is_contract_code_key(&trie_key) { contract_codes.push(ContractCode::new(value.to_vec(), None)); } @@ -229,6 +234,7 @@ impl Trie { insertions, deletions, }, + flat_state_delta, contract_codes, }) } diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 820cf38d1e8..3b5fd21bd8a 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -79,7 +79,8 @@ nightly = [ "nightly_protocol", "nearcore/nightly", "protocol_feature_fix_contract_loading_cost", - "protocol_feature_reject_blocks_with_outdated_protocol_version" + "protocol_feature_reject_blocks_with_outdated_protocol_version", + "protocol_feature_flat_state", ] nightly_protocol = ["nearcore/nightly_protocol"] sandbox = [ diff --git a/nearcore/Cargo.toml b/nearcore/Cargo.toml index 2599c7b73c4..12e9a2ddac7 100644 --- a/nearcore/Cargo.toml +++ b/nearcore/Cargo.toml @@ -108,7 +108,7 @@ protocol_feature_fix_staking_threshold = [ protocol_feature_fix_contract_loading_cost = [ "near-vm-runner/protocol_feature_fix_contract_loading_cost", ] -protocol_feature_flat_state = ["near-store/protocol_feature_flat_state", "near-chain/protocol_feature_flat_state", "node-runtime/protocol_feature_flat_state"] +protocol_feature_flat_state = ["near-client/protocol_feature_flat_state", "near-store/protocol_feature_flat_state", "near-chain/protocol_feature_flat_state", "node-runtime/protocol_feature_flat_state"] nightly = [ "nightly_protocol", @@ -118,6 +118,7 @@ nightly = [ "near-store/nightly", "protocol_feature_fix_staking_threshold", "protocol_feature_fix_contract_loading_cost", + "protocol_feature_flat_state", ] nightly_protocol = [ "near-primitives/nightly_protocol", diff --git a/nearcore/src/runtime/mod.rs b/nearcore/src/runtime/mod.rs index 592e712d774..fa2dbfca884 100644 --- a/nearcore/src/runtime/mod.rs +++ b/nearcore/src/runtime/mod.rs @@ -724,9 +724,22 @@ impl RuntimeAdapter for NightshadeRuntime { } _ => {} } + info!(target: "chain", %shard_id, "Flat storage creation status: {status:?}"); status } + fn remove_flat_storage_state_for_shard( + &self, + shard_id: ShardId, + epoch_id: &EpochId, + ) -> Result<(), Error> { + let shard_layout = self.get_shard_layout(epoch_id)?; + self.flat_state_factory + .remove_flat_storage_state_for_shard(shard_id, shard_layout) + .map_err(|e| Error::StorageError(e))?; + Ok(()) + } + fn set_flat_storage_state_for_genesis( &self, genesis_block: &CryptoHash, @@ -1339,12 +1352,14 @@ impl RuntimeAdapter for NightshadeRuntime { ) -> Result<(), Error> { let part = BorshDeserialize::try_from_slice(data) .expect("Part was already validated earlier, so could never fail here"); - let ApplyStatePartResult { trie_changes, contract_codes } = + let ApplyStatePartResult { trie_changes, flat_state_delta, contract_codes } = Trie::apply_state_part(state_root, part_id, part); let tries = self.get_tries(); let shard_uid = self.get_shard_uid_from_epoch_id(shard_id, epoch_id)?; let mut store_update = tries.store_update(); tries.apply_all(&trie_changes, shard_uid, &mut store_update); + debug!(target: "chain", %shard_id, "Inserting {} values to flat storage", flat_state_delta.len()); + flat_state_delta.apply_to_flat_state(&mut store_update); self.precompile_contracts(epoch_id, contract_codes)?; Ok(store_update.commit()?) } From d57ae75b7442273b3d32e218df6adf61b4bbd57a Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Wed, 21 Dec 2022 13:25:45 +0100 Subject: [PATCH 121/188] fix: flaky prefetcher tests (#8255) Prefetcher tests have become flaky in CI lately, failing with `"unexpected number of prefetched values"`. I was not able to reproduce it locally. But there is a timeout that is at least one source for flakyness. This commit removes this source by properly waiting on the background threads to finish instead of relying on a timeout. Hopefully resolves #8252. (Will have to reopen if not.) --- runtime/runtime/src/prefetch.rs | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/runtime/runtime/src/prefetch.rs b/runtime/runtime/src/prefetch.rs index 66378b72a1c..588e804a2cd 100644 --- a/runtime/runtime/src/prefetch.rs +++ b/runtime/runtime/src/prefetch.rs @@ -346,8 +346,36 @@ mod tests { ); } - // Request can still be pending. Stop threads and wait for them to finish. - std::thread::sleep(Duration::from_micros(1000)); + // The queue is empty now but there can still be requests in progress. + // Looking at `Pending` slots in the prefetching area is not sufficient + // here because threads can be in `Trie::lookup` between nodes, at which + // point no slot is reserved for them but they are still doing work. + // + // Solution: `drop(tries)`, which also drops prefetchers. In particular, + // we want to drop the `PrefetchingThreadsHandle` stored in tries. + // `drop(tries)` causes all background threads to stop after they finish + // the current work. It will even join them and wait until all threads + // are done. + // + // Because threads are joined, there is also a possibility this will + // hang forever. To avoid that, we drop in a separate thread. + let dropped = std::sync::atomic::AtomicBool::new(false); + std::thread::scope(|s| { + s.spawn(|| { + drop(tries); + dropped.store(true, std::sync::atomic::Ordering::Release); + }); + let spawned = Instant::now(); + while !dropped.load(std::sync::atomic::Ordering::Acquire) { + std::thread::yield_now(); + // 100ms should be enough to finish pending requests. If not, + // we should check how the prefetcher affects performance. + assert!( + spawned.elapsed() < Duration::from_millis(100), + "timeout while waiting for background threads to terminate" + ); + } + }); assert_eq!( prefetch_api.num_prefetched_and_staged(), From d462b8f8615d4cf6cf8b7c02740c9c32fa14025b Mon Sep 17 00:00:00 2001 From: pompon0 Date: Wed, 21 Dec 2022 14:51:26 +0100 Subject: [PATCH 122/188] set the in-flight limit on the demuxed calls to 1 for now (#8249) Lack of in-flight limit caused a bug in implementation of network graph recomputation: - recomputation demux rate limit was set to 1/s - recomputation was taking >10s on mainnet (for unrelated reasons) - recomputations cannot be executed in parallel (they acquire a single mutex) - steady inflow of new edges was triggering a recomputation every 1s, while each recomputation took >10s (after acquiring the mutex), making the recomputations queue unboundedly - the delay of adding an edge to the graph was growing indefinitely, eventually making the edges expire before even it was added to the graph --- chain/network/src/concurrency/demux.rs | 64 ++++++++++++++++---------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/chain/network/src/concurrency/demux.rs b/chain/network/src/concurrency/demux.rs index 251fe5c3a1c..08b9ccee3d6 100644 --- a/chain/network/src/concurrency/demux.rs +++ b/chain/network/src/concurrency/demux.rs @@ -122,6 +122,7 @@ impl Demux { if tokens < rl.burst && next_token.is_none() { next_token = Some(tokio::time::Instant::now() + interval); } + tokio::select! { // TODO(gprusak): implement sleep future support for FakeClock, // so that we don't use tokio directly here. @@ -139,35 +140,50 @@ impl Demux { }, } if !calls.is_empty() && tokens > 0 { + // First pop all the elements already accumulated on the queue. + // TODO(gprusak): technically calling try_recv() in a loop may cause a starvation, + // in case elements are added to the queue faster than we can take them out, + // so ideally we should rather atomically dump the content of the queue: + // we can achieve that by maintaining an atomic counter with number of elements in + // the queue. + while let Ok(call) = recv.try_recv() { + calls.push(call); + } + tokens -= 1; // TODO(gprusak): as of now Demux (as a concurrency primitive) doesn't support // cancellation. Once we add cancellation support, this task could accept a context sum: // the sum is valid iff any context is valid. let calls = std::mem::take(&mut calls); - // TODO(gprusak): don't spawn on the "current" runtime. See one of the previous TODOs. - tokio::spawn(async move { - let mut args = vec![]; - let mut outs = vec![]; - let mut handlers = vec![]; - for call in calls { - args.push(call.arg); - outs.push(call.out); - handlers.push(call.handler); - } - let res = handlers.swap_remove(0)(args).await; - assert_eq!( - res.len(), - outs.len(), - "demux handler returned {} results, expected {}", - res.len(), - outs.len(), - ); - for (res, out) in std::iter::zip(res, outs) { - // If the caller is no longer interested in the result, - // the channel will be closed. Ignore that. - let _ = out.send(res); - } - }); + let mut args = vec![]; + let mut outs = vec![]; + let mut handlers = vec![]; + // TODO(gprusak): due to inlining the call at most 1 call is executed at any + // given time. Ideally we should have a separate limit for the number of + // in-flight calls. It would be dangerous to have it unbounded, especially in a + // case when the concurrent calls would cause a contention. + // TODO(gprusak): add metrics for: + // - demuxed call latency (the one inlined here) + // - outer call latency (i.e. latency of the whole Demux.call, from the PoV of + // the caller). + for call in calls { + args.push(call.arg); + outs.push(call.out); + handlers.push(call.handler); + } + let res = handlers.swap_remove(0)(args).await; + assert_eq!( + res.len(), + outs.len(), + "demux handler returned {} results, expected {}", + res.len(), + outs.len(), + ); + for (res, out) in std::iter::zip(res, outs) { + // If the caller is no longer interested in the result, + // the channel will be closed. Ignore that. + let _ = out.send(res); + } } } }); From 1f3d4ea4c10f5623d71d991df2fa799f12b28296 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Wed, 21 Dec 2022 16:25:10 +0100 Subject: [PATCH 123/188] runtime: move get_vec_from_memory_or_register into separate function (#8259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calling get_vec_from_memory_or_register method mutably borrows the entire VMLogic structure. If the method was to return a borrow, it would ‘lock’ the VMLogic structure making it impossible to use any of its fields or methods. Replace the method with a separate get_memory_or_register function which takes gas counter, memory and registers as parameters. This allows the compiler to track borrows separately allowing the method to return reference to register values rather than having to make a copy of the data. With this, most places where the data is copied into a vector (via use of Cow::into_owned) are where the vector is needed. Though note that this only applies to registers at the moment. Data from memory is always copied into a vector. --- runtime/near-vm-logic/src/logic.rs | 108 ++++++++++++++------------- runtime/near-vm-logic/src/vmstate.rs | 26 +++++++ 2 files changed, 84 insertions(+), 50 deletions(-) diff --git a/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs index 8fee7ee296f..1d692fad580 100644 --- a/runtime/near-vm-logic/src/logic.rs +++ b/runtime/near-vm-logic/src/logic.rs @@ -82,6 +82,23 @@ enum Promise { NotReceipt(Vec), } +/// Helper for calling `crate::vmstate::get_memory_or_register`. +/// +/// crate::vmstate::get_memory_or_register has a whole lot of wordy arguments +/// which are always the same when invoked inside of one of VMLogic method. +/// This macro helps with that invocation. +macro_rules! get_memory_or_register { + ($logic:expr, $offset:expr, $len:expr) => { + crate::vmstate::get_memory_or_register( + &mut $logic.gas_counter, + &$logic.memory, + &$logic.registers, + $offset, + $len, + ) + }; +} + impl<'a> VMLogic<'a> { pub fn new_with_protocol_version( ext: &'a mut dyn External, @@ -154,18 +171,6 @@ impl<'a> VMLogic<'a> { &self.config } - // ########################### - // # Memory helper functions # - // ########################### - - fn get_vec_from_memory_or_register(&mut self, offset: u64, len: u64) -> Result> { - if len != u64::MAX { - self.memory.get_vec(&mut self.gas_counter, offset, len) - } else { - self.registers.get(&mut self.gas_counter, offset).map(Vec::from) - } - } - // ################# // # Registers API # // ################# @@ -749,7 +754,7 @@ impl<'a> VMLogic<'a> { register_id: u64, ) -> Result<()> { self.gas_counter.pay_base(alt_bn128_g1_multiexp_base)?; - let data = self.get_vec_from_memory_or_register(value_ptr, value_len)?; + let data = get_memory_or_register!(self, value_ptr, value_len)?; let elements = crate::alt_bn128::split_elements(&data)?; self.gas_counter.pay_per(alt_bn128_g1_multiexp_element, elements.len() as u64)?; @@ -792,7 +797,7 @@ impl<'a> VMLogic<'a> { register_id: u64, ) -> Result<()> { self.gas_counter.pay_base(alt_bn128_g1_sum_base)?; - let data = self.get_vec_from_memory_or_register(value_ptr, value_len)?; + let data = get_memory_or_register!(self, value_ptr, value_len)?; let elements = crate::alt_bn128::split_elements(&data)?; self.gas_counter.pay_per(alt_bn128_g1_sum_element, elements.len() as u64)?; @@ -831,7 +836,7 @@ impl<'a> VMLogic<'a> { /// `base + write_register_base + write_register_byte * num_bytes + alt_bn128_pairing_base + alt_bn128_pairing_element * num_elements` pub fn alt_bn128_pairing_check(&mut self, value_len: u64, value_ptr: u64) -> Result { self.gas_counter.pay_base(alt_bn128_pairing_check_base)?; - let data = self.get_vec_from_memory_or_register(value_ptr, value_len)?; + let data = get_memory_or_register!(self, value_ptr, value_len)?; let elements = crate::alt_bn128::split_elements(&data)?; self.gas_counter.pay_per(alt_bn128_pairing_check_element, elements.len() as u64)?; @@ -872,7 +877,7 @@ impl<'a> VMLogic<'a> { /// `base + write_register_base + write_register_byte * num_bytes + sha256_base + sha256_byte * num_bytes` pub fn sha256(&mut self, value_len: u64, value_ptr: u64, register_id: u64) -> Result<()> { self.gas_counter.pay_base(sha256_base)?; - let value = self.get_vec_from_memory_or_register(value_ptr, value_len)?; + let value = get_memory_or_register!(self, value_ptr, value_len)?; self.gas_counter.pay_per(sha256_byte, value.len() as u64)?; use sha2::Digest; @@ -898,7 +903,7 @@ impl<'a> VMLogic<'a> { /// `base + write_register_base + write_register_byte * num_bytes + keccak256_base + keccak256_byte * num_bytes` pub fn keccak256(&mut self, value_len: u64, value_ptr: u64, register_id: u64) -> Result<()> { self.gas_counter.pay_base(keccak256_base)?; - let value = self.get_vec_from_memory_or_register(value_ptr, value_len)?; + let value = get_memory_or_register!(self, value_ptr, value_len)?; self.gas_counter.pay_per(keccak256_byte, value.len() as u64)?; use sha3::Digest; @@ -924,7 +929,7 @@ impl<'a> VMLogic<'a> { /// `base + write_register_base + write_register_byte * num_bytes + keccak512_base + keccak512_byte * num_bytes` pub fn keccak512(&mut self, value_len: u64, value_ptr: u64, register_id: u64) -> Result<()> { self.gas_counter.pay_base(keccak512_base)?; - let value = self.get_vec_from_memory_or_register(value_ptr, value_len)?; + let value = get_memory_or_register!(self, value_ptr, value_len)?; self.gas_counter.pay_per(keccak512_byte, value.len() as u64)?; use sha3::Digest; @@ -952,7 +957,7 @@ impl<'a> VMLogic<'a> { /// `base + write_register_base + write_register_byte * num_bytes + ripemd160_base + ripemd160_block * message_blocks` pub fn ripemd160(&mut self, value_len: u64, value_ptr: u64, register_id: u64) -> Result<()> { self.gas_counter.pay_base(ripemd160_base)?; - let value = self.get_vec_from_memory_or_register(value_ptr, value_len)?; + let value = get_memory_or_register!(self, value_ptr, value_len)?; let message_blocks = value .len() @@ -1007,7 +1012,7 @@ impl<'a> VMLogic<'a> { self.gas_counter.pay_base(ecrecover_base)?; let signature = { - let vec = self.get_vec_from_memory_or_register(sig_ptr, sig_len)?; + let vec = get_memory_or_register!(self, sig_ptr, sig_len)?; if vec.len() != 64 { return Err(VMLogicError::HostError(HostError::ECRecoverError { msg: format!( @@ -1031,7 +1036,7 @@ impl<'a> VMLogic<'a> { }; let hash = { - let vec = self.get_vec_from_memory_or_register(hash_ptr, hash_len)?; + let vec = get_memory_or_register!(self, hash_ptr, hash_len)?; if vec.len() != 32 { return Err(VMLogicError::HostError(HostError::ECRecoverError { msg: format!( @@ -1111,7 +1116,7 @@ impl<'a> VMLogic<'a> { self.gas_counter.pay_base(ed25519_verify_base)?; let signature: ed25519_dalek::Signature = { - let vec = self.get_vec_from_memory_or_register(signature_ptr, signature_len)?; + let vec = get_memory_or_register!(self, signature_ptr, signature_len)?; if vec.len() != ed25519_dalek::SIGNATURE_LENGTH { return Err(VMLogicError::HostError(HostError::Ed25519VerifyInvalidInput { msg: "invalid signature length".to_string(), @@ -1123,11 +1128,11 @@ impl<'a> VMLogic<'a> { } }; - let message = self.get_vec_from_memory_or_register(message_ptr, message_len)?; + let message = get_memory_or_register!(self, message_ptr, message_len)?; self.gas_counter.pay_per(ed25519_verify_byte, message.len() as u64)?; let public_key: ed25519_dalek::PublicKey = { - let vec = self.get_vec_from_memory_or_register(public_key_ptr, public_key_len)?; + let vec = get_memory_or_register!(self, public_key_ptr, public_key_len)?; if vec.len() != ed25519_dalek::PUBLIC_KEY_LENGTH { return Err(VMLogicError::HostError(HostError::Ed25519VerifyInvalidInput { msg: "invalid public key length".to_string(), @@ -1550,20 +1555,18 @@ impl<'a> VMLogic<'a> { } .into()); } - let code = self.get_vec_from_memory_or_register(code_ptr, code_len)?; - if code.len() as u64 > self.config.limit_config.max_contract_size { - return Err(HostError::ContractSizeExceeded { - size: code.len() as u64, - limit: self.config.limit_config.max_contract_size, - } - .into()); + let code = get_memory_or_register!(self, code_ptr, code_len)?; + let code_len = code.len() as u64; + let limit = self.config.limit_config.max_contract_size; + if code_len > limit { + return Err(HostError::ContractSizeExceeded { size: code_len, limit }.into()); } + let code = code.into_owned(); let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; - let num_bytes = code.len() as u64; self.pay_action_base(ActionCosts::deploy_contract_base, sir)?; - self.pay_action_per_byte(ActionCosts::deploy_contract_byte, num_bytes, sir)?; + self.pay_action_per_byte(ActionCosts::deploy_contract_byte, code_len, sir)?; self.receipt_manager.append_action_deploy_contract(receipt_idx, code)?; Ok(()) @@ -1664,14 +1667,16 @@ impl<'a> VMLogic<'a> { .into()); } let amount = self.memory.get_u128(&mut self.gas_counter, amount_ptr)?; - let method_name = self.get_vec_from_memory_or_register(method_name_ptr, method_name_len)?; + let method_name = get_memory_or_register!(self, method_name_ptr, method_name_len)?; if method_name.is_empty() { return Err(HostError::EmptyMethodName.into()); } - let arguments = self.get_vec_from_memory_or_register(arguments_ptr, arguments_len)?; + let arguments = get_memory_or_register!(self, arguments_ptr, arguments_len)?; let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; + let method_name = method_name.into_owned(); + let arguments = arguments.into_owned(); // Input can't be large enough to overflow let num_bytes = method_name.len() as u64 + arguments.len() as u64; self.pay_action_base(ActionCosts::function_call_base, sir)?; @@ -1771,7 +1776,8 @@ impl<'a> VMLogic<'a> { .into()); } let amount = self.memory.get_u128(&mut self.gas_counter, amount_ptr)?; - let public_key = self.get_vec_from_memory_or_register(public_key_ptr, public_key_len)?; + let public_key = + get_memory_or_register!(self, public_key_ptr, public_key_len)?.into_owned(); let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; self.pay_action_base(ActionCosts::stake, sir)?; @@ -1810,7 +1816,8 @@ impl<'a> VMLogic<'a> { } .into()); } - let public_key = self.get_vec_from_memory_or_register(public_key_ptr, public_key_len)?; + let public_key = + get_memory_or_register!(self, public_key_ptr, public_key_len)?.into_owned(); let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; self.pay_action_base(ActionCosts::add_full_access_key, sir)?; @@ -1860,12 +1867,12 @@ impl<'a> VMLogic<'a> { } .into()); } - let public_key = self.get_vec_from_memory_or_register(public_key_ptr, public_key_len)?; + let public_key = + get_memory_or_register!(self, public_key_ptr, public_key_len)?.into_owned(); let allowance = self.memory.get_u128(&mut self.gas_counter, allowance_ptr)?; let allowance = if allowance > 0 { Some(allowance) } else { None }; let receiver_id = self.read_and_parse_account_id(receiver_id_ptr, receiver_id_len)?; - let raw_method_names = - self.get_vec_from_memory_or_register(method_names_ptr, method_names_len)?; + let raw_method_names = get_memory_or_register!(self, method_names_ptr, method_names_len)?; let method_names = split_method_names(&raw_method_names)?; let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; @@ -1916,7 +1923,8 @@ impl<'a> VMLogic<'a> { } .into()); } - let public_key = self.get_vec_from_memory_or_register(public_key_ptr, public_key_len)?; + let public_key = + get_memory_or_register!(self, public_key_ptr, public_key_len)?.into_owned(); let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; self.pay_action_base(ActionCosts::delete_key, sir)?; @@ -2085,7 +2093,7 @@ impl<'a> VMLogic<'a> { /// `base + cost of reading return value from memory or register + dispatch&exec cost per byte of the data sent * num data receivers` pub fn value_return(&mut self, value_len: u64, value_ptr: u64) -> Result<()> { self.gas_counter.pay_base(base)?; - let return_val = self.get_vec_from_memory_or_register(value_ptr, value_len)?; + let return_val = get_memory_or_register!(self, value_ptr, value_len)?; let mut burn_gas: Gas = 0; let num_bytes = return_val.len() as u64; if num_bytes > self.config.limit_config.max_length_returned_data { @@ -2123,7 +2131,7 @@ impl<'a> VMLogic<'a> { burn_gas, ActionCosts::new_data_receipt_byte, )?; - self.return_data = ReturnData::Value(return_val); + self.return_data = ReturnData::Value(return_val.into_owned()); Ok(()) } @@ -2256,7 +2264,7 @@ impl<'a> VMLogic<'a> { /// cost of reading buffer from register or memory, /// `utf8_decoding_base + utf8_decoding_byte * num_bytes`. fn read_and_parse_account_id(&mut self, ptr: u64, len: u64) -> Result { - let buf = self.get_vec_from_memory_or_register(ptr, len)?; + let buf = get_memory_or_register!(self, ptr, len)?; self.gas_counter.pay_base(utf8_decoding_base)?; self.gas_counter.pay_per(utf8_decoding_byte, buf.len() as u64)?; @@ -2264,7 +2272,7 @@ impl<'a> VMLogic<'a> { // backwards compatibility. For paths previously involving validation, like receipts // we retain validation further down the line in node-runtime/verifier.rs#fn(validate_receipt) // mimicing previous behaviour. - let account_id = String::from_utf8(buf) + let account_id = String::from_utf8(buf.into_owned()) .map( #[allow(deprecated)] AccountId::new_unvalidated, @@ -2309,7 +2317,7 @@ impl<'a> VMLogic<'a> { ); } self.gas_counter.pay_base(storage_write_base)?; - let key = self.get_vec_from_memory_or_register(key_ptr, key_len)?; + let key = get_memory_or_register!(self, key_ptr, key_len)?; if key.len() as u64 > self.config.limit_config.max_length_storage_key { return Err(HostError::KeyLengthExceeded { length: key.len() as u64, @@ -2317,7 +2325,7 @@ impl<'a> VMLogic<'a> { } .into()); } - let value = self.get_vec_from_memory_or_register(value_ptr, value_len)?; + let value = get_memory_or_register!(self, value_ptr, value_len)?; if value.len() as u64 > self.config.limit_config.max_length_storage_value { return Err(HostError::ValueLengthExceeded { length: value.len() as u64, @@ -2416,7 +2424,7 @@ impl<'a> VMLogic<'a> { pub fn storage_read(&mut self, key_len: u64, key_ptr: u64, register_id: u64) -> Result { self.gas_counter.pay_base(base)?; self.gas_counter.pay_base(storage_read_base)?; - let key = self.get_vec_from_memory_or_register(key_ptr, key_len)?; + let key = get_memory_or_register!(self, key_ptr, key_len)?; if key.len() as u64 > self.config.limit_config.max_length_storage_key { return Err(HostError::KeyLengthExceeded { length: key.len() as u64, @@ -2482,7 +2490,7 @@ impl<'a> VMLogic<'a> { ); } self.gas_counter.pay_base(storage_remove_base)?; - let key = self.get_vec_from_memory_or_register(key_ptr, key_len)?; + let key = get_memory_or_register!(self, key_ptr, key_len)?; if key.len() as u64 > self.config.limit_config.max_length_storage_key { return Err(HostError::KeyLengthExceeded { length: key.len() as u64, @@ -2549,7 +2557,7 @@ impl<'a> VMLogic<'a> { pub fn storage_has_key(&mut self, key_len: u64, key_ptr: u64) -> Result { self.gas_counter.pay_base(base)?; self.gas_counter.pay_base(storage_has_key_base)?; - let key = self.get_vec_from_memory_or_register(key_ptr, key_len)?; + let key = get_memory_or_register!(self, key_ptr, key_len)?; if key.len() as u64 > self.config.limit_config.max_length_storage_key { return Err(HostError::KeyLengthExceeded { length: key.len() as u64, diff --git a/runtime/near-vm-logic/src/vmstate.rs b/runtime/near-vm-logic/src/vmstate.rs index c45deacf63a..e566195589e 100644 --- a/runtime/near-vm-logic/src/vmstate.rs +++ b/runtime/near-vm-logic/src/vmstate.rs @@ -6,6 +6,7 @@ use near_primitives_core::config::VMLimitConfig; use near_vm_errors::{HostError, VMLogicError}; use core::mem::size_of; +use std::borrow::Cow; use std::collections::hash_map::Entry; type Result = ::std::result::Result; @@ -221,6 +222,31 @@ impl Registers { } } +/// Reads data from guest memory or register. +/// +/// If `len` is `u64::MAX` read register with index `offset`. Otherwise, reads +/// `len` bytes of guest memory starting at given offset. Returns error if +/// there’s insufficient gas, memory interval is out of bounds or given register +/// isn’t set. +/// +/// This is not a method on `VMLogic` so that the compiler can track borrowing +/// of gas counter, memory and registers separately. This allows `VMLogic` to +/// borrow value from a register and then continue constructing mutable +/// references to other fields in the structure.. +pub(super) fn get_memory_or_register<'a, 'b>( + gas_counter: &mut GasCounter, + memory: &Memory<'a>, + registers: &'b Registers, + offset: u64, + len: u64, +) -> Result> { + if len == u64::MAX { + registers.get(gas_counter, offset).map(Cow::Borrowed) + } else { + memory.get_vec(gas_counter, offset, len).map(Cow::Owned) + } +} + #[cfg(test)] mod tests { use super::Registers; From 4f8a2b4314b08307a0bcb2f4cfa99bd774726ac1 Mon Sep 17 00:00:00 2001 From: mzhangmzz <34969888+mzhangmzz@users.noreply.github.com> Date: Wed, 21 Dec 2022 16:40:11 -0500 Subject: [PATCH 124/188] Improve adversenet script (#8256) Some improvements on the run_adversenet script - Added help message - More logging - Option for not starting the adverset from scratch --- pytest/lib/mocknet.py | 16 ++- ...d_test_adversenet.py => run_adversenet.py} | 127 ++++++++++++------ 2 files changed, 100 insertions(+), 43 deletions(-) rename pytest/tests/mocknet/{load_test_adversenet.py => run_adversenet.py} (51%) mode change 100644 => 100755 diff --git a/pytest/lib/mocknet.py b/pytest/lib/mocknet.py index 36fb82d87e0..5b08ac08295 100644 --- a/pytest/lib/mocknet.py +++ b/pytest/lib/mocknet.py @@ -452,9 +452,10 @@ def compress_and_upload(nodes, src_filename, dst_filename): def redownload_neard(nodes, binary_url): pmap( - lambda node: node.machine.run('sudo -u ubuntu -i', - input='wget -O /home/ubuntu/neard {}'. - format(binary_url)), nodes) + lambda node: node.machine. + run('sudo -u ubuntu -i', + input='wget -O /home/ubuntu/neard {}; chmod +x /home/ubuntu/neard'. + format(binary_url)), nodes) # check each of /home/ubuntu/neard and /home/ubuntu/neard.upgrade to see @@ -1010,6 +1011,7 @@ def download_and_read_json(node, filename): def upload_json(node, filename, data): + logger.info(f'Upload file {filename} to {node.instance_name}') tmp_file = tempfile.NamedTemporaryFile(mode='r+', delete=False) with open(tmp_file.name, 'w') as f: json.dump(data, f, indent=2) @@ -1084,6 +1086,14 @@ def create_and_upload_config_file_from_default(nodes, chain_id, overrider=None): upload_json(node, '/home/ubuntu/.near/config.json', copied_config) +def update_existing_config_file(nodes, overrider=None): + for node in nodes: + config_json = download_and_read_json(nodes[0], + '/home/ubuntu/.near/config.json') + overrider(node, config_json) + upload_json(node, '/home/ubuntu/.near/config.json', config_json) + + def start_nodes(nodes, upgrade_schedule=None): pmap(lambda node: start_node(node, upgrade_schedule=upgrade_schedule), nodes) diff --git a/pytest/tests/mocknet/load_test_adversenet.py b/pytest/tests/mocknet/run_adversenet.py old mode 100644 new mode 100755 similarity index 51% rename from pytest/tests/mocknet/load_test_adversenet.py rename to pytest/tests/mocknet/run_adversenet.py index e4ba52beefc..aac631ee29d --- a/pytest/tests/mocknet/load_test_adversenet.py +++ b/pytest/tests/mocknet/run_adversenet.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -""" -Runs a loadtest on adversenet. + +help_str = """ +This script starts or updates a network of adversenet, in which some validators may be malicious. The setup requires you to have a few nodes that will be validators and at least one node that will be an RPC node. The script will recognize any node that has "validator" in its name as a validator, of which any node that has a @@ -8,22 +9,16 @@ Use https://github.com/near/near-ops/tree/master/provisioning/terraform/network/adversenet to bring up a set of VM instances for the test. - -This script will always initializes the network from scratch, and that is the intended purpose of adversenet. -(Because the point of adversenet is to break the network, it is likely the network is in a bad state, so that's -why it's a good idea to just always start from scratch.) """ + import argparse -import random import sys import time -from rc import pmap +from enum import Enum import pathlib sys.path.append(str(pathlib.Path(__file__).resolve().parents[2] / 'lib')) -from helpers import load_test_spoon_helper -from helpers import load_testing_add_and_delete_helper import mocknet import data @@ -76,24 +71,63 @@ def check_slow_blocks(initial_metrics, final_metrics): def override_config(node, config): # Add config here depending on the specific node build. pass + """ + if "bad" in node.instance_name: + config["adversarial"] = { + "produce_duplicate_blocks": True + } + """ + + +class Role(Enum): + Rpc = 0 + GoodValidator = 1 + BadValidator = 2 + + +def get_role(node): + if "validator" not in node.instance_name: + return Role.Rpc + elif "bad" in node.instance_name: + return Role.BadValidator + else: + return Role.GoodValidator if __name__ == '__main__': - logger.info('Starting Load test.') - parser = argparse.ArgumentParser(description='Run a load test') + logger.info('Starting adversenet.') + parser = argparse.ArgumentParser(description=help_str) + parser.add_argument( + 'mode', + choices=["new", "update"], + help= + "new: start a new network from scratch, update: update existing network" + ) parser.add_argument('--chain-id', required=False, default="adversenet") - parser.add_argument('--pattern', required=False, default="adversenet-node-") - parser.add_argument('--epoch-length', type=int, required=False, default=60) - parser.add_argument('--skip-setup', default=False, action='store_true') - parser.add_argument('--skip-reconfigure', - default=False, - action='store_true') - parser.add_argument('--num-seats', type=int, required=False, default=100) - parser.add_argument('--binary-url', required=False) - parser.add_argument('--clear-data', + parser.add_argument('--pattern', + required=False, + default="adversenet-node-", + help="pattern to filter the gcp instance names") + parser.add_argument( + '--epoch-length', + type=int, + required=False, + default=60, + help="epoch length of the network. Only used when mode == new") + parser.add_argument( + '--num-seats', + type=int, + required=False, + default=100, + help="number of validator seats. Only used when mode == new") + parser.add_argument('--bad-stake', required=False, - default=False, - action='store_true') + default=5, + type=int, + help="Total stake percentage for bad validators") + parser.add_argument('--binary-url', + required=False, + help="url to download neard binary") args = parser.parse_args() @@ -103,28 +137,41 @@ def override_config(node, config): assert epoch_length > 0 all_nodes = mocknet.get_nodes(pattern=pattern) - validator_nodes = [ - node for node in all_nodes if 'validator' in node.instance_name + rpcs = [n.instance_name for n in all_nodes if get_role(n) == Role.Rpc] + good_validators = [ + n.instance_name for n in all_nodes if get_role(n) == Role.GoodValidator ] - logger.info(f'validator_nodes: {validator_nodes}') - rpc_nodes = [ - node for node in all_nodes if 'validator' not in node.instance_name + bad_validators = [ + n.instance_name for n in all_nodes if get_role(n) == Role.BadValidator ] - logger.info( - f'Starting Load of {chain_id} test using {len(validator_nodes)} validator nodes and {len(rpc_nodes)} RPC nodes.' - ) + TOTAL_STAKE = 1000000 + bad_validator_stake = int(TOTAL_STAKE * args.bad_stake / + (100 * len(bad_validators))) + good_validator_stake = int(TOTAL_STAKE * (100 - args.bad_stake) / + (100 * len(good_validators))) + + logger.info(f'Starting chain {chain_id} with {len(all_nodes)} nodes. \n\ + Good validators: {good_validators} each with stake {good_validator_stake} NEAR\n\ + Bad validators: {bad_validators} each with stake {bad_validator_stake} NEAR\n\ + RPC nodes: {rpcs}\n') + + answer = input("Enter y to continue: ") + if answer != "y": + exit(0) mocknet.stop_nodes(all_nodes) time.sleep(10) - if not args.skip_reconfigure: - logger.info(f'Reconfiguring nodes') - # Make sure nodes are running by restarting them. - - if args.clear_data: - mocknet.clear_data(all_nodes) + validator_nodes = [n for n in all_nodes if get_role(n) != Role.Rpc] + rpc_nodes = [n for n in all_nodes if get_role(n) == Role.Rpc] + if args.binary_url: + mocknet.redownload_neard(all_nodes, args.binary_url) + if args.mode == "new": + logger.info(f'Configuring nodes from scratch') + mocknet.clear_data(all_nodes) mocknet.create_and_upload_genesis_file_from_empty_genesis( # Give bad validators less stake. - [(node, 1 if 'bad' in node.instance_name else 5) + [(node, bad_validator_stake * mocknet.ONE_NEAR if get_role(node) + == Role.BadValidator else good_validator_stake * mocknet.ONE_NEAR) for node in validator_nodes], rpc_nodes, chain_id, @@ -132,8 +179,8 @@ def override_config(node, config): num_seats=args.num_seats) mocknet.create_and_upload_config_file_from_default( all_nodes, chain_id, override_config) - if args.binary_url: - mocknet.redownload_neard(all_nodes, args.binary_url) + else: + mocknet.update_existing_config_file(all_nodes, override_config) mocknet.start_nodes(all_nodes) mocknet.wait_all_nodes_up(all_nodes) From 915b08b77916560339583d8579fb08f102948a93 Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Thu, 22 Dec 2022 09:53:28 +0100 Subject: [PATCH 125/188] refactor: clean up busy waiting in prefetcher blocking get (#8215) Part of #7723 This PR replaces busy waiting loop with update notifications on every underlying map update. It introduces single condition variable which is notified on every map update. One considered alternative is to maintain separate condition variable per slot to have more granular notifications. In practice this doesn't make any difference since additional iterations for irrelevant keys wouldn't cause any noticeable performance overhead, but it results in more complex and harder to reason about code. --- core/store/src/lib.rs | 1 + core/store/src/sync_utils.rs | 72 +++++++++++++++++++ .../src/trie/prefetching_trie_storage.rs | 59 +++++++++++---- 3 files changed, 117 insertions(+), 15 deletions(-) create mode 100644 core/store/src/sync_utils.rs diff --git a/core/store/src/lib.rs b/core/store/src/lib.rs index d9ba0837f7d..783fa0d8449 100644 --- a/core/store/src/lib.rs +++ b/core/store/src/lib.rs @@ -49,6 +49,7 @@ pub mod metadata; mod metrics; pub mod migrations; mod opener; +mod sync_utils; pub mod test_utils; mod trie; diff --git a/core/store/src/sync_utils.rs b/core/store/src/sync_utils.rs new file mode 100644 index 00000000000..c2abfccb911 --- /dev/null +++ b/core/store/src/sync_utils.rs @@ -0,0 +1,72 @@ +use std::ops::{Deref, DerefMut}; +use std::sync::{Condvar, Mutex, MutexGuard}; + +const POISONED_LOCK_ERR: &str = "The lock was poisoned."; + +/// A convenience wrapper around a Mutex and a Condvar. +/// +/// It enables blocking while waiting for the underlying value to be updated. +/// The implementation ensures that any modification results in all blocked +/// threads being notified. +pub(crate) struct Monitor { + cvar: Condvar, + mutex: Mutex, +} + +pub(crate) struct MonitorReadGuard<'a, T> { + guard: MutexGuard<'a, T>, +} + +pub(crate) struct MonitorWriteGuard<'a, T> { + guard: MutexGuard<'a, T>, + cvar: &'a Condvar, +} + +impl Monitor { + pub fn new(t: T) -> Self { + Self { mutex: Mutex::new(t), cvar: Condvar::new() } + } + + pub fn lock(&self) -> MonitorReadGuard<'_, T> { + let guard = self.mutex.lock().expect(POISONED_LOCK_ERR); + MonitorReadGuard { guard } + } + + pub fn lock_mut(&self) -> MonitorWriteGuard<'_, T> { + let guard = self.mutex.lock().expect(POISONED_LOCK_ERR); + MonitorWriteGuard { guard, cvar: &self.cvar } + } + + pub fn wait<'a>(&'a self, guard: MonitorReadGuard<'a, T>) -> MonitorReadGuard<'a, T> { + let guard = self.cvar.wait(guard.guard).expect(POISONED_LOCK_ERR); + MonitorReadGuard { guard } + } +} + +impl Deref for MonitorReadGuard<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.guard.deref() + } +} + +impl Deref for MonitorWriteGuard<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.guard.deref() + } +} + +impl DerefMut for MonitorWriteGuard<'_, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.guard.deref_mut() + } +} + +impl Drop for MonitorWriteGuard<'_, T> { + fn drop(&mut self) { + self.cvar.notify_all(); + } +} diff --git a/core/store/src/trie/prefetching_trie_storage.rs b/core/store/src/trie/prefetching_trie_storage.rs index efe0fa8311a..7719d4e6c41 100644 --- a/core/store/src/trie/prefetching_trie_storage.rs +++ b/core/store/src/trie/prefetching_trie_storage.rs @@ -1,3 +1,4 @@ +use crate::sync_utils::Monitor; use crate::trie::POISONED_LOCK_ERR; use crate::{ metrics, DBCol, StorageError, Store, Trie, TrieCache, TrieCachingStorage, TrieConfig, @@ -12,7 +13,7 @@ use near_primitives::shard_layout::ShardUId; use near_primitives::trie_key::TrieKey; use near_primitives::types::{AccountId, ShardId, StateRoot, TrieNodesCount}; use std::collections::HashMap; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::thread; const MAX_QUEUED_WORK_ITEMS: usize = 16 * 1024; @@ -107,7 +108,7 @@ pub enum PrefetchError { /// without the prefetcher, because the order in which it sees accesses is /// independent of the prefetcher. #[derive(Clone)] -pub(crate) struct PrefetchStagingArea(Arc>); +pub(crate) struct PrefetchStagingArea(Arc>); struct InnerPrefetchStagingArea { slots: SizeTrackedHashMap, @@ -311,7 +312,7 @@ impl TriePrefetchingStorage { impl PrefetchStagingArea { fn new(shard_id: ShardId) -> Self { let inner = InnerPrefetchStagingArea { slots: SizeTrackedHashMap::new(shard_id) }; - Self(Arc::new(Mutex::new(inner))) + Self(Arc::new(Monitor::new(inner))) } /// Release a slot in the prefetcher staging area. @@ -322,7 +323,7 @@ impl PrefetchStagingArea { /// 2: IO thread misses in the shard cache on the same key and starts fetching it again. /// 3: Main thread value is inserted in shard cache. pub(crate) fn release(&self, key: &CryptoHash) { - let mut guard = self.lock(); + let mut guard = self.0.lock_mut(); let dropped = guard.slots.remove(key); // `Done` is the result after a successful prefetch. // `PendingFetch` means the value has been read without a prefetch. @@ -345,13 +346,14 @@ impl PrefetchStagingArea { /// same data and thus are waiting on each other rather than the DB. /// Of course, that would require prefetching to be moved into an async environment, pub(crate) fn blocking_get(&self, key: CryptoHash) -> Option> { + let mut guard = self.0.lock(); loop { - match self.lock().slots.get(&key) { + match guard.slots.get(&key) { Some(PrefetchSlot::Done(value)) => return Some(value.clone()), Some(_) => (), None => return None, } - thread::sleep(std::time::Duration::from_micros(1)); + guard = self.0.wait(guard); } } @@ -362,7 +364,7 @@ impl PrefetchStagingArea { } fn insert_fetched(&self, key: CryptoHash, value: Arc<[u8]>) { - self.lock().slots.insert(key, PrefetchSlot::Done(value)); + self.0.lock_mut().slots.insert(key, PrefetchSlot::Done(value)); } /// Get prefetched value if available and otherwise atomically insert the @@ -372,7 +374,7 @@ impl PrefetchStagingArea { key: CryptoHash, set_if_empty: PrefetchSlot, ) -> PrefetcherResult { - let mut guard = self.lock(); + let mut guard = self.0.lock_mut(); let full = guard.slots.size_bytes > MAX_PREFETCH_STAGING_MEMORY - PREFETCH_RESERVED_BYTES_PER_SLOT; match guard.slots.map.get(&key) { @@ -393,12 +395,7 @@ impl PrefetchStagingArea { } fn clear(&self) { - self.lock().slots.clear(); - } - - #[track_caller] - fn lock(&self) -> std::sync::MutexGuard { - self.0.lock().expect(POISONED_LOCK_ERR) + self.0.lock_mut().slots.clear(); } } @@ -551,7 +548,7 @@ impl Drop for PrefetchingThreadsHandle { /// a minimal set of functions is required to check the inner /// state of the prefetcher. #[cfg(feature = "test_features")] -mod tests { +mod tests_utils { use super::{PrefetchApi, PrefetchSlot}; use crate::TrieCachingStorage; @@ -559,6 +556,7 @@ mod tests { /// Returns the number of prefetched values currently staged. pub fn num_prefetched_and_staged(&self) -> usize { self.prefetching + .0 .lock() .slots .map @@ -581,3 +579,34 @@ mod tests { } } } + +#[cfg(test)] +mod tests { + use super::{PrefetchStagingArea, PrefetcherResult}; + use near_primitives::hash::CryptoHash; + + #[test] + fn test_prefetch_staging_area_blocking_get_after_update() { + let key = CryptoHash::hash_bytes(&[1, 2, 3]); + let value: std::sync::Arc<[u8]> = vec![4, 5, 6].into(); + let prefetch_staging_area = PrefetchStagingArea::new(0); + assert!(matches!( + prefetch_staging_area.get_or_set_fetching(key), + PrefetcherResult::SlotReserved + )); + let prefetch_staging_area2 = prefetch_staging_area.clone(); + let value2 = value.clone(); + // We need to execute `blocking_get` before `insert_fetched` so that + // it blocks while waiting for the value to be updated. Spawning + // a new thread + yielding should give enough time for the main + // thread to make progress. Please note that even if `insert_fetched` + // is executed before `blocking_get`, that still wouldn't result in + // any flakiness since the test would still pass, it just won't verify + // the synchronization part of `blocking_get`. + std::thread::spawn(move || { + std::thread::yield_now(); + prefetch_staging_area2.insert_fetched(key, value2); + }); + assert_eq!(prefetch_staging_area.blocking_get(key), Some(value)); + } +} From 4510472d69c059644bb2d2579837c6bd6d94f190 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Thu, 22 Dec 2022 12:37:13 +0100 Subject: [PATCH 126/188] feat(estimator): Send/exec action costs (#8251) Add action cost estimations that measure send and exec separately in isolation from all other costs. These new costs do not replace existing estimations. They are only used to inform the decision for send/exec gas split, which today is almost always fifty-fifty. The new estiamtions show that often times the send costs should be cheaper. But we have to be careful before we reduce any costs because we unfortunately ignore network costs in all our estimations. (See #8214.) The main purpose of the new estimations is to verify the gas cost strategy for meta transactions. (See #8114.) There, we execute the send step twice, so we want to ensure this side of the action cost is not undercharged. It seems to be overcharged at the moment, so we should be safe to move forward with the proposed strategy. --- .../src/action_costs.rs | 620 ++++++++++++++++++ runtime/runtime-params-estimator/src/cost.rs | 49 +- .../src/estimator_context.rs | 63 +- runtime/runtime-params-estimator/src/lib.rs | 52 ++ .../src/transaction_builder.rs | 26 + runtime/runtime/src/lib.rs | 33 + 6 files changed, 838 insertions(+), 5 deletions(-) create mode 100644 runtime/runtime-params-estimator/src/action_costs.rs diff --git a/runtime/runtime-params-estimator/src/action_costs.rs b/runtime/runtime-params-estimator/src/action_costs.rs new file mode 100644 index 00000000000..4e80b87760c --- /dev/null +++ b/runtime/runtime-params-estimator/src/action_costs.rs @@ -0,0 +1,620 @@ +//! Estimation functions for action costs, separated by send and exec. +//! +//! Estimations in this module are more tailored towards specific gas parameters +//! compared to those in the parent module. But the estimations here potential +//! miss some overhead that is outside action verification and outside action +//! application. But in combination with the wholistic action cost estimation, +//! the picture should be fairly complete. + +use crate::estimator_context::{EstimatorContext, Testbed}; +use crate::gas_cost::GasCost; +use crate::transaction_builder::AccountRequirement; +use crate::utils::average_cost; +use near_crypto::{KeyType, PublicKey}; +use near_primitives::account::{AccessKey, AccessKeyPermission, FunctionCallPermission}; +use near_primitives::hash::CryptoHash; +use near_primitives::receipt::{ActionReceipt, Receipt}; +use near_primitives::transaction::{Action, ExecutionStatus}; +use near_primitives::types::AccountId; +use std::iter; + +/// A builder object for constructing action cost estimations. +/// +/// This module uses `ActionEstimation` as a builder object to specify the +/// details of each action estimation. For example, creating an account has the +/// requirement that the account does not exist, yet. But for a staking action, +/// it must exist. The builder object makes it easy to specify these +/// requirements separately for each estimation, with only a small amount of +/// boiler-plate code repeated. +/// +/// Besides account id requirements, the builder also accepts a few other +/// settings. See the available methods and their doc comments. +/// +/// Once `ActionEstimation` is complete, call either `verify_cost` or +/// `apply_cost` to receive just the execution cost or just the sender cost. +/// This will run a loop internally that spawns a bunch of actions using +/// different accounts. This allows to average the cost of a number of runs to +/// make the result more stable. +/// +/// By default, the inner actions are also multiplied within a receipt. This is +/// to reduce the overhead noise of the receipt cost, which can often dominate +/// compared to the cost of a single action inside. The only problem is that all +/// actions inside a receipt must share the receiver and the sender account ids. +/// This makes action duplication unsuitable for actions that cannot be +/// repeated, such as creating or deleting an account. In those cases, set inner +/// iterations to 1. +struct ActionEstimation { + /// generate account ids from the transaction builder with requirements + signer: AccountRequirement, + predecessor: AccountRequirement, + receiver: AccountRequirement, + /// the actions to estimate + actions: Vec, + /// how often actions are repeated in a receipt + inner_iters: usize, + /// how many receipts to measure + outer_iters: usize, + /// how many iterations to ignore for measurements + warmup: usize, + /// the gas metric to measure + metric: crate::config::GasMetric, + /// subtract the cost of an empty receipt from the measured cost + /// (`fasle` is only really useful for action receipt creation cost) + subtract_base: bool, +} + +impl ActionEstimation { + /// Create a new action estimation that can be modified using builder-style + /// methods. + /// + /// The object returned by this constructor uses random and unused accounts + /// for signer, predecessor, and receiver. This means the sender is not the + /// receiver. Further, the default returned here uses 100 inner iterations, + /// thereby duplicating all given actions 100 fold inside each receipt. + /// + /// Note that the object returned here does not contain any actions, yet. It + /// will operate on an action receipt with no actions inside, unless actions + /// are added. + fn new(ctx: &mut EstimatorContext) -> Self { + Self { + signer: AccountRequirement::RandomUnused, + predecessor: AccountRequirement::RandomUnused, + receiver: AccountRequirement::RandomUnused, + actions: vec![], + inner_iters: 100, + outer_iters: ctx.config.iter_per_block, + warmup: ctx.config.warmup_iters_per_block, + metric: ctx.config.metric, + subtract_base: true, + } + } + + /// Create a new action estimation that can be modified using builder-style + /// methods and sets the accounts ids such that the signer, sender, and + /// receiver are all the same account id. + /// + /// This constructor is also used for execution estimations because: + /// (1) Some actions require sender = receiver to execute without an error. + /// (2) It does not matter for execution performance. + fn new_sir(ctx: &mut EstimatorContext) -> Self { + Self { + signer: AccountRequirement::RandomUnused, + predecessor: AccountRequirement::SameAsSigner, + receiver: AccountRequirement::SameAsSigner, + actions: vec![], + inner_iters: 100, + outer_iters: ctx.config.iter_per_block, + warmup: ctx.config.warmup_iters_per_block, + metric: ctx.config.metric, + subtract_base: true, + } + } + + /// Set how to generate the predecessor, also known as sender, for each + /// transaction or action receipt. + fn predecessor(mut self, predecessor: AccountRequirement) -> Self { + self.predecessor = predecessor; + self + } + + /// Set how to generate the receiver account id for each transaction or + /// action receipt. + fn receiver(mut self, receiver: AccountRequirement) -> Self { + self.receiver = receiver; + self + } + + /// Add an action that will be duplicated for every inner iteration. + /// + /// Calling this multiple times is allowed and inner iterations will + /// duplicate the full group as a block, rather than individual actions. + /// (3 * AB = ABABAB, not AAABBB) + fn add_action(mut self, action: Action) -> Self { + self.actions.push(action); + self + } + + /// Set how many times thes actions are duplicated per receipt or transaction. + fn inner_iters(mut self, inner_iters: usize) -> Self { + self.inner_iters = inner_iters; + self + } + + /// If enabled, the estimation will automatically subtract the cost of an + /// empty action receipt from the measurement. (enabled by default) + fn subtract_base(mut self, yes: bool) -> Self { + self.subtract_base = yes; + self + } + + /// Estimate the gas cost for converting an action in a transaction to one in an + /// action receipt, without network costs. + /// + /// To convert a transaction into a receipt, each action has to be verified. + /// This happens on a different shard than the action execution and should + /// therefore be estimated and charged separately. + /// + /// Network costs should also be taken into account here but we don't do that, + /// yet. + #[track_caller] + fn verify_cost(&self, testbed: &mut Testbed) -> GasCost { + self.estimate_average_cost(testbed, Self::verify_actions_cost) + } + + /// Estimate the cost for executing the actions in the builder. + /// + /// This is the "apply" cost only, without validation, without sending and + /// without overhead that does not scale with the number of actions. + #[track_caller] + fn apply_cost(&self, testbed: &mut Testbed) -> GasCost { + self.estimate_average_cost(testbed, Self::apply_actions_cost) + } + + /// Estimate the cost of verifying a set of actions once. + #[track_caller] + fn verify_actions_cost(&self, testbed: &mut Testbed, actions: Vec) -> GasCost { + let tb = testbed.transaction_builder(); + let signer_id = tb.account_by_requirement(self.signer, None); + let predecessor_id = tb.account_by_requirement(self.predecessor, Some(&signer_id)); + let receiver_id = tb.account_by_requirement(self.receiver, Some(&signer_id)); + let tx = tb.transaction_from_actions(predecessor_id, receiver_id, actions); + let clock = GasCost::measure(self.metric); + testbed.verify_transaction(&tx).expect("tx verification should not fail in estimator"); + clock.elapsed() + } + + /// Estimate the cost of applying a set of actions once. + #[track_caller] + fn apply_actions_cost(&self, testbed: &mut Testbed, actions: Vec) -> GasCost { + let tb = testbed.transaction_builder(); + + let signer_id = tb.account_by_requirement(self.signer, None); + let predecessor_id = tb.account_by_requirement(self.predecessor, Some(&signer_id)); + let receiver_id = tb.account_by_requirement(self.receiver, Some(&signer_id)); + let signer_public_key = PublicKey::from_seed(KeyType::ED25519, &signer_id); + + let action_receipt = ActionReceipt { + signer_id, + signer_public_key, + gas_price: 100_000_000, + output_data_receivers: vec![], + input_data_ids: vec![], + actions, + }; + let receipt = Receipt { + predecessor_id, + receiver_id, + receipt_id: CryptoHash::new(), + receipt: near_primitives::receipt::ReceiptEnum::Action(action_receipt), + }; + let clock = GasCost::measure(self.metric); + let outcome = testbed.apply_action_receipt(&receipt); + let gas = clock.elapsed(); + match outcome.status { + ExecutionStatus::Unknown => panic!("receipt not applied"), + ExecutionStatus::Failure(err) => panic!("failed apply, {err:?}"), + ExecutionStatus::SuccessValue(_) | ExecutionStatus::SuccessReceiptId(_) => (), + } + gas + } + + /// Take a function that executes a list of actions on a testbed, execute + /// and measure it multiple times and return the average cost. + #[track_caller] + fn estimate_average_cost( + &self, + testbed: &mut Testbed, + estimated_fn: fn(&Self, &mut Testbed, Vec) -> GasCost, + ) -> GasCost { + let num_total_actions = self.actions.len() * self.inner_iters; + let actions: Vec = + self.actions.iter().cloned().cycle().take(num_total_actions).collect(); + + let gas_results = iter::repeat_with(|| estimated_fn(self, testbed, actions.clone())) + .skip(self.warmup) + .take(self.outer_iters) + .collect(); + + // This could be cached for efficiency. But experience so far shows that + // reusing caches values for many future estimations leads to the + // problem that a single "HIGH-VARIANCE" uncertain estimation can spoil + // all following estimations. In this case, rerunning is cheap and it + // ensures the base is computed in a very similar state of the machine as + // the measurement it is subtracted from. + let base = + if self.subtract_base { estimated_fn(self, testbed, vec![]) } else { GasCost::zero() }; + + let cost_per_tx = average_cost(gas_results); + (cost_per_tx - base) / self.inner_iters as u64 + } +} + +pub(crate) fn create_account_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(create_account_action()) + .receiver(AccountRequirement::SubOfSigner) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn create_account_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(create_account_action()) + .receiver(AccountRequirement::SubOfSigner) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn create_account_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(create_account_action()) + .receiver(AccountRequirement::SubOfSigner) + .inner_iters(1) // creating account works only once in a receipt + .add_action(create_transfer_action()) // must have balance for storage + .apply_cost(&mut ctx.testbed()) +} + +pub(crate) fn delete_account_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(delete_account_action()) + .inner_iters(1) // only one account deletion possible + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn delete_account_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(delete_account_action()) + .inner_iters(1) // only one account deletion possible + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn delete_account_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(delete_account_action()) + .inner_iters(1) // only one account deletion possible + .apply_cost(&mut ctx.testbed()) +} + +pub(crate) fn deploy_contract_base_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(deploy_action(ActionSize::Min)) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn deploy_contract_base_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(deploy_action(ActionSize::Min)) + .verify_cost(&mut ctx.testbed()) +} + +/// Note: This is not the best estimation because a dummy contract is clearly +/// not the worst-case scenario for gas costs. +pub(crate) fn deploy_contract_base_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(deploy_action(ActionSize::Min)) + .apply_cost(&mut ctx.testbed()) +} + +pub(crate) fn deploy_contract_byte_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(deploy_action(ActionSize::Max)) + .inner_iters(1) // circumvent TX size limit + .verify_cost(&mut ctx.testbed()) + / ActionSize::Max.deploy_contract() +} + +pub(crate) fn deploy_contract_byte_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(deploy_action(ActionSize::Max)) + .inner_iters(1) // circumvent TX size limit + .verify_cost(&mut ctx.testbed()) + / ActionSize::Max.deploy_contract() +} + +pub(crate) fn deploy_contract_byte_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(deploy_action(ActionSize::Max)) + .inner_iters(1) // circumvent TX size limit + .apply_cost(&mut ctx.testbed()) + / ActionSize::Max.deploy_contract() +} + +pub(crate) fn function_call_base_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(function_call_action(ActionSize::Min)) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn function_call_base_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(function_call_action(ActionSize::Min)) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn function_call_base_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(function_call_action(ActionSize::Min)) + .apply_cost(&mut ctx.testbed()) +} + +pub(crate) fn function_call_byte_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(function_call_action(ActionSize::Max)) + .verify_cost(&mut ctx.testbed()) + / ActionSize::Max.function_call_payload() +} + +pub(crate) fn function_call_byte_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(function_call_action(ActionSize::Max)) + .verify_cost(&mut ctx.testbed()) + / ActionSize::Max.function_call_payload() +} + +pub(crate) fn function_call_byte_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(function_call_action(ActionSize::Max)) + .apply_cost(&mut ctx.testbed()) + / ActionSize::Max.function_call_payload() +} + +pub(crate) fn transfer_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx).add_action(transfer_action()).verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn transfer_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx).add_action(transfer_action()).verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn transfer_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx).add_action(transfer_action()).apply_cost(&mut ctx.testbed()) +} + +pub(crate) fn stake_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx).add_action(stake_action()).verify_cost(&mut ctx.testbed()) +} + +/// This is not a useful action, as staking only works with sender = receiver. +/// But since this fails only in the exec step, we must still charge a fitting +/// amount of gas in the send step. +pub(crate) fn stake_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(stake_action()) + .predecessor(AccountRequirement::SameAsSigner) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn stake_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(stake_action()) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) // staking must be local + .apply_cost(&mut ctx.testbed()) +} + +pub(crate) fn add_full_access_key_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(add_full_access_key_action()) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn add_full_access_key_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(add_full_access_key_action()) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn add_full_access_key_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(add_full_access_key_action()) + .inner_iters(1) // adding the same key a second time would fail + .apply_cost(&mut ctx.testbed()) +} + +pub(crate) fn add_function_call_key_base_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(add_fn_access_key_action(ActionSize::Min)) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn add_function_call_key_base_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(add_fn_access_key_action(ActionSize::Min)) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn add_function_call_key_base_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(add_fn_access_key_action(ActionSize::Min)) + .inner_iters(1) // adding the same key a second time would fail + .apply_cost(&mut ctx.testbed()) +} + +pub(crate) fn add_function_call_key_byte_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(add_fn_access_key_action(ActionSize::Max)) + .verify_cost(&mut ctx.testbed()) + / ActionSize::Max.key_methods_list() +} + +pub(crate) fn add_function_call_key_byte_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(add_fn_access_key_action(ActionSize::Max)) + .verify_cost(&mut ctx.testbed()) + / ActionSize::Max.key_methods_list() +} + +pub(crate) fn add_function_call_key_byte_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx) + .add_action(add_fn_access_key_action(ActionSize::Max)) + .inner_iters(1) // adding the same key a second time would fail + .apply_cost(&mut ctx.testbed()) + / ActionSize::Max.key_methods_list() +} + +pub(crate) fn delete_key_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx).add_action(delete_key_action()).verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn delete_key_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx).add_action(delete_key_action()).verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn delete_key_exec(ctx: &mut EstimatorContext) -> GasCost { + // Cannot delete a key without creating it first. Therefore, compute cost of + // (create) and of (create + delete) and return the difference. + let base_builder = ActionEstimation::new_sir(ctx) + .inner_iters(1) + .add_action(add_fn_access_key_action(ActionSize::Max)); + let base = base_builder.apply_cost(&mut ctx.testbed()); + let total = base_builder + .add_action(delete_key_action()) + .inner_iters(100) + .apply_cost(&mut ctx.testbed()); + + total - base +} + +pub(crate) fn new_action_receipt_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx).subtract_base(false).verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn new_action_receipt_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx).subtract_base(false).verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn new_action_receipt_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new_sir(ctx).subtract_base(false).apply_cost(&mut ctx.testbed()) +} + +fn create_account_action() -> Action { + Action::CreateAccount(near_primitives::transaction::CreateAccountAction {}) +} + +fn create_transfer_action() -> Action { + Action::Transfer(near_primitives::transaction::TransferAction { deposit: 10u128.pow(24) }) +} + +fn stake_action() -> Action { + Action::Stake(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 { + Action::DeleteAccount(near_primitives::transaction::DeleteAccountAction { + beneficiary_id: "bob.near".parse().unwrap(), + }) +} + +fn deploy_action(size: ActionSize) -> Action { + Action::DeployContract(near_primitives::transaction::DeployContractAction { + code: near_test_contracts::sized_contract(size.deploy_contract() as usize), + }) +} + +fn add_full_access_key_action() -> Action { + Action::AddKey(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 { + // 3 bytes for "foo" and one for an implicit separator + 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 { + public_key: PublicKey::from_seed(KeyType::ED25519, "seed"), + access_key: AccessKey { + nonce: 0, + permission: AccessKeyPermission::FunctionCall(FunctionCallPermission { + allowance: Some(1), + receiver_id, + method_names, + }), + }, + }) +} + +fn delete_key_action() -> Action { + Action::DeleteKey(near_primitives::transaction::DeleteKeyAction { + public_key: PublicKey::from_seed(KeyType::ED25519, "seed"), + }) +} + +fn transfer_action() -> Action { + Action::Transfer(near_primitives::transaction::TransferAction { deposit: 77 }) +} + +fn function_call_action(size: ActionSize) -> Action { + let total_size = size.function_call_payload(); + 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 { + 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), + }) +} + +/// Helper enum to select how large an action should be generated. +#[derive(Clone, Copy)] +enum ActionSize { + Min, + Max, +} + +impl ActionSize { + fn function_call_payload(self) -> u64 { + match self { + // calling "noop" requires 4 bytes + ActionSize::Min => 4, + // max_arguments_length: 4_194_304 + // max_transaction_size: 4_194_304 + ActionSize::Max => (4_194_304 / 100) - 35, + } + } + + fn key_methods_list(self) -> u64 { + match self { + ActionSize::Min => 0, + // max_number_bytes_method_names: 2000 + ActionSize::Max => 2000, + } + } + + fn deploy_contract(self) -> u64 { + match self { + // small number that still allows to generate a valid contract + ActionSize::Min => 120, + // max_number_bytes_method_names: 2000 + // This size exactly touches tx limit with 1 deploy action. If this suddenly + // fails with `InvalidTxError(TransactionSizeExceeded`, it could be a + // protocol change due to the TX limit computation changing. + ActionSize::Max => 4 * 1024 * 1024 - 160, + } + } +} diff --git a/runtime/runtime-params-estimator/src/cost.rs b/runtime/runtime-params-estimator/src/cost.rs index a8ad2428e7a..bc4e1ff3c48 100644 --- a/runtime/runtime-params-estimator/src/cost.rs +++ b/runtime/runtime-params-estimator/src/cost.rs @@ -34,6 +34,9 @@ pub enum Cost { /// Estimation: Measure the creation and execution of an empty action /// receipt, where sender and receiver are the same account. ActionSirReceiptCreation, + ActionReceiptCreationSendNotSir, + ActionReceiptCreationSendSir, + ActionReceiptCreationExec, /// Estimates `data_receipt_creation_config.base_cost`, which is charged for /// every data dependency of created receipts. This occurs either through /// calls to `promise_batch_then` or `value_return`. Dispatch and execution @@ -43,6 +46,9 @@ pub enum Cost { /// only one of them also creates a callback that depends on the promise /// results. The difference in execution cost is divided by 1000. DataReceiptCreationBase, + DataReceiptCreationBaseSendNotSir, + DataReceiptCreationBaseSendSir, + DataReceiptCreationBaseExec, /// Estimates `data_receipt_creation_config.cost_per_byte`, which is charged /// for every byte in data dependency of created receipts. This occurs /// either through calls to `promise_batch_then` or `value_return`. Dispatch @@ -53,6 +59,9 @@ pub enum Cost { /// creates small data receipts, the other large ones. The difference in /// execution cost is divided by the total byte difference. DataReceiptCreationPerByte, + DataReceiptCreationPerByteSendNotSir, + DataReceiptCreationPerByteSendSir, + DataReceiptCreationPerByteExec, /// Estimates `action_creation_config.create_account_cost` which is charged /// for `CreateAccount` actions, the same value on sending and executing. /// @@ -60,6 +69,9 @@ pub enum Cost { /// an initial balance to it. Subtract the base cost of creating a receipt. // TODO(jakmeier): consider also subtracting transfer fee ActionCreateAccount, + ActionCreateAccountSendNotSir, + ActionCreateAccountSendSir, + ActionCreateAccountExec, // Deploying a new contract for an account on the blockchain stores the WASM // code in the trie. Additionally, it also triggers a compilation of the // code to check that it is valid WASM. The compiled code is then stored in @@ -72,6 +84,9 @@ pub enum Cost { /// /// Estimation: Measure deployment cost of a "smallest" contract. ActionDeployContractBase, + ActionDeployContractBaseSendNotSir, + ActionDeployContractBaseSendSir, + ActionDeployContractBaseExec, /// Estimates `action_creation_config.deploy_contract_cost_per_byte`, which /// is charged for every byte in the WASM code when deploying the contract /// @@ -79,6 +94,9 @@ pub enum Cost { /// a transaction. Subtract base costs and apply least-squares on the /// results to find the per-byte costs. ActionDeployContractPerByte, + ActionDeployContractPerByteSendNotSir, + ActionDeployContractPerByteSendSir, + ActionDeployContractPerByteExec, /// Estimates `action_creation_config.function_call_cost`, which is the base /// cost for adding a `FunctionCallAction` to a receipt. It aims to account /// for all costs of calling a function that are already known on the caller @@ -90,6 +108,9 @@ pub enum Cost { /// transaction is divided by N. Executable loading cost is also subtracted /// from the final result because this is charged separately. ActionFunctionCallBase, + ActionFunctionCallBaseSendNotSir, + ActionFunctionCallBaseSendSir, + ActionFunctionCallBaseExec, /// Estimates `action_creation_config.function_call_cost_per_byte`, which is /// the incremental cost for each byte of the method name and method /// arguments cost for adding a `FunctionCallAction` to a receipt. @@ -99,20 +120,29 @@ pub enum Cost { /// call with no argument. Divide the difference by the length of the /// argument. ActionFunctionCallPerByte, + ActionFunctionCallPerByteSendNotSir, + ActionFunctionCallPerByteSendSir, + ActionFunctionCallPerByteExec, /// Estimates `action_creation_config.transfer_cost` which is charged for /// every `Action::Transfer`, the same value for sending and executing. /// /// Estimation: Measure a transaction with only a transfer and subtract the /// base cost of creating a receipt. ActionTransfer, + ActionTransferSendNotSir, + ActionTransferSendSir, + ActionTransferExec, /// Estimates `action_creation_config.stake_cost` which is charged for every /// `Action::Stake`, a slightly higher value for sending than executing. /// /// Estimation: Measure a transaction with only a staking action and /// subtract the base cost of creating a sir-receipt. - // TODO(jakmeier): find out and document the reasoning behind send vs exec - // values in this specific case + /// + /// Note: The exec cost is probably a copy-paste mistake. (#8185) ActionStake, + ActionStakeSendNotSir, + ActionStakeSendSir, + ActionStakeExec, /// Estimates `action_creation_config.add_key_cost.full_access_cost` which /// is charged for every `Action::AddKey` where the key is a full access /// key. The same value is charged for sending and executing. @@ -120,6 +150,9 @@ pub enum Cost { /// Estimation: Measure a transaction that adds a full access key and /// subtract the base cost of creating a sir-receipt. ActionAddFullAccessKey, + ActionAddFullAccessKeySendNotSir, + ActionAddFullAccessKeySendSir, + ActionAddFullAccessKeyExec, /// Estimates `action_creation_config.add_key_cost.function_call_cost` which /// is charged once for every `Action::AddKey` where the key is a function /// call key. The same value is charged for sending and executing. @@ -127,6 +160,9 @@ pub enum Cost { /// Estimation: Measure a transaction that adds a function call key and /// subtract the base cost of creating a sir-receipt. ActionAddFunctionAccessKeyBase, + ActionAddFunctionAccessKeyBaseSendNotSir, + ActionAddFunctionAccessKeyBaseSendSir, + ActionAddFunctionAccessKeyBaseExec, /// Estimates /// `action_creation_config.add_key_cost.function_call_cost_per_byte` which /// is charged once for every byte in null-terminated method names listed in @@ -138,6 +174,9 @@ pub enum Cost { /// single method and of creating a sir-receipt. The result is divided by /// total bytes in the method names. ActionAddFunctionAccessKeyPerByte, + ActionAddFunctionAccessKeyPerByteSendNotSir, + ActionAddFunctionAccessKeyPerByteSendSir, + ActionAddFunctionAccessKeyPerByteExec, /// Estimates `action_creation_config.delete_key_cost` which is charged for /// `DeleteKey` actions, the same value on sending and executing. It does /// not matter whether it is a function call or full access key. @@ -147,6 +186,9 @@ pub enum Cost { /// receipt. // TODO(jakmeier): check cost for function call keys with many methods ActionDeleteKey, + ActionDeleteKeySendNotSir, + ActionDeleteKeySendSir, + ActionDeleteKeyExec, /// Estimates `action_creation_config.delete_account_cost` which is charged /// for `DeleteAccount` actions, the same value on sending and executing. /// @@ -154,6 +196,9 @@ pub enum Cost { /// Subtract the base cost of creating a sir-receipt. /// TODO(jakmeier): Consider different account states. ActionDeleteAccount, + ActionDeleteAccountSendNotSir, + ActionDeleteAccountSendSir, + ActionDeleteAccountExec, /// Estimates `wasm_config.ext_costs.base` which is intended to be charged /// once on every host function call. However, this is currently diff --git a/runtime/runtime-params-estimator/src/estimator_context.rs b/runtime/runtime-params-estimator/src/estimator_context.rs index 47f0f8426f3..8903d404877 100644 --- a/runtime/runtime-params-estimator/src/estimator_context.rs +++ b/runtime/runtime-params-estimator/src/estimator_context.rs @@ -7,14 +7,15 @@ use near_primitives::receipt::Receipt; use near_primitives::runtime::config_store::RuntimeConfigStore; use near_primitives::runtime::migration_data::{MigrationData, MigrationFlags}; use near_primitives::test_utils::MockEpochInfoProvider; -use near_primitives::transaction::{ExecutionStatus, SignedTransaction}; +use near_primitives::transaction::{ExecutionOutcome, ExecutionStatus, SignedTransaction}; use near_primitives::types::{Gas, MerkleHash}; use near_primitives::version::PROTOCOL_VERSION; -use near_store::{ShardTries, ShardUId, Store, StoreCompiledContractCache}; +use near_store::{ShardTries, ShardUId, Store, StoreCompiledContractCache, TrieUpdate}; use near_store::{TrieCache, TrieCachingStorage, TrieConfig}; use near_vm_logic::{ExtCosts, VMLimitConfig}; use node_runtime::{ApplyState, Runtime}; use std::collections::HashMap; +use std::rc::Rc; use std::sync::Arc; /// Global context shared by all cost calculating functions. @@ -240,10 +241,11 @@ impl Testbed<'_> { transactions: &[SignedTransaction], allow_failures: bool, ) -> Gas { + let trie = self.trie(); let apply_result = self .runtime .apply( - self.tries.get_trie_for_shard(ShardUId::single_shard(), self.root.clone()), + trie, &None, &self.apply_state, &self.prev_receipts, @@ -285,4 +287,59 @@ impl Testbed<'_> { } n } + + /// Process just the verification of a transaction, without action execution. + /// + /// Use this method for measuring the SEND cost of actions. This is the + /// workload done on the sender's shard before an action receipt is created. + /// Network costs for sending are not included. + pub(crate) fn verify_transaction( + &mut self, + tx: &SignedTransaction, + ) -> Result { + let mut state_update = TrieUpdate::new(Rc::new(self.trie())); + // gas price and block height can be anything, it doesn't affect performance + // but making it too small affects max_depth and thus pessimistic inflation + let gas_price = 100_000_000; + let block_height = None; + // do a full verification + let verify_signature = true; + node_runtime::verify_and_charge_transaction( + &self.apply_state.config, + &mut state_update, + gas_price, + tx, + verify_signature, + block_height, + PROTOCOL_VERSION, + ) + } + + /// Process only the execution step of an action receipt. + /// + /// Use this method to estimate action exec costs. + pub(crate) fn apply_action_receipt(&mut self, receipt: &Receipt) -> ExecutionOutcome { + let mut state_update = TrieUpdate::new(Rc::new(self.trie())); + let mut outgoing_receipts = vec![]; + let mut validator_proposals = vec![]; + let mut stats = node_runtime::ApplyStats::default(); + // TODO: mock is not accurate, potential DB requests are skipped in the mock! + let epoch_info_provider = MockEpochInfoProvider::new([].into_iter()); + let exec_result = node_runtime::estimator::apply_action_receipt( + &mut state_update, + &self.apply_state, + receipt, + &mut outgoing_receipts, + &mut validator_proposals, + &mut stats, + &epoch_info_provider, + ) + .expect("applying receipt in estimator should not fail"); + exec_result.outcome + } + + /// Instantiate a new trie for the estimator. + fn trie(&mut self) -> near_store::Trie { + self.tries.get_trie_for_shard(ShardUId::single_shard(), self.root.clone()) + } } diff --git a/runtime/runtime-params-estimator/src/lib.rs b/runtime/runtime-params-estimator/src/lib.rs index a5335c6e67b..f31766d1a3c 100644 --- a/runtime/runtime-params-estimator/src/lib.rs +++ b/runtime/runtime-params-estimator/src/lib.rs @@ -57,6 +57,7 @@ //! digging deeper. //! +mod action_costs; mod cost; mod cost_table; mod costs_to_runtime_config; @@ -128,18 +129,69 @@ pub use crate::rocksdb::RocksDBTestConfig; static ALL_COSTS: &[(Cost, fn(&mut EstimatorContext) -> GasCost)] = &[ (Cost::ActionReceiptCreation, action_receipt_creation), (Cost::ActionSirReceiptCreation, action_sir_receipt_creation), + (Cost::ActionReceiptCreationSendSir, action_costs::new_action_receipt_send_sir), + (Cost::ActionReceiptCreationSendNotSir, action_costs::new_action_receipt_send_not_sir), + (Cost::ActionReceiptCreationExec, action_costs::new_action_receipt_exec), (Cost::ActionTransfer, action_transfer), + (Cost::ActionTransferSendSir, action_costs::transfer_send_sir), + (Cost::ActionTransferSendNotSir, action_costs::transfer_send_not_sir), + (Cost::ActionTransferExec, action_costs::transfer_exec), (Cost::ActionCreateAccount, action_create_account), + (Cost::ActionCreateAccountSendSir, action_costs::create_account_send_sir), + (Cost::ActionCreateAccountSendNotSir, action_costs::create_account_send_not_sir), + (Cost::ActionCreateAccountExec, action_costs::create_account_exec), (Cost::ActionDeleteAccount, action_delete_account), + (Cost::ActionDeleteAccountSendSir, action_costs::delete_account_send_sir), + (Cost::ActionDeleteAccountSendNotSir, action_costs::delete_account_send_not_sir), + (Cost::ActionDeleteAccountExec, action_costs::delete_account_exec), (Cost::ActionAddFullAccessKey, action_add_full_access_key), + (Cost::ActionAddFullAccessKeySendSir, action_costs::add_full_access_key_send_sir), + (Cost::ActionAddFullAccessKeySendNotSir, action_costs::add_full_access_key_send_not_sir), + (Cost::ActionAddFullAccessKeyExec, action_costs::add_full_access_key_exec), (Cost::ActionAddFunctionAccessKeyBase, action_add_function_access_key_base), + ( + Cost::ActionAddFunctionAccessKeyBaseSendSir, + action_costs::add_function_call_key_base_send_sir, + ), + ( + Cost::ActionAddFunctionAccessKeyBaseSendNotSir, + action_costs::add_function_call_key_base_send_not_sir, + ), + (Cost::ActionAddFunctionAccessKeyBaseExec, action_costs::add_function_call_key_base_exec), (Cost::ActionAddFunctionAccessKeyPerByte, action_add_function_access_key_per_byte), + ( + Cost::ActionAddFunctionAccessKeyPerByteSendSir, + action_costs::add_function_call_key_byte_send_sir, + ), + ( + Cost::ActionAddFunctionAccessKeyPerByteSendNotSir, + action_costs::add_function_call_key_byte_send_not_sir, + ), + (Cost::ActionAddFunctionAccessKeyPerByteExec, action_costs::add_function_call_key_byte_exec), (Cost::ActionDeleteKey, action_delete_key), + (Cost::ActionDeleteKeySendSir, action_costs::delete_key_send_sir), + (Cost::ActionDeleteKeySendNotSir, action_costs::delete_key_send_not_sir), + (Cost::ActionDeleteKeyExec, action_costs::delete_key_exec), (Cost::ActionStake, action_stake), + (Cost::ActionStakeSendNotSir, action_costs::stake_send_not_sir), + (Cost::ActionStakeSendSir, action_costs::stake_send_sir), + (Cost::ActionStakeExec, action_costs::stake_exec), (Cost::ActionDeployContractBase, action_deploy_contract_base), + (Cost::ActionDeployContractBaseSendNotSir, action_costs::deploy_contract_base_send_not_sir), + (Cost::ActionDeployContractBaseSendSir, action_costs::deploy_contract_base_send_sir), + (Cost::ActionDeployContractBaseExec, action_costs::deploy_contract_base_exec), (Cost::ActionDeployContractPerByte, action_deploy_contract_per_byte), + (Cost::ActionDeployContractPerByteSendNotSir, action_costs::deploy_contract_byte_send_not_sir), + (Cost::ActionDeployContractPerByteSendSir, action_costs::deploy_contract_byte_send_sir), + (Cost::ActionDeployContractPerByteExec, action_costs::deploy_contract_byte_exec), (Cost::ActionFunctionCallBase, action_function_call_base), + (Cost::ActionFunctionCallBaseSendNotSir, action_costs::function_call_base_send_not_sir), + (Cost::ActionFunctionCallBaseSendSir, action_costs::function_call_base_send_sir), + (Cost::ActionFunctionCallBaseExec, action_costs::function_call_base_exec), (Cost::ActionFunctionCallPerByte, action_function_call_per_byte), + (Cost::ActionFunctionCallPerByteSendNotSir, action_costs::function_call_byte_send_not_sir), + (Cost::ActionFunctionCallPerByteSendSir, action_costs::function_call_byte_send_sir), + (Cost::ActionFunctionCallPerByteExec, action_costs::function_call_byte_exec), (Cost::HostFunctionCall, host_function_call), (Cost::WasmInstruction, wasm_instruction), (Cost::DataReceiptCreationBase, data_receipt_creation_base), diff --git a/runtime/runtime-params-estimator/src/transaction_builder.rs b/runtime/runtime-params-estimator/src/transaction_builder.rs index ef56a63f3d7..6ad09e264df 100644 --- a/runtime/runtime-params-estimator/src/transaction_builder.rs +++ b/runtime/runtime-params-estimator/src/transaction_builder.rs @@ -18,6 +18,18 @@ pub(crate) struct TransactionBuilder { unused_index: usize, } +/// Define how accounts should be generated. +#[derive(Clone, Copy)] +pub(crate) enum AccountRequirement { + /// Use a different random account on every iteration, account exists and + /// has estimator contract deployed. + RandomUnused, + /// Use the same account as the signer. Must not be used for signer id. + SameAsSigner, + /// Use sub account of the signer. Useful for `CreateAction` estimations. + SubOfSigner, +} + impl TransactionBuilder { pub(crate) fn new(accounts: Vec) -> TransactionBuilder { let n = accounts.len(); @@ -120,6 +132,20 @@ impl TransactionBuilder { } } + pub(crate) fn account_by_requirement( + &mut self, + src: AccountRequirement, + signer_id: Option<&AccountId>, + ) -> AccountId { + match src { + AccountRequirement::RandomUnused => self.random_unused_account(), + AccountRequirement::SameAsSigner => signer_id.expect("no signer_id provided").clone(), + AccountRequirement::SubOfSigner => { + format!("sub.{}", signer_id.expect("no signer_id")).parse().unwrap() + } + } + } + pub(crate) fn random_vec(&mut self, len: usize) -> Vec { (0..len).map(|_| self.rng().gen()).collect() } diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index baea4da1b63..c7edaf6ac59 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -2449,3 +2449,36 @@ mod tests { .expect("Compilation result should be non-empty"); } } + +/// Interface provided for gas cost estimations. +pub mod estimator { + use super::Runtime; + use crate::ApplyStats; + use near_primitives::errors::RuntimeError; + use near_primitives::receipt::Receipt; + use near_primitives::runtime::apply_state::ApplyState; + use near_primitives::transaction::ExecutionOutcomeWithId; + use near_primitives::types::validator_stake::ValidatorStake; + use near_primitives::types::EpochInfoProvider; + use near_store::TrieUpdate; + + pub fn apply_action_receipt( + state_update: &mut TrieUpdate, + apply_state: &ApplyState, + receipt: &Receipt, + outgoing_receipts: &mut Vec, + validator_proposals: &mut Vec, + stats: &mut ApplyStats, + epoch_info_provider: &dyn EpochInfoProvider, + ) -> Result { + Runtime {}.apply_action_receipt( + state_update, + apply_state, + receipt, + outgoing_receipts, + validator_proposals, + stats, + epoch_info_provider, + ) + } +} From cf8fd8183b7d1901b3ac915e5ec73aa52744eb01 Mon Sep 17 00:00:00 2001 From: Ekleog-NEAR <96595974+Ekleog-NEAR@users.noreply.github.com> Date: Thu, 22 Dec 2022 14:36:37 +0100 Subject: [PATCH 127/188] wasmer2: do not re-validate wasm code after instrumentation (#8007) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Performance characteristics: between 5% and 15% wall-clock gain depending on the test sample. See https://github.com/near/nearcore-private/issues/1 for more details Safety characteristics: I ran a comparative fuzzer between wasmer2 with and without secondary validation for 2 weeks on an older commit, plus 4 days on this specific commit to validate the rebase. The fuzzer was not able to find any difference in behavior between the two. In addition, as far as I can tell validation already happens in prepare_contract’s call to validate_contract (within wasmparser_decode at the validator.validate(&body) line), so removing this second validation should be fine. However, it does mean that we accept including instrumentation within our "trusted code base" of parts that if they fail could cause harm. In theory wasmer2 should not cause UB if passed unvalidated input, but even if this is true it could definitely panic if passed unvalidated input. As such, this is still a somewhat risky change to make. However, seeing how instrumentation is relatively simple wasm code addition and how it passed meticulous fuzzing, I think it has been derisked enough for the performance gains to outweigh the third-layer-of-security loss. --- Cargo.lock | 152 +++++-------------- Cargo.toml | 2 +- runtime/near-vm-runner/Cargo.toml | 1 + runtime/near-vm-runner/src/prepare.rs | 24 +++ runtime/near-vm-runner/src/wasmer2_runner.rs | 10 +- 5 files changed, 71 insertions(+), 118 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abe200c9e34..afcc30ee9ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,7 +12,7 @@ dependencies = [ "actix_derive", "bitflags", "bytes", - "crossbeam-channel 0.5.4", + "crossbeam-channel", "futures-core", "futures-sink", "futures-task", @@ -560,25 +560,25 @@ dependencies = [ [[package]] name = "bolero" -version = "0.6.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256a795047239482fdc98374c3a050d51ca8921427c842e089b3bd7267659427" +checksum = "3387d308f66ed222bdbb19c6ba06b1517168c4e45dc64051c5f1b4845db2901c" dependencies = [ "bolero-afl", "bolero-engine", "bolero-generator", "bolero-honggfuzz", + "bolero-kani", "bolero-libfuzzer", "cfg-if 1.0.0", - "libtest-mimic", "rand 0.8.5", ] [[package]] name = "bolero-afl" -version = "0.6.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c2595f3cc558c84e285318f8d29d3552140ecd106dbf3a356094898dc5619" +checksum = "973bc6341b6a865dee93f17b78de4a100551014a527798ff1d7265d3bc0f7d89" dependencies = [ "bolero-engine", "cc", @@ -586,9 +586,9 @@ dependencies = [ [[package]] name = "bolero-engine" -version = "0.6.2" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "839667421d443c03ca5746ec1c1b8db10d305fdb062f6f20c62b3f4cfcd431b5" +checksum = "5c506a476cea9e95f58c264b343ee279c353d93ceaebe98cbfb16e74bfaee2e2" dependencies = [ "anyhow", "backtrace", @@ -600,21 +600,20 @@ dependencies = [ [[package]] name = "bolero-generator" -version = "0.6.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5ff9a4b0a1f80c09e3a35c4dc47a3bed344e5a431f2b96ca74952beb6c0767" +checksum = "48d52eca8714d110e581cf17eeacf0d1a0d409d38a9e9ce07efeda6125f7febb" dependencies = [ "bolero-generator-derive", - "byteorder", "either", "rand_core 0.6.3", ] [[package]] name = "bolero-generator-derive" -version = "0.6.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a2ef03f5627ff547424f470cdf527bc5c7551ec48bd560f3a0e794d0082c6f9" +checksum = "3b3c57c2a0967ad1a09ba4c2bf8f1c6b6db2f71e8c0db4fa280c65a0f6c249c3" dependencies = [ "proc-macro2", "quote", @@ -623,18 +622,27 @@ dependencies = [ [[package]] name = "bolero-honggfuzz" -version = "0.6.2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7996a3fa8d93652358b9b3b805233807168f49740a8bf91a531cd61e4da65355" +dependencies = [ + "bolero-engine", +] + +[[package]] +name = "bolero-kani" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dc187a50ea23588958b0160113a742181b09ba4dba8412072c5e311a062bb4b" +checksum = "206879993fffa1cf2c703b1ef93b0febfa76bae85a0a5d4ae0ee6d99a2e3b74e" dependencies = [ "bolero-engine", ] [[package]] name = "bolero-libfuzzer" -version = "0.6.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7734f24b16e80871f6a54e636e0db8338c22eea957685b4751e29b1dce1a5b" +checksum = "cdc5547411b84703d9020914f15a7d709cfb738c72b5e0f5a499fe56b8465c98" dependencies = [ "bolero-engine", "cc", @@ -936,13 +944,9 @@ version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ - "ansi_term", - "atty", "bitflags", - "strsim 0.8.0", "textwrap 0.11.0", "unicode-width", - "vec_map", ] [[package]] @@ -957,7 +961,7 @@ dependencies = [ "clap_lex", "indexmap", "lazy_static", - "strsim 0.10.0", + "strsim", "termcolor", "textwrap 0.15.0", ] @@ -1237,21 +1241,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" dependencies = [ "cfg-if 1.0.0", - "crossbeam-channel 0.5.4", + "crossbeam-channel", "crossbeam-deque", "crossbeam-epoch", "crossbeam-queue", - "crossbeam-utils 0.8.8", -] - -[[package]] -name = "crossbeam-channel" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" -dependencies = [ - "crossbeam-utils 0.7.2", - "maybe-uninit", + "crossbeam-utils", ] [[package]] @@ -1261,7 +1255,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.8", + "crossbeam-utils", ] [[package]] @@ -1272,7 +1266,7 @@ checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" dependencies = [ "cfg-if 1.0.0", "crossbeam-epoch", - "crossbeam-utils 0.8.8", + "crossbeam-utils", ] [[package]] @@ -1283,7 +1277,7 @@ checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" dependencies = [ "autocfg", "cfg-if 1.0.0", - "crossbeam-utils 0.8.8", + "crossbeam-utils", "lazy_static", "memoffset", "scopeguard", @@ -1296,18 +1290,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd42583b04998a5363558e5f9291ee5a5ff6b49944332103f251e7479a82aa7" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.8", -] - -[[package]] -name = "crossbeam-utils" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" -dependencies = [ - "autocfg", - "cfg-if 0.1.10", - "lazy_static", + "crossbeam-utils", ] [[package]] @@ -2514,18 +2497,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "libtest-mimic" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a7b8ac1f53f7be8d895ce6f7f534e49581c85c499b47429634b2cb2995e2ae" -dependencies = [ - "crossbeam-channel 0.4.4", - "rayon", - "structopt", - "termcolor", -] - [[package]] name = "libz-sys" version = "1.1.6" @@ -2660,12 +2631,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" -[[package]] -name = "maybe-uninit" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" - [[package]] name = "memchr" version = "2.5.0" @@ -2873,7 +2838,7 @@ dependencies = [ "assert_matches", "borsh", "chrono", - "crossbeam-channel 0.5.4", + "crossbeam-channel", "delay-detector", "enum-map", "insta", @@ -3284,7 +3249,7 @@ dependencies = [ "bytesize", "chrono", "criterion", - "crossbeam-channel 0.5.4", + "crossbeam-channel", "delay-detector", "futures", "futures-util", @@ -3657,6 +3622,7 @@ dependencies = [ "bolero", "borsh", "expect-test", + "hex", "loupe", "memoffset", "near-cache", @@ -4030,7 +3996,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6105e89802af13fdf48c49d7646d3b533a70e536d818aae7e78ba0433d01acb8" dependencies = [ "async-trait", - "crossbeam-channel 0.5.4", + "crossbeam-channel", "futures-channel", "futures-executor", "futures-util", @@ -4332,9 +4298,9 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "pretty-hex" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131" +checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" [[package]] name = "pretty_assertions" @@ -4696,9 +4662,9 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" dependencies = [ - "crossbeam-channel 0.5.4", + "crossbeam-channel", "crossbeam-deque", - "crossbeam-utils 0.8.8", + "crossbeam-utils", "num_cpus", ] @@ -5545,42 +5511,12 @@ dependencies = [ "testlib", ] -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "structopt" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" -dependencies = [ - "clap 2.34.0", - "lazy_static", - "structopt-derive", -] - -[[package]] -name = "structopt-derive" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" -dependencies = [ - "heck 0.3.3", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "strum" version = "0.24.0" @@ -6051,7 +5987,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" dependencies = [ - "crossbeam-channel 0.5.4", + "crossbeam-channel", "time 0.3.9", "tracing-subscriber", ] @@ -6249,12 +6185,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index d12adb67eca..00d560e5b79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,7 +86,7 @@ bencher = "0.1.5" bitflags = "1.2" blake2 = "0.9.1" bn = { package = "zeropool-bn", version = "0.5.11" } -bolero = "0.6.2" +bolero = "0.8.0" borsh = { version = "0.9", features = ["rc"] } bs58 = "0.4" byteorder = "1.3" diff --git a/runtime/near-vm-runner/Cargo.toml b/runtime/near-vm-runner/Cargo.toml index 5d53160132f..8dd6001678e 100644 --- a/runtime/near-vm-runner/Cargo.toml +++ b/runtime/near-vm-runner/Cargo.toml @@ -56,6 +56,7 @@ arbitrary.workspace = true assert_matches.workspace = true bolero.workspace = true expect-test.workspace = true +hex.workspace = true rand.workspace = true wasm-smith.workspace = true wasmprinter.workspace = true diff --git a/runtime/near-vm-runner/src/prepare.rs b/runtime/near-vm-runner/src/prepare.rs index f42a02bd516..37e6784f704 100644 --- a/runtime/near-vm-runner/src/prepare.rs +++ b/runtime/near-vm-runner/src/prepare.rs @@ -492,4 +492,28 @@ mod tests { assert_matches!(r, Err(Error::Instantiate)); */ } + + #[test] + fn preparation_generates_valid_contract() { + bolero::check!().for_each(|input: &[u8]| { + // DO NOT use ArbitraryModule. We do want modules that may be invalid here, if they pass our validation step! + let config = VMConfig::test(); + if let Ok(_) = validate_contract(input, &config) { + match prepare_contract(input, &config) { + Err(_e) => (), // TODO: this should be a panic, but for now it’d actually trigger + Ok(code) => { + let mut validator = wasmparser::Validator::new(); + validator.wasm_features(WASM_FEATURES); + match validator.validate_all(&code) { + Ok(()) => (), + Err(e) => panic!( + "prepared code failed validation: {e:?}\ncontract: {}", + hex::encode(input), + ), + } + } + } + } + }); + } } diff --git a/runtime/near-vm-runner/src/wasmer2_runner.rs b/runtime/near-vm-runner/src/wasmer2_runner.rs index 9208d21b527..4229ac93a8f 100644 --- a/runtime/near-vm-runner/src/wasmer2_runner.rs +++ b/runtime/near-vm-runner/src/wasmer2_runner.rs @@ -268,12 +268,10 @@ impl Wasmer2VM { let prepared_code = prepare::prepare_contract(code.code(), &self.config) .map_err(CompilationError::PrepareError)?; - self.engine - .validate(&prepared_code) - .map_err(|err| { - tracing::error!(?err, "wasmer failed to validate the prepared code (this is defense-in-depth, the error was recovered from but should be reported to pagoda)"); - CompilationError::WasmerCompileError { msg: err.to_string() } - })?; + debug_assert!( + matches!(self.engine.validate(&prepared_code), Ok(_)), + "wasmer failed to validate the prepared code" + ); let executable = self .engine .compile_universal(&prepared_code, &self) From 04969dce5cfa385e2a07ffa71115a0324e9580cb Mon Sep 17 00:00:00 2001 From: Marcelo Diop-Gonzalez Date: Thu, 22 Dec 2022 09:41:02 -0500 Subject: [PATCH 128/188] features: remove the memory_stats feature and near-rust-allocator-proxy (#8268) setting the global allocator to the one provided by near-rust-allocator-proxy leads to different sorts of problems (deadlocks and invalid thread local accesses), and that won't change until/unless it's rewritten somewhat. For now, we can't really use it correctly, so we'll just remove it. Shouldn't be too hard to re-add in the future if it's fixed. See these issues: https://github.com/near/near-memory-tracker/issues/103 https://github.com/near/near-memory-tracker/issues/102 As a result of this, perf builds no longer print memory stats in logs. Also we're just going to use `std::thread::current().id()` instead of `near_rust_allocator_proxy::get_tid()` (and we won't sort them in logs), but this shouldn't be too much of a problem --- Cargo.lock | 15 ---- Cargo.toml | 1 - Makefile | 23 +++-- chain/network/Cargo.toml | 2 - core/o11y/src/lib.rs | 1 - nearcore/Cargo.toml | 3 - nearcore/src/lib.rs | 6 -- neard/Cargo.toml | 2 - neard/src/main.rs | 8 -- utils/near-performance-metrics/Cargo.toml | 4 +- .../src/actix_enabled.rs | 5 +- .../src/stats_enabled.rs | 86 ++++--------------- 12 files changed, 33 insertions(+), 123 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index afcc30ee9ad..3a4e3f45187 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3261,7 +3261,6 @@ dependencies = [ "near-performance-metrics", "near-performance-metrics-macros", "near-primitives", - "near-rust-allocator-proxy", "near-stable-hasher", "near-store", "once_cell", @@ -3323,7 +3322,6 @@ dependencies = [ "bytesize", "futures", "libc", - "near-rust-allocator-proxy", "once_cell", "strum", "tokio", @@ -3474,17 +3472,6 @@ dependencies = [ "syn", ] -[[package]] -name = "near-rust-allocator-proxy" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be44da452581a4f2e7870d86886f50605853943ded9b6a7975495914645cdca4" -dependencies = [ - "backtrace", - "nix", - "tracing", -] - [[package]] name = "near-stable-hasher" version = "0.0.0" @@ -3706,7 +3693,6 @@ dependencies = [ "near-pool", "near-primitives", "near-rosetta-rpc", - "near-rust-allocator-proxy", "near-store", "near-telemetry", "near-vm-runner", @@ -3750,7 +3736,6 @@ dependencies = [ "near-performance-metrics", "near-ping", "near-primitives", - "near-rust-allocator-proxy", "near-state-parts", "near-store", "nearcore", diff --git a/Cargo.toml b/Cargo.toml index 00d560e5b79..181f79e1f2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -130,7 +130,6 @@ log = "0.4" loupe = "0.1" lru = "0.7.2" memoffset = "0.6" -near-rust-allocator-proxy = "0.4" nix = "0.15.0" num-bigint = "0.3" num-rational = { version = "0.3.1", features = ["serde"] } diff --git a/Makefile b/Makefile index 243fe66ef99..758b8095574 100644 --- a/Makefile +++ b/Makefile @@ -44,33 +44,32 @@ debug: neard-debug perf-release: NEAR_RELEASE_BUILD=release perf-release: - CARGO_PROFILE_RELEASE_DEBUG=true cargo build -p neard --release --features performance_stats,memory_stats - cargo build -p store-validator --release --features nearcore/performance_stats,nearcore/memory_stats + CARGO_PROFILE_RELEASE_DEBUG=true cargo build -p neard --release --features performance_stats + cargo build -p store-validator --release --features nearcore/performance_stats perf-debug: - cargo build -p neard --features performance_stats,memory_stats - cargo build -p store-validator --features nearcore/performance_stats,nearcore/memory_stats + cargo build -p neard --features performance_stats + cargo build -p store-validator --features nearcore/performance_stats nightly-release: neard-nightly-release - cargo build -p store-validator --release --features nearcore/nightly,nearcore/performance_stats,nearcore/memory_stats - cargo build -p genesis-populate --release --features nearcore/nightly,nearcore/performance_stats,nearcore/memory_stats + cargo build -p store-validator --release --features nearcore/nightly,nearcore/performance_stats + cargo build -p genesis-populate --release --features nearcore/nightly,nearcore/performance_stats neard-nightly-release: - cargo build -p neard --release --features nightly,performance_stats,memory_stats + cargo build -p neard --release --features nightly,performance_stats nightly-debug: - cargo build -p neard --features nightly,performance_stats,memory_stats - cargo build -p store-validator --features nearcore/nightly,nearcore/performance_stats,nearcore/memory_stats - cargo build -p genesis-populate --features nearcore/nightly,nearcore/performance_stats,nearcore/memory_stats + cargo build -p neard --features nightly,performance_stats + cargo build -p store-validator --features nearcore/nightly,nearcore/performance_stats + cargo build -p genesis-populate --features nearcore/nightly,nearcore/performance_stats assertions-release: NEAR_RELEASE_BUILD=release assertions-release: - CARGO_PROFILE_RELEASE_DEBUG=true CARGO_PROFILE_RELEASE_DEBUG_ASSERTIONS=true cargo build -p neard --release --features performance_stats,memory_stats - + CARGO_PROFILE_RELEASE_DEBUG=true CARGO_PROFILE_RELEASE_DEBUG_ASSERTIONS=true cargo build -p neard --release --features performance_stats sandbox: CARGO_TARGET_DIR=sandbox sandbox: neard-sandbox diff --git a/chain/network/Cargo.toml b/chain/network/Cargo.toml index 82136551947..359be249f6f 100644 --- a/chain/network/Cargo.toml +++ b/chain/network/Cargo.toml @@ -25,7 +25,6 @@ futures.workspace = true im.workspace = true itertools.workspace = true lru.workspace = true -near-rust-allocator-proxy = { workspace = true, optional = true } once_cell.workspace = true opentelemetry.workspace = true parking_lot.workspace = true @@ -62,6 +61,5 @@ rlimit.workspace = true delay_detector = ["delay-detector/delay_detector"] performance_stats = [ "near-performance-metrics/performance_stats", - "near-rust-allocator-proxy", ] test_features = [] diff --git a/core/o11y/src/lib.rs b/core/o11y/src/lib.rs index da868093547..f432c825382 100644 --- a/core/o11y/src/lib.rs +++ b/core/o11y/src/lib.rs @@ -87,7 +87,6 @@ pub const DEFAULT_RUST_LOG: &'static str = "tokio_reactor=info,\ db=info,\ delay_detector=info,\ near-performance-metrics=info,\ - near-rust-allocator-proxy=info,\ warn"; /// The resource representing a registered subscriber. diff --git a/nearcore/Cargo.toml b/nearcore/Cargo.toml index 12e9a2ddac7..eda7102ee5c 100644 --- a/nearcore/Cargo.toml +++ b/nearcore/Cargo.toml @@ -20,7 +20,6 @@ futures.workspace = true hyper-tls.workspace = true hyper.workspace = true indicatif.workspace = true -near-rust-allocator-proxy = { workspace = true, optional = true } num-rational.workspace = true once_cell.workspace = true rand.workspace = true @@ -77,9 +76,7 @@ default = ["json_rpc", "rosetta_rpc"] performance_stats = [ "near-performance-metrics/performance_stats", - "near-rust-allocator-proxy", ] -memory_stats = ["near-performance-metrics/memory_stats"] c_memory_stats = ["near-performance-metrics/c_memory_stats"] test_features = [ "near-client/test_features", diff --git a/nearcore/src/lib.rs b/nearcore/src/lib.rs index 8368a1cbc48..3ce9d2f0da3 100644 --- a/nearcore/src/lib.rs +++ b/nearcore/src/lib.rs @@ -11,8 +11,6 @@ use near_network::time; use near_network::types::NetworkRecipient; use near_network::PeerManagerActor; use near_primitives::block::GenesisId; -#[cfg(feature = "performance_stats")] -use near_rust_allocator_proxy::reset_memory_usage_max; use near_store::{DBCol, Mode, NodeStorage, StoreOpenerError, Temperature}; use near_telemetry::TelemetryActor; use std::path::{Path, PathBuf}; @@ -246,10 +244,6 @@ pub fn start_with_config_and_synchronization( trace!(target: "diagnostic", key="log", "Starting NEAR node with diagnostic activated"); - // We probably reached peak memory once on this thread, we want to see when it happens again. - #[cfg(feature = "performance_stats")] - reset_memory_usage_max(); - Ok(NearNode { client: client_actor, view_client, diff --git a/neard/Cargo.toml b/neard/Cargo.toml index be84e3315a5..f29b9e7e41d 100644 --- a/neard/Cargo.toml +++ b/neard/Cargo.toml @@ -16,7 +16,6 @@ ansi_term.workspace = true anyhow.workspace = true clap.workspace = true futures.workspace = true -near-rust-allocator-proxy = { workspace = true, optional = true } once_cell.workspace = true openssl-probe.workspace = true opentelemetry.workspace = true @@ -54,7 +53,6 @@ rustc_version = "0.4" default = ["json_rpc", "rosetta_rpc"] performance_stats = ["nearcore/performance_stats"] -memory_stats = ["nearcore/memory_stats", "near-rust-allocator-proxy"] c_memory_stats = ["nearcore/c_memory_stats"] test_features = ["nearcore/test_features"] expensive_tests = ["nearcore/expensive_tests"] diff --git a/neard/src/main.rs b/neard/src/main.rs index 34578b01e3d..ab11ffac7c0 100644 --- a/neard/src/main.rs +++ b/neard/src/main.rs @@ -32,12 +32,6 @@ fn neard_version() -> Version { static DEFAULT_HOME: Lazy = Lazy::new(get_default_home); -#[cfg(feature = "memory_stats")] -#[global_allocator] -static ALLOC: near_rust_allocator_proxy::ProxyAllocator = - near_rust_allocator_proxy::ProxyAllocator::new(tikv_jemallocator::Jemalloc); - -#[cfg(not(feature = "memory_stats"))] #[global_allocator] static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; @@ -52,8 +46,6 @@ fn main() -> anyhow::Result<()> { .build_global() .context("failed to create the threadpool")?; - #[cfg(feature = "memory_stats")] - ALLOC.set_report_usage_interval(512 << 20).enable_stack_trace(true); // We use it to automatically search the for root certificates to perform HTTPS calls // (sending telemetry and downloading genesis) openssl_probe::init_ssl_cert_env_vars(); diff --git a/utils/near-performance-metrics/Cargo.toml b/utils/near-performance-metrics/Cargo.toml index ba888a1a901..e6e7f414a77 100644 --- a/utils/near-performance-metrics/Cargo.toml +++ b/utils/near-performance-metrics/Cargo.toml @@ -12,7 +12,6 @@ bytes.workspace = true bytesize = { workspace = true, optional = true } futures.workspace = true libc.workspace = true -near-rust-allocator-proxy = { workspace = true, optional = true } once_cell.workspace = true strum.workspace = true tokio.workspace = true @@ -21,5 +20,4 @@ tracing.workspace = true [features] c_memory_stats = [] -memory_stats = [] -performance_stats = ["bytesize", "near-rust-allocator-proxy"] +performance_stats = ["bytesize"] diff --git a/utils/near-performance-metrics/src/actix_enabled.rs b/utils/near-performance-metrics/src/actix_enabled.rs index 3185a0b1ecd..93fc0027f3d 100644 --- a/utils/near-performance-metrics/src/actix_enabled.rs +++ b/utils/near-performance-metrics/src/actix_enabled.rs @@ -1,5 +1,4 @@ use crate::stats_enabled::{get_thread_stats_logger, MyFuture, REF_COUNTER, SLOW_CALL_THRESHOLD}; -use near_rust_allocator_proxy::get_tid; use std::panic::Location; use std::time::{Duration, Instant}; use tracing::warn; @@ -36,9 +35,9 @@ where stat.lock().unwrap().log("run_later", loc.file(), loc.line(), took, ended, ""); if took > SLOW_CALL_THRESHOLD { warn!( - "Slow function call {}:{} {}:{} took: {}ms", + "Slow function call {}:{:?} {}:{} took: {}ms", "run_later", - get_tid(), + std::thread::current().id(), loc.file(), loc.line(), took.as_millis() diff --git a/utils/near-performance-metrics/src/stats_enabled.rs b/utils/near-performance-metrics/src/stats_enabled.rs index 85dc843a130..d696d41bffe 100644 --- a/utils/near-performance-metrics/src/stats_enabled.rs +++ b/utils/near-performance-metrics/src/stats_enabled.rs @@ -1,10 +1,6 @@ use bytesize::ByteSize; use futures; use futures::task::Context; -use near_rust_allocator_proxy::{ - current_thread_memory_usage, current_thread_peak_memory_usage, get_tid, reset_memory_usage_max, - thread_memory_count, thread_memory_usage, total_memory_usage, -}; use once_cell::sync::Lazy; use std::cmp::{max, min}; use std::collections::{HashMap, HashSet}; @@ -15,9 +11,6 @@ use std::task::Poll; use std::time::{Duration, Instant}; use tracing::{info, warn}; -static MEMORY_LIMIT: u64 = 512 * bytesize::MIB; -static MIN_MEM_USAGE_REPORT_SIZE: u64 = 100 * bytesize::MIB; - pub static NTHREADS: AtomicUsize = AtomicUsize::new(0); pub(crate) const SLOW_CALL_THRESHOLD: Duration = Duration::from_millis(500); const MIN_OCCUPANCY_RATIO_THRESHOLD: f64 = 0.02; @@ -126,10 +119,10 @@ impl ThreadStats { fn print_stats_and_clear( &mut self, - tid: usize, + tid: std::thread::ThreadId, sleep_time: Duration, now: Instant, - ) -> (f64, f64, ByteSize, usize) { + ) -> (f64, f64) { let mut ratio = self.time.as_nanos() as f64; if let Some(in_progress_since) = self.in_progress_since { let from = max(in_progress_since, self.last_check); @@ -137,20 +130,16 @@ impl ThreadStats { } ratio /= sleep_time.as_nanos() as f64; - let tmu = ByteSize::b(thread_memory_usage(tid) as u64); - let show_stats = ratio >= MIN_OCCUPANCY_RATIO_THRESHOLD - || tmu + self.c_mem >= ByteSize::b(MIN_MEM_USAGE_REPORT_SIZE); + let show_stats = ratio >= MIN_OCCUPANCY_RATIO_THRESHOLD; if show_stats { let class_name = format!("{:?}", self.classes); warn!( - " Thread:{} ratio: {:.3} {}:{} Rust mem: {}({}) C mem: {}", + " {:?}: ratio: {:.3} {}:{:?} C mem: {}", tid, ratio, class_name, - get_tid(), - tmu, - thread_memory_count(tid), + std::thread::current().id(), self.c_mem, ); if self.write_buf_added.as_u64() > 0 @@ -189,9 +178,9 @@ impl ThreadStats { self.clear(); if show_stats { - (ratio, 0.0, ByteSize::default(), 0) + (ratio, 0.0) } else { - (ratio, ratio, ByteSize::b(thread_memory_usage(tid) as u64), thread_memory_count(tid)) + (ratio, ratio) } } @@ -203,7 +192,7 @@ impl ThreadStats { } pub(crate) struct Stats { - stats: HashMap>>, + stats: HashMap>>, } #[cfg(all(target_os = "linux", feature = "c_memory_stats"))] @@ -240,7 +229,7 @@ impl Stats { } pub(crate) fn add_entry(&mut self, local_stats: &Arc>) { - let tid = get_tid(); + let tid = std::thread::current().id(); self.stats.entry(tid).or_insert_with(|| Arc::clone(local_stats)); } @@ -250,27 +239,18 @@ impl Stats { self.stats.len(), MIN_OCCUPANCY_RATIO_THRESHOLD ); - let mut s: Vec<_> = self.stats.iter().collect(); - s.sort_by_key(|f| f.0); + let s: Vec<_> = self.stats.iter().collect(); let mut ratio = 0.0; let mut other_ratio = 0.0; - let mut other_memory_size = ByteSize::default(); - let mut other_memory_count = 0; let now = Instant::now(); for entry in s { - let (tmp_ratio, tmp_other_ratio, tmp_other_memory_size, tmp_other_memory_count) = + let (tmp_ratio, tmp_other_ratio) = entry.1.lock().unwrap().print_stats_and_clear(*entry.0, sleep_time, now); ratio += tmp_ratio; other_ratio += tmp_other_ratio; - other_memory_size += tmp_other_memory_size; - other_memory_count += tmp_other_memory_count; } - info!( - " Other threads ratio {:.3} memory: {}({})", - other_ratio, other_memory_size, other_memory_count - ); - info!(" Rust alloc total memory usage: {}", ByteSize::b(total_memory_usage() as u64)); + info!(" Other threads ratio {:.3}", other_ratio); let c_memory_usage = get_c_memory_usage(); if c_memory_usage > ByteSize::default() { info!(" C alloc total memory usage: {}", c_memory_usage); @@ -314,8 +294,6 @@ where { let stat = get_thread_stats_logger(); - reset_memory_usage_max(); - let initial_memory_usage = current_thread_memory_usage(); let start = Instant::now(); stat.lock().unwrap().pre_log(start); let result = f(msg); @@ -323,43 +301,17 @@ where let ended = Instant::now(); let took = ended.saturating_duration_since(start); - let peak_memory = - ByteSize::b(current_thread_peak_memory_usage().saturating_sub(initial_memory_usage) as u64); - - if peak_memory >= ByteSize::b(MEMORY_LIMIT) { + if took >= SLOW_CALL_THRESHOLD { + let text_field = msg_text.map(|x| format!(" msg: {}", x)).unwrap_or(format!("")); warn!( - "Function exceeded memory limit {}:{} {:?} took: {}ms peak_memory: {}", + "Function exceeded time limit {}:{:?} {:?} took: {}ms {}", class_name, - get_tid(), + std::thread::current().id(), std::any::type_name::(), took.as_millis(), - peak_memory, + text_field, ); } - - if took >= SLOW_CALL_THRESHOLD { - let text_field = msg_text.map(|x| format!(" msg: {}", x)).unwrap_or(format!("")); - if peak_memory > ByteSize::default() { - warn!( - "Function exceeded time limit {}:{} {:?} took: {}ms {} peak_memory: {}", - class_name, - get_tid(), - std::any::type_name::(), - took.as_millis(), - text_field, - peak_memory, - ); - } else { - warn!( - "Function exceeded time limit {}:{} {:?} took: {}ms {}", - class_name, - get_tid(), - std::any::type_name::(), - took.as_millis(), - text_field, - ); - } - } stat.lock().unwrap().log( class_name, std::any::type_name::(), @@ -401,9 +353,9 @@ where if took > SLOW_CALL_THRESHOLD { warn!( - "Function exceeded time limit {}:{} {}:{} took: {}ms", + "Function exceeded time limit {}:{:?} {}:{} took: {}ms", this.class_name, - get_tid(), + std::thread::current().id(), this.file, this.line, took.as_millis() From 33f7c113c0a4cf0c9f5f9e2d29c93ba918ed6e04 Mon Sep 17 00:00:00 2001 From: Marcelo Diop-Gonzalez Date: Thu, 22 Dec 2022 18:48:52 -0500 Subject: [PATCH 129/188] logging: print out the part ords in chunk request/response debug logs (#8267) when trying to debug why an archival node was being slow to apply blocks/chunks, even though it was receiving lots of chunk part responses for a long time, it was kind of difficult to tell which of the logs for chunk parts were relevant, because some of them might just be duplicates that we've already seen. So printing this info would probably help a bit here. --- chain/chunks/src/lib.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/chain/chunks/src/lib.rs b/chain/chunks/src/lib.rs index dc4a516a422..7cafcdd812c 100644 --- a/chain/chunks/src/lib.rs +++ b/chain/chunks/src/lib.rs @@ -648,7 +648,16 @@ impl ShardsManager { for (target_account, part_ords) in bp_to_parts { // extra check that we are not sending request to ourselves. if no_account_id || me != target_account.as_ref() { - let parts_count = part_ords.len(); + let prefer_peer = request_from_archival || rand::thread_rng().gen::(); + debug!( + target: "chunks", + ?part_ords, + shard_id, + ?target_account, + prefer_peer, + "Requesting parts", + ); + let request = PartialEncodedChunkRequestMsg { chunk_hash: chunk_hash.clone(), part_ords, @@ -660,12 +669,11 @@ impl ShardsManager { }; let target = AccountIdOrPeerTrackingShard { account_id: target_account, - prefer_peer: request_from_archival || rand::thread_rng().gen::(), + prefer_peer, shard_id, only_archival: request_from_archival, min_height: height.saturating_sub(CHUNK_REQUEST_PEER_HORIZON), }; - debug!(target: "chunks", "Requesting {} parts for shard {} from {:?} prefer {}", parts_count, shard_id, target.account_id, target.prefer_peer); self.peer_manager_adapter.do_send( PeerManagerMessageRequest::NetworkRequests( @@ -1588,8 +1596,14 @@ impl ShardsManager { partial_encoded_chunk.map(|chunk| PartialEncodedChunkV2::from(chunk)); let header = &partial_encoded_chunk.header; let chunk_hash = header.chunk_hash(); - debug!(target: "chunks", ?chunk_hash, height=header.height_created(), shard_id=header.shard_id(), "Process partial encoded chunk: parts {}", - partial_encoded_chunk.get_inner().parts.len()); + debug!( + target: "chunks", + ?chunk_hash, + height = header.height_created(), + shard_id = header.shard_id(), + parts = ?partial_encoded_chunk.get_inner().parts.iter().map(|p| p.part_ord).collect::>(), + "Process partial encoded chunk", + ); // Verify the partial encoded chunk is valid and worth processing // 1.a Leave if we received known chunk if let Some(entry) = self.encoded_chunks.get(&chunk_hash) { From 0b8543db1bfbea7d305bf3356b858cd27f12a908 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Sat, 24 Dec 2022 13:14:49 +0100 Subject: [PATCH 130/188] test-utils: remove kind of pointless encode_int function (#8274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The function is used in only one place and really it’s more readable to just use i32::to_le_bytes. --- Cargo.lock | 1 - integration-tests/src/tests/runtime/state_viewer.rs | 4 ++-- test-utils/testlib/Cargo.toml | 1 - test-utils/testlib/src/runtime_utils.rs | 7 ------- 4 files changed, 2 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a4e3f45187..18fdb868f83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5628,7 +5628,6 @@ dependencies = [ name = "testlib" version = "0.0.0" dependencies = [ - "byteorder", "near-chain", "near-chain-configs", "near-crypto", diff --git a/integration-tests/src/tests/runtime/state_viewer.rs b/integration-tests/src/tests/runtime/state_viewer.rs index 5ff1e5ce62b..974053a08cc 100644 --- a/integration-tests/src/tests/runtime/state_viewer.rs +++ b/integration-tests/src/tests/runtime/state_viewer.rs @@ -19,7 +19,7 @@ use near_primitives::{ use near_store::{set_account, NibbleSlice, RawTrieNode, RawTrieNodeWithSize}; use node_runtime::state_viewer::errors; use node_runtime::state_viewer::*; -use testlib::runtime_utils::{alice_account, encode_int}; +use testlib::runtime_utils::alice_account; struct ProofVerifier { nodes: HashMap, @@ -122,7 +122,7 @@ fn test_view_call() { &MockEpochInfoProvider::default(), ); - assert_eq!(result.unwrap(), encode_int(10)); + assert_eq!(result.unwrap(), (10i32).to_le_bytes()); } #[test] diff --git a/test-utils/testlib/Cargo.toml b/test-utils/testlib/Cargo.toml index 1ba92263fae..93e414b5452 100644 --- a/test-utils/testlib/Cargo.toml +++ b/test-utils/testlib/Cargo.toml @@ -6,7 +6,6 @@ publish = false edition.workspace = true [dependencies] -byteorder.workspace = true once_cell.workspace = true near-chain-configs = { path = "../../core/chain-configs" } diff --git a/test-utils/testlib/src/runtime_utils.rs b/test-utils/testlib/src/runtime_utils.rs index 3e0cc23fcb1..f9b43634a70 100644 --- a/test-utils/testlib/src/runtime_utils.rs +++ b/test-utils/testlib/src/runtime_utils.rs @@ -1,4 +1,3 @@ -use byteorder::{ByteOrder, LittleEndian}; use once_cell::sync::Lazy; use near_chain_configs::Genesis; @@ -46,9 +45,3 @@ pub fn add_test_contract(genesis: &mut Genesis, account_id: &AccountId) { code: near_test_contracts::rs_contract().to_vec(), }); } - -pub fn encode_int(val: i32) -> [u8; 4] { - let mut tmp = [0u8; 4]; - LittleEndian::write_i32(&mut tmp, val); - tmp -} From d17fae90fc8093ae74ad2c2b2205eba843c35e81 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Mon, 2 Jan 2023 12:38:07 +0100 Subject: [PATCH 131/188] runtime: introduce PublicKeyBuffer to avoid memory allocation (#8277) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently when public key is read it’s unnecessarily copied into a temporary vector. The copy is necessary because otherwise public key data would hold a shared reference on self.registers which prevents any methods which take exclusive reference to self to be called. However, if we decode the public key immediately after reading it, we can deal with sized objects which can be stored on stack. This is perfect except because we cannot change when errors are reported we have to postpone returning of the deserialisation errors. Introduce PublicKeyBuffer which facilitates that. It hides from the users the fact that deserialisation happens immediately as soon as the data is read providing an interface which looks as if deserialisation was happening separately from reading. An alternative would be to use a fixed-size 65-byte array and separate reading the data from deserialisation just like it’s happening now. A minor downside of that is hard-coding the 65-byte size. We’d probably want PublicKey::MAX_LEN but even then having such a constant doesn’t seem too clean of a solution to me. --- runtime/near-vm-logic/src/logic.rs | 57 ++++++++++++++------ runtime/near-vm-logic/src/receipt_manager.rs | 41 ++++---------- 2 files changed, 51 insertions(+), 47 deletions(-) diff --git a/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs index 1d692fad580..86121c928ff 100644 --- a/runtime/near-vm-logic/src/logic.rs +++ b/runtime/near-vm-logic/src/logic.rs @@ -23,7 +23,7 @@ use near_vm_errors::{FunctionCallError, InconsistentStateError}; use near_vm_errors::{HostError, VMLogicError}; use std::mem::size_of; -pub type Result = ::std::result::Result; +pub type Result = ::std::result::Result; pub struct VMLogic<'a> { /// Provides access to the components outside the Wasm runtime for operations on the trie and @@ -99,6 +99,32 @@ macro_rules! get_memory_or_register { }; } +/// A wrapper for reading public key. +/// +/// This exists for historical reasons because we must maintain when errors are +/// returned. In the old days, between reading the public key and decoding it +/// we could return unrelated error. Because of that we cannot change the code +/// to return deserialisation errors immediately after reading the public key. +/// +/// This struct abstracts away the fact that we’re deserialising the key +/// immediately. Decoding errors are detected as soon as this object is created +/// but they are communicated to the user only once they call [`Self::decode`]. +/// +/// Why not just keep the old ways without this noise? By doing deserialisation +/// immediately we’re copying the data onto the stack without having to allocate +/// a temporary vector. +struct PublicKeyBuffer(Result); + +impl PublicKeyBuffer { + fn new(data: &[u8]) -> Self { + Self(borsh::BorshDeserialize::try_from_slice(data).map_err(|_| ())) + } + + fn decode(self) -> Result { + self.0.map_err(|_| HostError::InvalidPublicKey.into()) + } +} + impl<'a> VMLogic<'a> { pub fn new_with_protocol_version( ext: &'a mut dyn External, @@ -418,6 +444,10 @@ impl<'a> VMLogic<'a> { Ok(()) } + fn get_public_key(&mut self, ptr: u64, len: u64) -> Result { + Ok(PublicKeyBuffer::new(&get_memory_or_register!(self, ptr, len)?)) + } + // ############### // # Context API # // ############### @@ -1776,12 +1806,10 @@ impl<'a> VMLogic<'a> { .into()); } let amount = self.memory.get_u128(&mut self.gas_counter, amount_ptr)?; - let public_key = - get_memory_or_register!(self, public_key_ptr, public_key_len)?.into_owned(); - + 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)?; + self.receipt_manager.append_action_stake(receipt_idx, amount, public_key.decode()?); Ok(()) } @@ -1816,16 +1844,14 @@ impl<'a> VMLogic<'a> { } .into()); } - let public_key = - get_memory_or_register!(self, public_key_ptr, public_key_len)?.into_owned(); - + 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, + public_key.decode()?, nonce, - )?; + ); Ok(()) } @@ -1867,8 +1893,7 @@ impl<'a> VMLogic<'a> { } .into()); } - let public_key = - get_memory_or_register!(self, public_key_ptr, public_key_len)?.into_owned(); + let public_key = self.get_public_key(public_key_ptr, public_key_len)?; let allowance = self.memory.get_u128(&mut self.gas_counter, allowance_ptr)?; let allowance = if allowance > 0 { Some(allowance) } else { None }; let receiver_id = self.read_and_parse_account_id(receiver_id_ptr, receiver_id_len)?; @@ -1884,7 +1909,7 @@ impl<'a> VMLogic<'a> { self.receipt_manager.append_action_add_key_with_function_call( receipt_idx, - public_key, + public_key.decode()?, nonce, allowance, receiver_id, @@ -1923,12 +1948,10 @@ impl<'a> VMLogic<'a> { } .into()); } - let public_key = - get_memory_or_register!(self, public_key_ptr, public_key_len)?.into_owned(); - + 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)?; + self.receipt_manager.append_action_delete_key(receipt_idx, public_key.decode()?); Ok(()) } diff --git a/runtime/near-vm-logic/src/receipt_manager.rs b/runtime/near-vm-logic/src/receipt_manager.rs index ad030d6a222..6dc1723139a 100644 --- a/runtime/near-vm-logic/src/receipt_manager.rs +++ b/runtime/near-vm-logic/src/receipt_manager.rs @@ -1,7 +1,6 @@ use crate::logic; use crate::types::ReceiptIndex; use crate::External; -use borsh::BorshDeserialize; use near_crypto::PublicKey; use near_primitives::receipt::DataReceiver; use near_primitives::transaction::{ @@ -243,17 +242,9 @@ impl ReceiptManager { &mut self, receipt_index: ReceiptIndex, stake: Balance, - public_key: Vec, - ) -> logic::Result<()> { - self.append_action( - receipt_index, - Action::Stake(StakeAction { - stake, - public_key: PublicKey::try_from_slice(&public_key) - .map_err(|_| HostError::InvalidPublicKey)?, - }), - ); - Ok(()) + public_key: PublicKey, + ) { + self.append_action(receipt_index, Action::Stake(StakeAction { stake, public_key })); } /// Attach the [`AddKeyAction`] action to an existing receipt. @@ -270,18 +261,16 @@ impl ReceiptManager { pub(crate) fn append_action_add_key_with_full_access( &mut self, receipt_index: ReceiptIndex, - public_key: Vec, + public_key: PublicKey, nonce: Nonce, - ) -> logic::Result<()> { + ) { self.append_action( receipt_index, Action::AddKey(AddKeyAction { - public_key: PublicKey::try_from_slice(&public_key) - .map_err(|_| HostError::InvalidPublicKey)?, + public_key, access_key: AccessKey { nonce, permission: AccessKeyPermission::FullAccess }, }), ); - Ok(()) } /// Attach the [`AddKeyAction`] action an existing receipt. @@ -304,7 +293,7 @@ impl ReceiptManager { pub(crate) fn append_action_add_key_with_function_call( &mut self, receipt_index: ReceiptIndex, - public_key: Vec, + public_key: PublicKey, nonce: Nonce, allowance: Option, receiver_id: AccountId, @@ -313,8 +302,7 @@ impl ReceiptManager { self.append_action( receipt_index, Action::AddKey(AddKeyAction { - public_key: PublicKey::try_from_slice(&public_key) - .map_err(|_| HostError::InvalidPublicKey)?, + public_key, access_key: AccessKey { nonce, permission: AccessKeyPermission::FunctionCall(FunctionCallPermission { @@ -347,16 +335,9 @@ impl ReceiptManager { pub(crate) fn append_action_delete_key( &mut self, receipt_index: ReceiptIndex, - public_key: Vec, - ) -> logic::Result<()> { - self.append_action( - receipt_index, - Action::DeleteKey(DeleteKeyAction { - public_key: PublicKey::try_from_slice(&public_key) - .map_err(|_| HostError::InvalidPublicKey)?, - }), - ); - Ok(()) + public_key: PublicKey, + ) { + self.append_action(receipt_index, Action::DeleteKey(DeleteKeyAction { public_key })); } /// Attach the [`DeleteAccountAction`] action to an existing receipt From fd5f2ebfca1e3ef89561799565539b7707d9a5d9 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Mon, 2 Jan 2023 16:34:34 +0100 Subject: [PATCH 132/188] runtime: introduce MemoryLike::view_memory to avoid memcpy (#8276) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a view_memory method to MemoryLike trait which returns a view of the guest memory. This allows caller to read data directly from guest memory instead of using read_memory to first copy it into a vector. To accommodate implementations which cannot support borrowing the memory, the method returns a Cow which allows such implementations to return a copy of the memory. WasmerMemory doesn’t support it because its memory is made of `Cell`s and transmuting `&[Cell]` to `&[u8]` is maybe not sound. Importantly though, Wasmer2Memory supports returning the view. Also introduce a simple MemSlice structure which combines guest memory pointer and slice length in a single object. This avoids confusing when function accepts two u64 arguments and caller just needs to know which is the pointer and which is the length. --- runtime/near-vm-logic/src/dependencies.rs | 65 +++++++-- runtime/near-vm-logic/src/lib.rs | 2 +- runtime/near-vm-logic/src/logic.rs | 16 ++- .../near-vm-logic/src/mocks/mock_memory.rs | 11 +- runtime/near-vm-logic/src/vmstate.rs | 55 ++++---- runtime/near-vm-runner/src/memory.rs | 19 ++- runtime/near-vm-runner/src/tests.rs | 26 ++-- runtime/near-vm-runner/src/wasmer2_runner.rs | 129 +++++++++--------- runtime/near-vm-runner/src/wasmtime_runner.rs | 16 ++- 9 files changed, 195 insertions(+), 144 deletions(-) diff --git a/runtime/near-vm-logic/src/dependencies.rs b/runtime/near-vm-logic/src/dependencies.rs index f0dfd04d35a..f8236745ce4 100644 --- a/runtime/near-vm-logic/src/dependencies.rs +++ b/runtime/near-vm-logic/src/dependencies.rs @@ -5,16 +5,51 @@ use near_primitives::types::TrieNodesCount; use near_primitives_core::types::{AccountId, Balance}; use near_vm_errors::VMLogicError; +use std::borrow::Cow; + +/// Representation of the address slice of guest memory. +#[derive(Clone, Copy)] +pub struct MemSlice { + /// Offset within guest memory at which the slice starts. + pub ptr: u64, + /// Length of the slice. + pub len: u64, +} + +impl MemSlice { + #[inline] + pub fn len>(&self) -> Result { + T::try_from(self.len).map_err(|_| ()) + } + + #[inline] + pub fn end>(&self) -> Result { + T::try_from(self.ptr.checked_add(self.len).ok_or(())?).map_err(|_| ()) + } + + #[inline] + pub fn range>(&self) -> Result, ()> { + let end = self.end()?; + let start = T::try_from(self.ptr).map_err(|_| ())?; + Ok(start..end) + } +} + /// An abstraction over the memory of the smart contract. pub trait MemoryLike { /// Returns success if the memory interval is completely inside smart /// contract’s memory. /// - /// You often don’t need to use this method since [`Self::read_memory`] and - /// [`Self::write_memory`] will perform the check, however it may be - /// necessary to prevent potential denial of service attacks. See - /// [`Self::read_memory`] for description. - fn fits_memory(&self, offset: u64, len: u64) -> Result<(), ()>; + /// You often don’t need to use this method since other methods will perform + /// the check, however it may be necessary to prevent potential denial of + /// service attacks. See [`Self::read_memory`] for description. + fn fits_memory(&self, slice: MemSlice) -> Result<(), ()>; + + /// Returns view of the content of the given memory interval. + /// + /// Not all implementations support borrowing the memory directly. In those + /// cases, the data is copied into a vector. + fn view_memory(&self, slice: MemSlice) -> Result, ()>; /// Reads the content of the given memory interval. /// @@ -27,27 +62,27 @@ pub trait MemoryLike { /// attacks. For example, consider the following function: /// /// ``` - /// # use near_vm_logic::MemoryLike; + /// # use near_vm_logic::{MemoryLike, MemSlice}; /// - /// fn read_vec(mem: &dyn MemoryLike, ptr: u64, len: u64) -> Result, ()> { - /// let mut vec = vec![0; usize::try_from(len).map_err(|_| ())?]; - /// mem.read_memory(ptr, &mut vec[..])?; + /// fn read_vec(mem: &dyn MemoryLike, slice: MemSlice) -> Result, ()> { + /// let mut vec = vec![0; slice.len()?]; + /// mem.read_memory(slice.ptr, &mut vec[..])?; /// Ok(vec) /// } /// ``` /// - /// If attacker controls `len` argument, it may cause attempt at allocation + /// If attacker controls length argument, it may cause attempt at allocation /// of arbitrarily-large buffer and crash the program. In situations like /// this, it’s necessary to use [`Self::fits_memory`] method to verify that /// the length is valid. For example: /// /// ``` - /// # use near_vm_logic::MemoryLike; + /// # use near_vm_logic::{MemoryLike, MemSlice}; /// - /// fn read_vec(mem: &dyn MemoryLike, ptr: u64, len: u64) -> Result, ()> { - /// mem.fits_memory(ptr, len)?; - /// let mut vec = vec![0; len as usize]; - /// mem.read_memory(ptr, &mut vec[..])?; + /// fn read_vec(mem: &dyn MemoryLike, slice: MemSlice) -> Result, ()> { + /// mem.fits_memory(slice)?; + /// let mut vec = vec![0; slice.len()?]; + /// mem.read_memory(slice.ptr, &mut vec[..])?; /// Ok(vec) /// } /// ``` diff --git a/runtime/near-vm-logic/src/lib.rs b/runtime/near-vm-logic/src/lib.rs index fe7a8166a5f..44927ad7244 100644 --- a/runtime/near-vm-logic/src/lib.rs +++ b/runtime/near-vm-logic/src/lib.rs @@ -14,7 +14,7 @@ mod utils; mod vmstate; pub use context::VMContext; -pub use dependencies::{External, MemoryLike, StorageGetMode, ValuePtr}; +pub use dependencies::{External, MemSlice, MemoryLike, StorageGetMode, ValuePtr}; pub use logic::{VMLogic, VMOutcome}; pub use near_primitives_core::config::*; pub use near_primitives_core::profile; diff --git a/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs index 86121c928ff..c9bbb587e7a 100644 --- a/runtime/near-vm-logic/src/logic.rs +++ b/runtime/near-vm-logic/src/logic.rs @@ -1,5 +1,5 @@ use crate::context::VMContext; -use crate::dependencies::{External, MemoryLike}; +use crate::dependencies::{External, MemSlice, MemoryLike}; use crate::gas_counter::{FastGasCounter, GasCounter}; use crate::receipt_manager::ReceiptManager; use crate::types::{PromiseIndex, PromiseResult, ReceiptIndex, ReturnData}; @@ -264,7 +264,8 @@ impl<'a> VMLogic<'a> { /// `base + read_memory_base + read_memory_bytes * num_bytes + write_register_base + write_register_bytes * num_bytes` pub fn write_register(&mut self, register_id: u64, data_len: u64, data_ptr: u64) -> Result<()> { self.gas_counter.pay_base(base)?; - let data = self.memory.get_vec(&mut self.gas_counter, data_ptr, data_len)?; + let data = + self.memory.view(&mut self.gas_counter, MemSlice { ptr: data_ptr, len: data_len })?; self.registers.set(&mut self.gas_counter, &self.config.limit_config, register_id, data) } @@ -302,7 +303,7 @@ impl<'a> VMLogic<'a> { } .into()); } - buf = self.memory.get_vec(&mut self.gas_counter, ptr, len)?; + buf = self.memory.view(&mut self.gas_counter, MemSlice { ptr, len })?.into_owned(); } else { buf = vec![]; for i in 0..=max_len { @@ -331,7 +332,7 @@ impl<'a> VMLogic<'a> { /// * It's up to the caller to set correct len #[cfg(feature = "sandbox")] fn sandbox_get_utf8_string(&mut self, len: u64, ptr: u64) -> Result { - let buf = self.memory.get_vec_for_free(ptr, len)?; + let buf = self.memory.view_for_free(MemSlice { ptr, len })?.into_owned(); String::from_utf8(buf).map_err(|_| HostError::BadUTF8.into()) } @@ -356,7 +357,7 @@ impl<'a> VMLogic<'a> { let max_len = self.config.limit_config.max_total_log_length.saturating_sub(self.total_log_length); if len != u64::MAX { - let input = self.memory.get_vec(&mut self.gas_counter, ptr, len)?; + let input = self.memory.view(&mut self.gas_counter, MemSlice { ptr, len })?; if len % 2 != 0 { return Err(HostError::BadUTF16.into()); } @@ -1367,8 +1368,9 @@ impl<'a> VMLogic<'a> { self.gas_counter.pay_per(promise_and_per_promise, memory_len)?; // Read indices as little endian u64. - let promise_indices = - self.memory.get_vec(&mut self.gas_counter, promise_idx_ptr, memory_len)?; + let promise_indices = self + .memory + .view(&mut self.gas_counter, MemSlice { ptr: promise_idx_ptr, len: memory_len })?; let promise_indices = stdx::as_chunks_exact::<{ size_of::() }, u8>(&promise_indices) .unwrap() .into_iter() diff --git a/runtime/near-vm-logic/src/mocks/mock_memory.rs b/runtime/near-vm-logic/src/mocks/mock_memory.rs index f75be26c372..f00a13736da 100644 --- a/runtime/near-vm-logic/src/mocks/mock_memory.rs +++ b/runtime/near-vm-logic/src/mocks/mock_memory.rs @@ -1,13 +1,20 @@ -use crate::MemoryLike; +use crate::{MemSlice, MemoryLike}; + +use std::borrow::Cow; #[derive(Default)] pub struct MockedMemory {} impl MemoryLike for MockedMemory { - fn fits_memory(&self, _offset: u64, _len: u64) -> Result<(), ()> { + fn fits_memory(&self, _slice: MemSlice) -> Result<(), ()> { Ok(()) } + fn view_memory(&self, slice: MemSlice) -> Result, ()> { + let view = unsafe { std::slice::from_raw_parts(slice.ptr as *const u8, slice.len()?) }; + Ok(Cow::Borrowed(view)) + } + fn read_memory(&self, offset: u64, buffer: &mut [u8]) -> Result<(), ()> { let src = unsafe { std::slice::from_raw_parts(offset as *const u8, buffer.len()) }; buffer.copy_from_slice(src); diff --git a/runtime/near-vm-logic/src/vmstate.rs b/runtime/near-vm-logic/src/vmstate.rs index e566195589e..91552c90e28 100644 --- a/runtime/near-vm-logic/src/vmstate.rs +++ b/runtime/near-vm-logic/src/vmstate.rs @@ -1,4 +1,4 @@ -use crate::dependencies::MemoryLike; +use crate::dependencies::{MemSlice, MemoryLike}; use crate::gas_counter::GasCounter; use near_primitives_core::config::ExtCosts::*; @@ -55,6 +55,26 @@ impl<'a> Memory<'a> { Self(mem) } + /// Returns view of the guest memory. + /// + /// Not all runtimes support returning a view to the guest memory so this + /// may return an owned vector. + pub(super) fn view<'s>( + &'s self, + gas_counter: &mut GasCounter, + slice: MemSlice, + ) -> Result> { + gas_counter.pay_base(read_memory_base)?; + gas_counter.pay_per(read_memory_byte, slice.len)?; + self.0.view_memory(slice).map_err(|_| HostError::MemoryAccessViolation.into()) + } + + #[cfg(feature = "sandbox")] + /// Like [`Self::view`] but does not pay gas fees. + pub(super) fn view_for_free(&self, slice: MemSlice) -> Result> { + self.0.view_memory(slice).map_err(|_| HostError::MemoryAccessViolation.into()) + } + /// Copies data from guest memory into provided buffer accounting for gas. fn get_into(&self, gas_counter: &mut GasCounter, offset: u64, buf: &mut [u8]) -> Result<()> { gas_counter.pay_base(read_memory_base)?; @@ -63,29 +83,6 @@ impl<'a> Memory<'a> { self.0.read_memory(offset, buf).map_err(|_| HostError::MemoryAccessViolation.into()) } - /// Copies data from guest memory into a newly allocated vector accounting for gas. - pub(super) fn get_vec( - &self, - gas_counter: &mut GasCounter, - offset: u64, - len: u64, - ) -> Result> { - gas_counter.pay_base(read_memory_base)?; - gas_counter.pay_per(read_memory_byte, len)?; - self.get_vec_for_free(offset, len) - } - - /// Like [`Self::get_vec`] but does not pay gas fees. - pub(super) fn get_vec_for_free(&self, offset: u64, len: u64) -> Result> { - // This check is redundant in the sense that read_memory will perform it - // as well however it’s here to validate that `len` is a valid value. - // See documentation of MemoryLike::read_memory for more information. - self.0.fits_memory(offset, len).map_err(|_| HostError::MemoryAccessViolation)?; - let mut buf = vec![0; len as usize]; - self.0.read_memory(offset, &mut buf).map_err(|_| HostError::MemoryAccessViolation)?; - Ok(buf) - } - /// Copies data from provided buffer into guest memory accounting for gas. pub(super) fn set( &mut self, @@ -224,7 +221,7 @@ impl Registers { /// Reads data from guest memory or register. /// -/// If `len` is `u64::MAX` read register with index `offset`. Otherwise, reads +/// If `len` is `u64::MAX` read register with index `ptr`. Otherwise, reads /// `len` bytes of guest memory starting at given offset. Returns error if /// there’s insufficient gas, memory interval is out of bounds or given register /// isn’t set. @@ -235,15 +232,15 @@ impl Registers { /// references to other fields in the structure.. pub(super) fn get_memory_or_register<'a, 'b>( gas_counter: &mut GasCounter, - memory: &Memory<'a>, + memory: &'b Memory<'a>, registers: &'b Registers, - offset: u64, + ptr: u64, len: u64, ) -> Result> { if len == u64::MAX { - registers.get(gas_counter, offset).map(Cow::Borrowed) + registers.get(gas_counter, ptr).map(Cow::Borrowed) } else { - memory.get_vec(gas_counter, offset, len).map(Cow::Owned) + memory.view(gas_counter, MemSlice { ptr, len }) } } diff --git a/runtime/near-vm-runner/src/memory.rs b/runtime/near-vm-runner/src/memory.rs index 3949ae75567..7c62e7dbf74 100644 --- a/runtime/near-vm-runner/src/memory.rs +++ b/runtime/near-vm-runner/src/memory.rs @@ -1,4 +1,7 @@ -use near_vm_logic::MemoryLike; +use near_vm_logic::{MemSlice, MemoryLike}; + +use std::borrow::Cow; + use wasmer_runtime::units::Pages; use wasmer_runtime::wasm::MemoryDescriptor; use wasmer_runtime::Memory; @@ -26,9 +29,9 @@ impl WasmerMemory { } impl WasmerMemory { - fn with_memory(&self, offset: u64, len: usize, func: F) -> Result<(), ()> + fn with_memory(&self, offset: u64, len: usize, func: F) -> Result where - F: FnOnce(core::slice::Iter<'_, std::cell::Cell>), + F: FnOnce(core::slice::Iter<'_, std::cell::Cell>) -> T, { let start = usize::try_from(offset).map_err(|_| ())?; let end = start.checked_add(len).ok_or(())?; @@ -37,8 +40,14 @@ impl WasmerMemory { } impl MemoryLike for WasmerMemory { - fn fits_memory(&self, offset: u64, len: u64) -> Result<(), ()> { - self.with_memory(offset, usize::try_from(len).map_err(|_| ())?, |_| ()) + fn fits_memory(&self, slice: MemSlice) -> Result<(), ()> { + self.with_memory(slice.ptr, slice.len()?, |_| ()) + } + + fn view_memory(&self, slice: MemSlice) -> Result, ()> { + self.with_memory(slice.ptr, slice.len()?, |mem| { + Cow::Owned(mem.map(core::cell::Cell::get).collect()) + }) } fn read_memory(&self, offset: u64, buffer: &mut [u8]) -> Result<(), ()> { diff --git a/runtime/near-vm-runner/src/tests.rs b/runtime/near-vm-runner/src/tests.rs index 599345a34e4..dad2ada3f31 100644 --- a/runtime/near-vm-runner/src/tests.rs +++ b/runtime/near-vm-runner/src/tests.rs @@ -9,7 +9,7 @@ mod wasm_validation; use crate::vm_kind::VMKind; use near_primitives::version::ProtocolVersion; -use near_vm_logic::VMContext; +use near_vm_logic::{MemSlice, MemoryLike, VMContext}; const CURRENT_ACCOUNT_ID: &str = "alice"; const SIGNER_ACCOUNT_ID: &str = "bob"; @@ -70,34 +70,34 @@ fn prepaid_loading_gas(bytes: usize) -> u64 { /// memory must be configured for indices `0..WASM_PAGE_SIZE` to be valid. /// /// Panics if any of the tests fails. -pub(crate) fn test_memory_like(factory: impl FnOnce() -> Box) { +pub(crate) fn test_memory_like(factory: impl FnOnce() -> Box) { const PAGE: u64 = wasmer_types::WASM_PAGE_SIZE as u64; struct TestContext { - mem: Box, + mem: Box, buf: [u8; PAGE as usize + 1], } impl TestContext { - fn test_read(&mut self, offset: u64, len: u64, value: u8) { + fn test_read(&mut self, ptr: u64, len: u64, value: u8) { self.buf.fill(!value); - self.mem.fits_memory(offset, len).unwrap(); - self.mem.read_memory(offset, &mut self.buf[..(len as usize)]).unwrap(); + self.mem.fits_memory(MemSlice { ptr, len }).unwrap(); + self.mem.read_memory(ptr, &mut self.buf[..(len as usize)]).unwrap(); assert!(self.buf[..(len as usize)].iter().all(|&v| v == value)); } - fn test_write(&mut self, offset: u64, len: u64, value: u8) { + fn test_write(&mut self, ptr: u64, len: u64, value: u8) { self.buf.fill(value); - self.mem.fits_memory(offset, len).unwrap(); - self.mem.write_memory(offset, &self.buf[..(len as usize)]).unwrap(); + self.mem.fits_memory(MemSlice { ptr, len }).unwrap(); + self.mem.write_memory(ptr, &self.buf[..(len as usize)]).unwrap(); } - fn test_oob(&mut self, offset: u64, len: u64) { + fn test_oob(&mut self, ptr: u64, len: u64) { self.buf.fill(42); - self.mem.fits_memory(offset, len).unwrap_err(); - self.mem.read_memory(offset, &mut self.buf[..(len as usize)]).unwrap_err(); + self.mem.fits_memory(MemSlice { ptr, len }).unwrap_err(); + self.mem.read_memory(ptr, &mut self.buf[..(len as usize)]).unwrap_err(); assert!(self.buf[..(len as usize)].iter().all(|&v| v == 42)); - self.mem.write_memory(offset, &self.buf[..(len as usize)]).unwrap_err(); + self.mem.write_memory(ptr, &self.buf[..(len as usize)]).unwrap_err(); } } diff --git a/runtime/near-vm-runner/src/wasmer2_runner.rs b/runtime/near-vm-runner/src/wasmer2_runner.rs index 4229ac93a8f..4ea35b630c1 100644 --- a/runtime/near-vm-runner/src/wasmer2_runner.rs +++ b/runtime/near-vm-runner/src/wasmer2_runner.rs @@ -14,7 +14,8 @@ use near_vm_errors::{ }; use near_vm_logic::gas_counter::FastGasCounter; use near_vm_logic::types::{PromiseResult, ProtocolVersion}; -use near_vm_logic::{External, MemoryLike, VMConfig, VMContext, VMLogic, VMOutcome}; +use near_vm_logic::{External, MemSlice, MemoryLike, VMConfig, VMContext, VMLogic, VMOutcome}; +use std::borrow::Cow; use std::hash::{Hash, Hasher}; use std::mem::size_of; use std::sync::Arc; @@ -59,26 +60,47 @@ impl Wasmer2Memory { )?))) } - // Returns the pointer to memory at the specified offset and the size of the buffer starting at - // the returned pointer. - fn data_offset(&self, offset: u64) -> Option<(*mut u8, usize)> { - let size = self.0.size().bytes().0; - let offset = usize::try_from(offset).ok()?; - // `checked_sub` here verifies that offsetting the buffer by offset still lands us - // in-bounds of the allocated object. - let remaining = size.checked_sub(offset)?; - Some(unsafe { - // SAFETY: we verified that offsetting the base pointer by `offset` still lands us - // in-bounds of the original object. - (self.0.vmmemory().as_ref().base.add(offset), remaining) - }) + /// Returns pointer to memory at the specified offset provided that there’s + /// enough space in the buffer starting at the returned pointer. + /// + /// Safety: Caller must guarantee that the returned pointer is not used + /// after guest memory mapping is changed (e.g. grown). + unsafe fn get_ptr(&self, offset: u64, len: usize) -> Result<*mut u8, ()> { + let offset = usize::try_from(offset).map_err(|_| ())?; + // SAFETY: Caller promisses memory mapping won’t change. + let vmmem = unsafe { self.0.vmmemory().as_ref() }; + // `checked_sub` here verifies that offsetting the buffer by offset + // still lands us in-bounds of the allocated object. + let remaining = vmmem.current_length.checked_sub(offset).ok_or(())?; + if len <= remaining { + Ok(vmmem.base.add(offset)) + } else { + Err(()) + } } - fn get_memory_buffer(&self, offset: u64, len: usize) -> Result<*mut u8, ()> { - match self.data_offset(offset) { - Some((ptr, remaining)) if len <= remaining => Ok(ptr), - _ => Err(()), - } + /// Returns shared reference to slice in guest memory at given offset. + /// + /// Safety: Caller must guarantee that guest memory mapping is not changed + /// (e.g. grown) while the slice is held. + unsafe fn get(&self, offset: u64, len: usize) -> Result<&[u8], ()> { + // SAFETY: Caller promisses memory mapping won’t change. + let ptr = unsafe { self.get_ptr(offset, len)? }; + // SAFETY: get_ptr verifies that [ptr, ptr+len) is valid slice. + Ok(unsafe { core::slice::from_raw_parts(ptr, len) }) + } + + /// Returns shared reference to slice in guest memory at given offset. + /// + /// Safety: Caller must guarantee that guest memory mapping is not changed + /// (e.g. grown) while the slice is held. + unsafe fn get_mut(&mut self, offset: u64, len: usize) -> Result<&mut [u8], ()> { + // SAFETY: Caller promisses memory mapping won’t change. + let ptr = unsafe { self.get_ptr(offset, len)? }; + // SAFETY: get_ptr verifies that [ptr, ptr+len) is valid slice and since + // we’re holding exclusive self reference another mut reference won’t be + // created + Ok(unsafe { core::slice::from_raw_parts_mut(ptr, len) }) } pub(crate) fn vm(&self) -> VMMemory { @@ -87,31 +109,30 @@ impl Wasmer2Memory { } impl MemoryLike for Wasmer2Memory { - fn fits_memory(&self, offset: u64, len: u64) -> Result<(), ()> { - let len = usize::try_from(len).map_err(|_| ())?; - self.get_memory_buffer(offset, len).map(|_| ()) + fn fits_memory(&self, slice: MemSlice) -> Result<(), ()> { + // SAFETY: Contracts are executed on a single thread thus we know no one + // will change guest memory mapping under us. + unsafe { self.get_ptr(slice.ptr, slice.len()?) }.map(|_| ()) + } + + fn view_memory(&self, slice: MemSlice) -> Result, ()> { + // SAFETY: Firstly, contracts are executed on a single thread thus we + // know no one will change guest memory mapping under us. Secondly, the + // way MemoryLike interface is used we know the memory mapping won’t be + // changed by the caller while it holds the slice reference. + unsafe { self.get(slice.ptr, slice.len()?) }.map(Cow::Borrowed) } fn read_memory(&self, offset: u64, buffer: &mut [u8]) -> Result<(), ()> { - let memory = self.get_memory_buffer(offset, buffer.len())?; - unsafe { - // SAFETY: we verified indices into are valid and the pointer will always be valid as - // well. Our runtime is currently only executing Wasm code on a single thread, so data - // races aren't a concern here. - std::ptr::copy_nonoverlapping(memory, buffer.as_mut_ptr(), buffer.len()); - } - Ok(()) + // SAFETY: Contracts are executed on a single thread thus we know no one + // will change guest memory mapping under us. + Ok(buffer.copy_from_slice(unsafe { self.get(offset, buffer.len())? })) } fn write_memory(&mut self, offset: u64, buffer: &[u8]) -> Result<(), ()> { - let memory = self.get_memory_buffer(offset, buffer.len())?; - unsafe { - // SAFETY: we verified indices into are valid and the pointer will always be valid as - // well. Our runtime is currently only executing Wasm code on a single thread, so data - // races aren't a concern here. - std::ptr::copy_nonoverlapping(buffer.as_ptr(), memory, buffer.len()); - } - Ok(()) + // SAFETY: Contracts are executed on a single thread thus we know no one + // will change guest memory mapping under us. + Ok(unsafe { self.get_mut(offset, buffer.len())? }.copy_from_slice(buffer)) } } @@ -651,33 +672,7 @@ impl crate::runner::VM for Wasmer2VM { } } -#[cfg(test)] -mod tests { - use assert_matches::assert_matches; - use wasmer_types::WASM_PAGE_SIZE; - - #[test] - fn get_memory_buffer() { - let memory = super::Wasmer2Memory::new(1, 1).unwrap(); - // these should not panic with memory out of bounds - memory.get_memory_buffer(0, WASM_PAGE_SIZE).unwrap(); - memory.get_memory_buffer(WASM_PAGE_SIZE as u64 - 1, 1).unwrap(); - memory.get_memory_buffer(WASM_PAGE_SIZE as u64, 0).unwrap(); - } - - #[test] - fn memory_data_offset() { - let memory = super::Wasmer2Memory::new(1, 1).unwrap(); - assert_matches!(memory.data_offset(0), Some((_, size)) => assert_eq!(size, WASM_PAGE_SIZE)); - assert_matches!(memory.data_offset(WASM_PAGE_SIZE as u64), Some((_, size)) => { - assert_eq!(size, 0) - }); - assert_matches!(memory.data_offset(WASM_PAGE_SIZE as u64 + 1), None); - assert_matches!(memory.data_offset(0xFFFF_FFFF_FFFF_FFFF), None); - } - - #[test] - fn test_memory_like() { - crate::tests::test_memory_like(|| Box::new(super::Wasmer2Memory::new(1, 1).unwrap())); - } +#[test] +fn test_memory_like() { + crate::tests::test_memory_like(|| Box::new(Wasmer2Memory::new(1, 1).unwrap())); } diff --git a/runtime/near-vm-runner/src/wasmtime_runner.rs b/runtime/near-vm-runner/src/wasmtime_runner.rs index ed10438f8a6..029314b529a 100644 --- a/runtime/near-vm-runner/src/wasmtime_runner.rs +++ b/runtime/near-vm-runner/src/wasmtime_runner.rs @@ -11,10 +11,10 @@ use near_vm_errors::{ VMRunnerError, WasmTrap, }; use near_vm_logic::types::PromiseResult; -use near_vm_logic::{External, MemoryLike, VMContext, VMLogic, VMOutcome}; +use near_vm_logic::{External, MemSlice, MemoryLike, VMContext, VMLogic, VMOutcome}; +use std::borrow::Cow; use std::cell::RefCell; use std::ffi::c_void; -use std::str; use wasmtime::ExternType::Func; use wasmtime::{Engine, Linker, Memory, MemoryType, Module, Store, TrapCode}; @@ -42,9 +42,8 @@ fn with_caller(func: impl FnOnce(&mut Caller) -> T) -> T { } impl MemoryLike for WasmtimeMemory { - fn fits_memory(&self, offset: u64, len: u64) -> Result<(), ()> { - let end = offset.checked_add(len).ok_or(())?; - let end = usize::try_from(end).map_err(|_| ())?; + fn fits_memory(&self, slice: MemSlice) -> Result<(), ()> { + let end = slice.end::()?; if end <= with_caller(|caller| self.0.data_size(caller)) { Ok(()) } else { @@ -52,6 +51,13 @@ impl MemoryLike for WasmtimeMemory { } } + fn view_memory(&self, slice: MemSlice) -> Result, ()> { + let range = slice.range::()?; + with_caller(|caller| { + self.0.data(caller).get(range).map(|slice| Cow::Owned(slice.to_vec())).ok_or(()) + }) + } + fn read_memory(&self, offset: u64, buffer: &mut [u8]) -> Result<(), ()> { let start = usize::try_from(offset).map_err(|_| ())?; let end = start.checked_add(buffer.len()).ok_or(())?; From e8bda2381b1990fc37e04ca2ecd351963cf9997c Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Mon, 2 Jan 2023 16:51:55 +0100 Subject: [PATCH 133/188] refactor: fix clippy issues in near-vm-errors (#8273) Part of https://github.com/near/nearcore/issues/8145
Fixed issues ``` warning: you are deriving `PartialEq` and can implement `Eq` --> runtime/near-vm-errors/src/lib.rs:292:17 | 292 | #[derive(Debug, PartialEq)] | ^^^^^^^^^ help: consider deriving `Eq` as well: `PartialEq, Eq` | = note: `#[warn(clippy::derive_partial_eq_without_eq)]` on by default = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#derive_partial_eq_without_eq warning: this returns a `Result<_, ()>` --> runtime/near-vm-errors/src/lib.rs:512:5 | 512 | pub fn downcast(self) -> Result { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: `#[warn(clippy::result_unit_err)]` on by default = help: use a custom `Error` type instead = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#result_unit_err warning: deref on an immutable reference --> runtime/near-vm-errors/src/lib.rs:548:9 | 548 | &*self | ^^^^^^ help: if you would like to reborrow, try removing `&*`: `self` | = note: `#[warn(clippy::borrow_deref_ref)]` on by default = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#borrow_deref_ref warning: `near-vm-errors` (lib test) generated 3 warnings ```
--- runtime/near-vm-errors/src/lib.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/runtime/near-vm-errors/src/lib.rs b/runtime/near-vm-errors/src/lib.rs index 4f5b919f9d6..1067fdf2748 100644 --- a/runtime/near-vm-errors/src/lib.rs +++ b/runtime/near-vm-errors/src/lib.rs @@ -289,7 +289,7 @@ pub enum HostError { Ed25519VerifyInvalidInput { msg: String }, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum VMLogicError { /// Errors coming from native Wasm VM. HostError(HostError), @@ -505,14 +505,18 @@ pub struct AnyError { any: Box, } +#[derive(Debug, thiserror::Error)] +#[error("failed to downcast")] +pub struct DowncastFailedError; + impl AnyError { pub fn new(err: E) -> AnyError { AnyError { any: Box::new(err) } } - pub fn downcast(self) -> Result { + pub fn downcast(self) -> Result { match self.any.into_any().downcast::() { Ok(it) => Ok(*it), - Err(_) => Err(()), + Err(_) => Err(DowncastFailedError), } } } @@ -545,7 +549,7 @@ impl AnyEq for T { } } fn as_any(&self) -> &dyn Any { - &*self + self } fn into_any(self: Box) -> Box { self From 912b864898c1811457517a16c87f57eabb88f13d Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Wed, 4 Jan 2023 13:18:21 +0100 Subject: [PATCH 134/188] refactor: avoid code code duplication when locking TrieCache (#8286) Introduce `TrieCache::lock` method to encapsulate `self.0.lock().expect(POISONED_LOCK_ERR)` which is currently used in quite a few places. --- .../src/trie/prefetching_trie_storage.rs | 5 ++--- core/store/src/trie/trie_storage.rs | 20 +++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/core/store/src/trie/prefetching_trie_storage.rs b/core/store/src/trie/prefetching_trie_storage.rs index 7719d4e6c41..e1e6a017537 100644 --- a/core/store/src/trie/prefetching_trie_storage.rs +++ b/core/store/src/trie/prefetching_trie_storage.rs @@ -1,5 +1,4 @@ use crate::sync_utils::Monitor; -use crate::trie::POISONED_LOCK_ERR; use crate::{ metrics, DBCol, StorageError, Store, Trie, TrieCache, TrieCachingStorage, TrieConfig, TrieStorage, @@ -224,7 +223,7 @@ impl TrieStorage for TriePrefetchingStorage { // block the main thread otherwise. fn retrieve_raw_bytes(&self, hash: &CryptoHash) -> Result, StorageError> { // Try to get value from shard cache containing most recently touched nodes. - let mut shard_cache_guard = self.shard_cache.0.lock().expect(POISONED_LOCK_ERR); + let mut shard_cache_guard = self.shard_cache.lock(); if let Some(val) = shard_cache_guard.get(hash) { return Ok(val); } @@ -270,7 +269,7 @@ impl TrieStorage for TriePrefetchingStorage { .or_else(|| { // `blocking_get` will return None if the prefetch slot has been removed // by the main thread and the value inserted into the shard cache. - let mut guard = self.shard_cache.0.lock().expect(POISONED_LOCK_ERR); + let mut guard = self.shard_cache.lock(); guard.get(hash) }) .ok_or_else(|| { diff --git a/core/store/src/trie/trie_storage.rs b/core/store/src/trie/trie_storage.rs index 306fc93612b..b478f1d6de7 100644 --- a/core/store/src/trie/trie_storage.rs +++ b/core/store/src/trie/trie_storage.rs @@ -250,15 +250,15 @@ impl TrieCache { } pub fn get(&self, key: &CryptoHash) -> Option> { - self.0.lock().expect(POISONED_LOCK_ERR).get(key) + self.lock().get(key) } pub fn clear(&self) { - self.0.lock().expect(POISONED_LOCK_ERR).clear() + self.lock().clear() } pub fn update_cache(&self, ops: Vec<(CryptoHash, Option<&[u8]>)>) { - let mut guard = self.0.lock().expect(POISONED_LOCK_ERR); + let mut guard = self.lock(); for (hash, opt_value_rc) in ops { if let Some(value_rc) = opt_value_rc { if let (Some(value), _rc) = decode_value_with_rc(&value_rc) { @@ -276,9 +276,13 @@ impl TrieCache { } } + pub(crate) fn lock(&self) -> std::sync::MutexGuard { + self.0.lock().expect(POISONED_LOCK_ERR) + } + #[cfg(test)] pub(crate) fn len(&self) -> usize { - let guard = self.0.lock().expect(POISONED_LOCK_ERR); + let guard = self.lock(); guard.len() } } @@ -505,7 +509,7 @@ impl TrieStorage for TrieCachingStorage { self.metrics.chunk_cache_misses.inc(); // Try to get value from shard cache containing most recently touched nodes. - let mut guard = self.shard_cache.0.lock().expect(POISONED_LOCK_ERR); + let mut guard = self.shard_cache.lock(); self.metrics.shard_cache_size.set(guard.len() as i64); self.metrics.shard_cache_current_total_size.set(guard.current_total_size() as i64); let val = match guard.get(hash) { @@ -574,7 +578,7 @@ impl TrieStorage for TrieCachingStorage { // 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. if val.len() < TrieConfig::max_cached_value_size() { - let mut guard = self.shard_cache.0.lock().expect(POISONED_LOCK_ERR); + let mut guard = self.shard_cache.lock(); guard.put(*hash, val.clone()); } else { self.metrics.shard_cache_too_large.inc(); @@ -807,7 +811,7 @@ mod trie_cache_tests { ) { let shard_uid = ShardUId { version: 0, shard_id: shard_id as u32 }; let trie_cache = TrieCache::new(&trie_config, shard_uid, is_view); - assert_eq!(expected_size, trie_cache.0.lock().unwrap().total_size_limit,); - assert_eq!(is_view, trie_cache.0.lock().unwrap().is_view,); + assert_eq!(expected_size, trie_cache.lock().total_size_limit,); + assert_eq!(is_view, trie_cache.lock().is_view,); } } From 87ac804771093842357ca19324945e2e38dcbc69 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Wed, 4 Jan 2023 16:09:33 +0100 Subject: [PATCH 135/188] =?UTF-8?q?Minor=20refactoring=20around=20?= =?UTF-8?q?=E2=80=98let=20=E2=80=A6=20=3D=20if=20=E2=80=A6=E2=80=99=20and?= =?UTF-8?q?=20=E2=80=98let=20=E2=80=A6=20=3D=20match=20=E2=80=A6=E2=80=99?= =?UTF-8?q?=20statements=20(#8236)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I’ve been looking at let-else actually and stumbled upon a few places where code around ‘let … = if …’ and ‘let … = match …’ could be improved. --- chain/chunks/src/lib.rs | 39 ++++++++----------- chain/network/src/broadcast/mod.rs | 5 +-- .../src/peer_manager/network_state/routing.rs | 33 ++++++++-------- runtime/near-vm-runner/src/wasmer2_runner.rs | 26 ++++++------- runtime/near-vm-runner/src/wasmer_runner.rs | 13 ++----- .../src/concurrency/weak_map.rs | 10 ++--- 6 files changed, 53 insertions(+), 73 deletions(-) diff --git a/chain/chunks/src/lib.rs b/chain/chunks/src/lib.rs index 7cafcdd812c..686b025cf79 100644 --- a/chain/chunks/src/lib.rs +++ b/chain/chunks/src/lib.rs @@ -603,32 +603,25 @@ impl ShardsManager { continue; } - let need_to_fetch_part = if request_full || seal.contains_part_ord(&part_ord) { - true + // Note: If request_from_archival is true, we potentially call + // get_part_owner unnecessarily. It’s probably not worth optimising + // though unless you can think of a concise way to do it. + let part_owner = self.runtime_adapter.get_part_owner(&epoch_id, part_ord)?; + let we_own_part = Some(&part_owner) == me; + if !request_full && !we_own_part && !seal.contains_part_ord(&part_ord) { + continue; + } + + let fetch_from = if request_from_archival { + shard_representative_target.clone() + } else if we_own_part { + // If missing own part, request it from the chunk producer / node tracking shard + shard_representative_target.clone() } else { - if let Some(me) = me { - &self.runtime_adapter.get_part_owner(&epoch_id, part_ord)? == me - } else { - false - } + Some(part_owner) }; - if need_to_fetch_part { - let fetch_from = if request_from_archival { - shard_representative_target.clone() - } else { - let part_owner = self.runtime_adapter.get_part_owner(&epoch_id, part_ord)?; - - if Some(&part_owner) == me { - // If missing own part, request it from the chunk producer / node tracking shard - shard_representative_target.clone() - } else { - Some(part_owner) - } - }; - - bp_to_parts.entry(fetch_from).or_default().push(part_ord); - } + bp_to_parts.entry(fetch_from).or_default().push(part_ord); } let shards_to_fetch_receipts = diff --git a/chain/network/src/broadcast/mod.rs b/chain/network/src/broadcast/mod.rs index 04d063a1c60..0e9715478c5 100644 --- a/chain/network/src/broadcast/mod.rs +++ b/chain/network/src/broadcast/mod.rs @@ -67,9 +67,8 @@ impl Receiver { let new_value_pushed = self.channel.notify.notified(); // Synchronically check if the channel is non-empty. // If so, pop a value and return immediately. - let v = if l.len() > self.next - 1 { Some(l[self.next - 1].clone()) } else { None }; - if let Some(v) = v { - return v; + if let Some(v) = l.get(self.next - 1) { + return v.clone(); } new_value_pushed }; diff --git a/chain/network/src/peer_manager/network_state/routing.rs b/chain/network/src/peer_manager/network_state/routing.rs index a272f0c62af..f326db28cf8 100644 --- a/chain/network/src/peer_manager/network_state/routing.rs +++ b/chain/network/src/peer_manager/network_state/routing.rs @@ -90,17 +90,26 @@ impl NetworkState { // instead, however that would be backward incompatible, so it can be introduced in // PROTOCOL_VERSION 60 earliest. let (edges, ok) = self.graph.verify(edges).await; - let result = match ok { - true => Ok(()), - false => Err(ReasonForBan::InvalidEdge), - }; - // Skip recomputation if no new edges have been verified. - if edges.len() == 0 { - return result; + // Recompute if there are new edges which have been verified. + if !edges.is_empty() { + self.recompute_added_edges(clock.clone(), edges).await; } + // self.graph.verify() returns a partial result if it encountered an invalid edge: + // Edge verification is expensive, and it would be an attack vector if we dropped on the + // floor valid edges verified so far: an attacker could prepare a SyncRoutingTable + // containing a lot of valid edges, except for the last one, and send it repeatedly to a + // node. The node would then validate all the edges every time, then reject the whole set + // because just the last edge was invalid. Instead, we accept all the edges verified so + // far and return an error only afterwards. + if ok { + Ok(()) + } else { + Err(ReasonForBan::InvalidEdge) + } + } + async fn recompute_added_edges(self: &Arc, clock: time::Clock, edges: Vec) { let this = self.clone(); - let clock = clock.clone(); let _ = self .add_edges_demux .call(edges, |edges: Vec>| async move { @@ -123,13 +132,5 @@ impl NetworkState { results }) .await; - // self.graph.verify() returns a partial result if it encountered an invalid edge: - // Edge verification is expensive, and it would be an attack vector if we dropped on the - // floor valid edges verified so far: an attacker could prepare a SyncRoutingTable - // containing a lot of valid edges, except for the last one, and send it repeatedly to a - // node. The node would then validate all the edges every time, then reject the whole set - // because just the last edge was invalid. Instead, we accept all the edges verified so - // far and return an error only afterwards. - result } } diff --git a/runtime/near-vm-runner/src/wasmer2_runner.rs b/runtime/near-vm-runner/src/wasmer2_runner.rs index 4ea35b630c1..c01e7755048 100644 --- a/runtime/near-vm-runner/src/wasmer2_runner.rs +++ b/runtime/near-vm-runner/src/wasmer2_runner.rs @@ -381,22 +381,18 @@ impl Wasmer2VM { } }; - let artifact: VMArtifact = match stored_artifact { - Some(it) => it, - None => { - let executable_or_error = self.compile_and_cache(code, cache)?; - match executable_or_error { - Ok(executable) => self - .engine - .load_universal_executable(&executable) - .map(Arc::new) - .map_err(|err| VMRunnerError::LoadingError(err.to_string()))?, - Err(it) => return Ok(Err(it)), - } + Ok(if let Some(it) = stored_artifact { + Ok(it) + } else { + match self.compile_and_cache(code, cache)? { + Ok(executable) => Ok(self + .engine + .load_universal_executable(&executable) + .map(Arc::new) + .map_err(|err| VMRunnerError::LoadingError(err.to_string()))?), + Err(err) => Err(err), } - }; - - Ok(Ok(artifact)) + }) }; #[cfg(feature = "no_cache")] diff --git a/runtime/near-vm-runner/src/wasmer_runner.rs b/runtime/near-vm-runner/src/wasmer_runner.rs index d6f95282acc..dc5ff56b1ce 100644 --- a/runtime/near-vm-runner/src/wasmer_runner.rs +++ b/runtime/near-vm-runner/src/wasmer_runner.rs @@ -332,15 +332,10 @@ impl Wasmer0VM { } }; - let module = match stored_module { - Some(it) => it, - None => match self.compile_and_cache(code, cache)? { - Ok(it) => it, - Err(it) => return Ok(Err(it)), - }, - }; - - Ok(Ok(module)) + Ok(match stored_module { + Some(it) => Ok(it), + None => self.compile_and_cache(code, cache)?, + }) }; #[cfg(feature = "no_cache")] diff --git a/tools/chainsync-loadtest/src/concurrency/weak_map.rs b/tools/chainsync-loadtest/src/concurrency/weak_map.rs index c16e963790d..2bc7d8879e1 100644 --- a/tools/chainsync-loadtest/src/concurrency/weak_map.rs +++ b/tools/chainsync-loadtest/src/concurrency/weak_map.rs @@ -34,14 +34,10 @@ impl Drop for Ref { // because that might trigger this function // recursively and cause a deadlock. let mut m = self.map.inner.lock().unwrap(); - let e = match m.entry(self.key.clone()) { - Entry::Occupied(e) => e, - Entry::Vacant(_) => { - return; + if let Entry::Occupied(e) = m.entry(self.key.clone()) { + if e.get().strong_count() == 0 { + e.remove_entry(); } - }; - if e.get().strong_count() == 0 { - e.remove_entry(); } } } From 8c3dca907952ea332aec7570b374474b9baaf0a3 Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Wed, 4 Jan 2023 20:08:27 +0100 Subject: [PATCH 136/188] feat: try reading shard_cache if prefetcher blocking_get returns None (#8287) This is the second part of https://github.com/near/nearcore/issues/7723: avoid an unnecessary storage lookup from the main thread in the scenario of forks. --- core/store/src/metrics.rs | 8 ++++++++ core/store/src/trie/prefetching_trie_storage.rs | 3 +-- core/store/src/trie/trie_storage.rs | 15 ++++++++++++--- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/core/store/src/metrics.rs b/core/store/src/metrics.rs index a569701809b..213c5e6e93d 100644 --- a/core/store/src/metrics.rs +++ b/core/store/src/metrics.rs @@ -181,6 +181,14 @@ pub static PREFETCH_MEMORY_LIMIT_REACHED: Lazy = Lazy::new(|| { ) .unwrap() }); +pub static PREFETCH_CONFLICT: Lazy = Lazy::new(|| { + try_create_int_counter_vec( + "near_prefetch_conflict", + "Main thread retrieved value from shard_cache after a conflict with another main thread from a fork.", + &["shard_id"], + ) + .unwrap() +}); pub static PREFETCH_RETRY: Lazy = Lazy::new(|| { try_create_int_counter_vec( "near_prefetch_retries", diff --git a/core/store/src/trie/prefetching_trie_storage.rs b/core/store/src/trie/prefetching_trie_storage.rs index e1e6a017537..b5d9cbcf625 100644 --- a/core/store/src/trie/prefetching_trie_storage.rs +++ b/core/store/src/trie/prefetching_trie_storage.rs @@ -269,8 +269,7 @@ impl TrieStorage for TriePrefetchingStorage { .or_else(|| { // `blocking_get` will return None if the prefetch slot has been removed // by the main thread and the value inserted into the shard cache. - let mut guard = self.shard_cache.lock(); - guard.get(hash) + self.shard_cache.get(hash) }) .ok_or_else(|| { // This could only happen if this thread started prefetching a value diff --git a/core/store/src/trie/trie_storage.rs b/core/store/src/trie/trie_storage.rs index b478f1d6de7..47aec1837a0 100644 --- a/core/store/src/trie/trie_storage.rs +++ b/core/store/src/trie/trie_storage.rs @@ -414,6 +414,7 @@ struct TrieCacheInnerMetrics { prefetch_not_requested: GenericCounter, prefetch_memory_limit_reached: GenericCounter, prefetch_retry: GenericCounter, + prefetch_conflict: GenericCounter, } impl TrieCachingStorage { @@ -447,6 +448,7 @@ impl TrieCachingStorage { prefetch_memory_limit_reached: metrics::PREFETCH_MEMORY_LIMIT_REACHED .with_label_values(&metrics_labels[..1]), prefetch_retry: metrics::PREFETCH_RETRY.with_label_values(&metrics_labels[..1]), + prefetch_conflict: metrics::PREFETCH_CONFLICT.with_label_values(&metrics_labels[..1]), }; TrieCachingStorage { store, @@ -560,10 +562,17 @@ impl TrieStorage for TrieCachingStorage { // therefore blocking read will usually not return empty unless there // was a storage error. Or in the case of forks and parallel chunk // processing where one chunk cleans up prefetched data from the other. - // In any case, we can try again from the main thread. + // So first we need to check if the data was inserted to shard_cache + // by the main thread from another fork and only if that fails then + // fetch the data from the DB. None => { - self.metrics.prefetch_retry.inc(); - self.read_from_db(hash)? + if let Some(value) = self.shard_cache.get(hash) { + self.metrics.prefetch_conflict.inc(); + value + } else { + self.metrics.prefetch_retry.inc(); + self.read_from_db(hash)? + } } } } From 9e4ff14a1c8e595d3f7b7f0ff37680fc5666d850 Mon Sep 17 00:00:00 2001 From: wacban Date: Thu, 5 Jan 2023 09:47:32 +0000 Subject: [PATCH 137/188] [fix] Set tracked shards to none in pytest localnet command usage (#8282) --- pytest/lib/cluster.py | 17 +++++++++++++---- pytest/tests/sanity/repro_2916.py | 4 ++-- scripts/nayduck.py | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/pytest/lib/cluster.py b/pytest/lib/cluster.py index fa8af93af44..bf38b24711e 100644 --- a/pytest/lib/cluster.py +++ b/pytest/lib/cluster.py @@ -717,11 +717,20 @@ def init_cluster(num_nodes, num_observers, num_shards, config, logger.info("Creating %s cluster configuration with %s nodes" % ("LOCAL" if is_local else "REMOTE", num_nodes + num_observers)) + binary_path = os.path.join(near_root, binary_name) process = subprocess.Popen([ - os.path.join(near_root, binary_name), "localnet", "--v", - str(num_nodes), "--shards", - str(num_shards), "--n", - str(num_observers), "--prefix", "test" + binary_path, + "localnet", + "--validators", + str(num_nodes), + "--non-validators", + str(num_observers), + "--shards", + str(num_shards), + "--tracked-shards", + "none", + "--prefix", + "test", ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) diff --git a/pytest/tests/sanity/repro_2916.py b/pytest/tests/sanity/repro_2916.py index f88ab1496bc..a5c4be2812d 100755 --- a/pytest/tests/sanity/repro_2916.py +++ b/pytest/tests/sanity/repro_2916.py @@ -12,8 +12,8 @@ # incorrect reconstruction of the receipts. import asyncio, sys, time -import socket, base58 -import nacl.signing, hashlib +import base58 +import nacl.signing import pathlib sys.path.append(str(pathlib.Path(__file__).resolve().parents[2] / 'lib')) diff --git a/scripts/nayduck.py b/scripts/nayduck.py index 6504487b88d..d3606e3283e 100755 --- a/scripts/nayduck.py +++ b/scripts/nayduck.py @@ -254,7 +254,7 @@ def run_locally(args, tests): shlex.quote(str(cwd)), ' '.join(shlex.quote(str(arg)) for arg in cmd))) continue - print("RUNNING COMMAND cwd=%s cmd = %s", (cwd, cmd)) + print(f"RUNNING COMMAND cwd = {cwd} cmd = {cmd}") subprocess.check_call(cmd, cwd=cwd, timeout=_parse_timeout(timeout)) From 3ddb60857cbe16c9b26861eb0d01ee09d218f3b5 Mon Sep 17 00:00:00 2001 From: nikurt <86772482+nikurt@users.noreply.github.com> Date: Thu, 5 Jan 2023 12:40:41 +0100 Subject: [PATCH 138/188] Fuzz 1.31.0 (#8281) --- nightly/fuzz.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nightly/fuzz.toml b/nightly/fuzz.toml index 3a5a4fedc80..204f4e3db01 100644 --- a/nightly/fuzz.toml +++ b/nightly/fuzz.toml @@ -3,11 +3,11 @@ name = "master" weight = 10 [[branch]] -name = "1.30.0" +name = "1.31.0" weight = 10 [[branch]] -name = "1.29.0" +name = "1.30.0" weight = 10 [[target]] From 009b1cbf29d653deaa5e73e33ed8b846fcb1601f Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Thu, 5 Jan 2023 13:55:33 +0100 Subject: [PATCH 139/188] fix: Deterministic tx size in estimation (#8285) Fixes a problem for daily estimations, introduced in #8251. Because account IDs were not constant, the TX sizes depended on the randomly picked indices, which caused some failures in daily estimations where more accounts were used than in local testing. --- genesis-tools/genesis-populate/src/lib.rs | 20 +++++++++++- .../src/action_costs.rs | 32 ++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/genesis-tools/genesis-populate/src/lib.rs b/genesis-tools/genesis-populate/src/lib.rs index ebc9f0d0e15..4de8a535600 100644 --- a/genesis-tools/genesis-populate/src/lib.rs +++ b/genesis-tools/genesis-populate/src/lib.rs @@ -23,11 +23,29 @@ use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; use std::sync::Arc; +/// Deterministically construct an account ID by index. +/// +/// This is used by the estimator to fill a DB with many accounts for which the +/// name can be constructed again during estimations. +/// +/// The ids are constructed to form a somewhat interesting shape in the trie. It +/// starts with a hash that will be different for each account, followed by a +/// string that is sufficiently long. The hash is supposed to produces a bunch +/// of branches, whereas the string after that will produce an extension. +/// +/// If anyone has a reason to change the format, there is no strong reason to +/// keep it exactly as it is. But keeping the length of the accounts the same +/// would be desired to avoid breaking tests and estimations. +/// +/// Note that existing estimator DBs need to be reconstructed when the format +/// changes. Daily estimations are not affected by this. pub fn get_account_id(account_index: u64) -> AccountId { let mut hasher = std::collections::hash_map::DefaultHasher::new(); account_index.hash(&mut hasher); let hash = hasher.finish(); - AccountId::try_from(format!("{hash}_near_{account_index}_{account_index}")).unwrap() + // Some estimations rely on the account ID length being constant. + // Pad booth numbers to length 20, the longest decimal representation of an u64. + AccountId::try_from(format!("{hash:020}_near_{account_index:020}")).unwrap() } pub type Result = std::result::Result>; diff --git a/runtime/runtime-params-estimator/src/action_costs.rs b/runtime/runtime-params-estimator/src/action_costs.rs index 4e80b87760c..ed922c36c9d 100644 --- a/runtime/runtime-params-estimator/src/action_costs.rs +++ b/runtime/runtime-params-estimator/src/action_costs.rs @@ -614,7 +614,37 @@ impl ActionSize { // This size exactly touches tx limit with 1 deploy action. If this suddenly // fails with `InvalidTxError(TransactionSizeExceeded`, it could be a // protocol change due to the TX limit computation changing. - ActionSize::Max => 4 * 1024 * 1024 - 160, + // The test `test_deploy_contract_tx_max_size` checks this. + ActionSize::Max => 4 * 1024 * 1024 - 182, } } } + +#[cfg(test)] +mod tests { + use super::{deploy_action, ActionSize}; + use genesis_populate::get_account_id; + + #[test] + fn test_deploy_contract_tx_max_size() { + // The size of a transaction constructed from this must be exactly at the limit. + let deploy_action = deploy_action(ActionSize::Max); + let limit = 4_194_304; + + // We also need some account IDs constructed the same way as in the estimator. + // Let's try multiple index sizes to ensure this does not affect the length. + let sender_0 = get_account_id(0); + let receiver_0 = get_account_id(1); + let sender_1 = get_account_id(1000); + let receiver_1 = get_account_id(20001); + let test_accounts = + vec![sender_0.clone(), sender_1.clone(), receiver_0.clone(), receiver_1.clone()]; + let mut tb = crate::TransactionBuilder::new(test_accounts); + + let tx_0 = tb.transaction_from_actions(sender_0, receiver_0, vec![deploy_action.clone()]); + assert_eq!(tx_0.get_size(), limit, "TX size changed"); + + let tx_1 = tb.transaction_from_actions(sender_1, receiver_1, vec![deploy_action]); + assert_eq!(tx_1.get_size(), limit, "TX size changed"); + } +} From 09c44e5bb4ab3813c9053df356e6197e516bcab3 Mon Sep 17 00:00:00 2001 From: nikurt <86772482+nikurt@users.noreply.github.com> Date: Thu, 5 Jan 2023 14:18:53 +0100 Subject: [PATCH 140/188] Update changelog for 1.30 and 1.31 (#8283) @pompon0 Please check TIER1 major release version and referenced PRs. --- CHANGELOG.md | 93 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a49bf11b48..d2d9154825e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,64 @@ ### Protocol Changes -* Stabilize `account_id_in_function_call_permission` feature: enforcing validity - of account ids in function call permission. +### Non-protocol Changes + +## 1.31.0 + +### Non-protocol Changes + * Enable TIER1 network. Participants of the BFT consensus (block & chunk producers) now can establish direct TIER1 connections between each other, which will optimize the communication latency and minimize the number of dropped chunks. To configure this feature, see [advanced\_configuration/networking](./docs/advanced_configuration/networking.md). + [#8141](https://github.com/near/nearcore/pull/8141) + [#8085](https://github.com/near/nearcore/pull/8085) + [#7759](https://github.com/near/nearcore/pull/7759) +* [Network] Started creating connections with larger nonces, that are periodically + refreshed Start creating connections (edges) with large nonces + [#7966](https://github.com/near/nearcore/pull/7966) +* `/status` response has now two more fields: `node_public_key` and + `validator_public_key`. The `node_key` field is now deprecated and should not + be used since it confusingly holds validator key. + [#7828](https://github.com/near/nearcore/pull/7828) +* Added `near_node_protocol_upgrade_voting_start` Prometheus metric whose value + is timestamp when voting for the next protocol version starts. + [#7877](https://github.com/near/nearcore/pull/7877) +* neard cmd can now verify proofs from JSON files. + [#7840](https://github.com/near/nearcore/pull/7840) +* In storage configuration, the value `trie_cache_capacities` now is no longer + a hard limit but instead sets a memory consumption limit. For large trie nodes, + the limits are close to equivalent. For small values, there can now fit more + in the cache than previously. + [#7749](https://github.com/near/nearcore/pull/7749) +* New options `store.trie_cache` and `store.view_trie_cache` in `config.json` + to set limits on the trie cache. Deprecates the never announced + `store.trie_cache_capacities` option which was mentioned in previous change. + [#7578](https://github.com/near/nearcore/pull/7578) +* New option `store.background_migration_threads` in `config.json`. Defines + number of threads to execute background migrations of storage. Currently used + for flat storage migration. Set to 8 by default, can be reduced if it slows down + block processing too much or increased if you want to speed up migration. + [#8088](https://github.com/near/nearcore/pull/8088), +* Tracing of work across actix workers within a process: + [#7866](https://github.com/near/nearcore/pull/7866), + [#7819](https://github.com/near/nearcore/pull/7819), + [#7773](https://github.com/near/nearcore/pull/7773). +* Scope of collected tracing information can be configured at run-time: + [#7701](https://github.com/near/nearcore/pull/7701). +* Attach node's `chain_id`, `node_id`, and `account_id` values to tracing + information: [#7711](https://github.com/near/nearcore/pull/7711). +* Change exporter of tracing information from `opentelemetry-jaeger` to + `opentelemetry-otlp`: [#7563](https://github.com/near/nearcore/pull/7563). +* Tracing of requests across processes: + [#8004](https://github.com/near/nearcore/pull/8004). + +## 1.30.0 + +### Protocol Changes + +* Stabilize `account_id_in_function_call_permission` feature: enforcing validity + of account ids in function call permission. + [#7569](https://github.com/near/nearcore/pull/7569) ### Non-protocol Changes @@ -16,18 +69,15 @@ deprecated. If they are set in `config.json` the node will fail if migration needs to be performed. Use `store.migration_snapshot` instead to configure the behaviour [#7486](https://github.com/near/nearcore/pull/7486) -* `/status` response has now two more fields: `node_public_key` and - `validator_public_key`. The `node_key` field is now deprecated and should not - be used since it confusingly holds validator key. * Added `near_peer_message_sent_by_type_bytes` and `near_peer_message_sent_by_type_total` Prometheus metrics measuring size and number of messages sent to peers. + [#7523](https://github.com/near/nearcore/pull/7523) * `near_peer_message_received_total` Prometheus metric is now deprecated. Instead of it aggregate `near_peer_message_received_by_type_total` metric. For example, to get total rate of received messages use `sum(rate(near_peer_message_received_by_type_total{...}[5m]))`. -* Added `near_node_protocol_upgrade_voting_start` Prometheus metric whose value - is timestamp when voting for the next protocol version starts. + [#7548](https://github.com/near/nearcore/pull/7548) * Few changes to `view_state` JSON RPC query: - The requset has now an optional `include_proof` argument. When set to `true`, response’s `proof` will be populated. @@ -38,40 +88,15 @@ sent even if proof has not been requested. In the future the field will be skipped in those cases. Clients should accept responses with this field missing (unless they set `include_proof`). + [#7603](https://github.com/near/nearcore/pull/7603) * Backtraces on panics are enabled by default, so you no longer need to set `RUST_BACKTRACE=1` environmental variable. To disable backtraces, set - `RUST_BACKTRACE=0`. + `RUST_BACKTRACE=0`. [#7562](https://github.com/near/nearcore/pull/7562) * Enable receipt prefetching by default. This feature makes receipt processing faster by parallelizing IO requests, which has been introduced in [#7590](https://github.com/near/nearcore/pull/7590) and enabled by default with [#7661](https://github.com/near/nearcore/pull/7661). Configurable in `config.json` using `store.enable_receipt_prefetching`. -* neard cmd can now verify proofs from JSON files. -* In storage configuration, the value `trie_cache_capacities` now is no longer - a hard limit but instead sets a memory consumption limit. For large trie nodes, - the limits are close to equivalent. For small values, there can now fit more - in the cache than previously. - [#7749](https://github.com/near/nearcore/pull/7749) -* New options `store.trie_cache` and `store.view_trie_cache` in `config.json` - to set limits on the trie cache. Deprecates the never announced - `store.trie_cache_capacities` option which was mentioned in previous change. - [#7578](https://github.com/near/nearcore/pull/7578) -* New option `store.background_migration_threads` in `config.json`. Defines - number of threads to execute background migrations of storage. Currently used - for flat storage migration. Set to 8 by default, can be reduced if it slows down - block processing too much or increased if you want to speed up migration. -* Tracing of work across actix workers within a process: - [#7866](https://github.com/near/nearcore/pull/7866), - [#7819](https://github.com/near/nearcore/pull/7819), - [#7773](https://github.com/near/nearcore/pull/7773). -* Scope of collected tracing information can be configured at run-time: - [#7701](https://github.com/near/nearcore/pull/7701). -* Attach node's `chain_id`, `node_id`, and `account_id` values to tracing - information: [#7711](https://github.com/near/nearcore/pull/7711). -* Change exporter of tracing information from `opentelemetry-jaeger` to - `opentelemetry-otlp`: [#7563](https://github.com/near/nearcore/pull/7563). -* Tracing of requests across processes: - [#8004](https://github.com/near/nearcore/pull/8004). ## 1.29.0 [2022-08-15] From 45c81db332e6afa0f5468616d5ba337bf214f3e0 Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Thu, 5 Jan 2023 15:53:09 +0100 Subject: [PATCH 141/188] refactor: fix clippy issues in near-primitives-core (#8246) Part of #8145 Please note the change in `to_base58_impl` interface to address the following: ``` warning: methods with the following characteristics: (`to_*` and `self` type is `Copy`) usually take `self` by value --> core/primitives-core/src/hash.rs:78:28 | 78 | fn to_base58_impl(&self, visitor: impl FnOnce(&str) -> Out) -> Out { | ^^^^^ | = note: `#[warn(clippy::wrong_self_convention)]` on by default = help: consider choosing a less ambiguous name = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#wrong_self_convention ``` The size of `CyptoHash` is only 32 stack-allocated bytes, so copying instead of passing by reference should not introduce any runtime overhead. --- Cargo.lock | 1 + core/primitives-core/Cargo.toml | 1 + core/primitives-core/src/hash.rs | 15 +++++++++------ core/primitives-core/src/serialize.rs | 12 ++++++++---- core/primitives-core/src/types.rs | 4 ++-- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18fdb868f83..16bae69b109 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3419,6 +3419,7 @@ dependencies = [ "serde_repr", "sha2 0.10.2", "strum", + "thiserror", ] [[package]] diff --git a/core/primitives-core/Cargo.toml b/core/primitives-core/Cargo.toml index bc013f52dd1..2c3bd756710 100644 --- a/core/primitives-core/Cargo.toml +++ b/core/primitives-core/Cargo.toml @@ -24,6 +24,7 @@ serde.workspace = true serde_repr.workspace = true sha2.workspace = true strum.workspace = true +thiserror.workspace = true near-account-id = { path = "../account-id", features = ["arbitrary"] } diff --git a/core/primitives-core/src/hash.rs b/core/primitives-core/src/hash.rs index 00ab3caf6e2..ce384d55cdf 100644 --- a/core/primitives-core/src/hash.rs +++ b/core/primitives-core/src/hash.rs @@ -75,7 +75,7 @@ impl CryptoHash { /// The conversion is performed without any memory allocation. The visitor /// is given a reference to a string stored on stack. Returns whatever the /// visitor returns. - fn to_base58_impl(&self, visitor: impl FnOnce(&str) -> Out) -> Out { + fn to_base58_impl(self, visitor: impl FnOnce(&str) -> Out) -> Out { // base58-encoded string is at most 1.4 times longer than the binary // sequence. We’re serialising 32 bytes so ⌈32 * 1.4⌉ = 45 should be // enough. @@ -221,6 +221,9 @@ impl fmt::Display for CryptoHash { } } +// This implementation is compatible with derived PartialEq. +// Custom PartialEq implementation was explicitly removed in #4220. +#[allow(clippy::derive_hash_xor_eq)] impl Hash for CryptoHash { fn hash(&self, state: &mut H) { state.write(self.as_ref()); @@ -259,7 +262,7 @@ mod tests { } fn slice(want: &str, slice: &[T]) { - assert_eq!(want, CryptoHash::hash_borsh(&slice).to_string()); + assert_eq!(want, CryptoHash::hash_borsh(slice).to_string()); iter(want, slice.iter()); iter(want, slice); } @@ -280,12 +283,12 @@ mod tests { slice("CuoNgQBWsXnTqup6FY3UXNz6RRufnYyQVxx8HKZLUaRt", "foo".as_bytes()); iter( "CuoNgQBWsXnTqup6FY3UXNz6RRufnYyQVxx8HKZLUaRt", - "FOO".bytes().map(|ch| u8::try_from(ch.to_ascii_lowercase()).unwrap()), + "FOO".bytes().map(|ch| ch.to_ascii_lowercase()), ); value("3yMApqCuCjXDWPrbjfR5mjCPTHqFG8Pux1TxQrEM35jj", b"foo"); value("3yMApqCuCjXDWPrbjfR5mjCPTHqFG8Pux1TxQrEM35jj", [b'f', b'o', b'o']); - value("3yMApqCuCjXDWPrbjfR5mjCPTHqFG8Pux1TxQrEM35jj", &[b'f', b'o', b'o']); + value("3yMApqCuCjXDWPrbjfR5mjCPTHqFG8Pux1TxQrEM35jj", [b'f', b'o', b'o']); slice("CuoNgQBWsXnTqup6FY3UXNz6RRufnYyQVxx8HKZLUaRt", &[b'f', b'o', b'o']); } @@ -326,7 +329,7 @@ mod tests { "1".repeat(33), "1".repeat(1000), ] { - test(&encoded, "incorrect length for hash"); + test(encoded, "incorrect length for hash"); } } @@ -350,7 +353,7 @@ mod tests { format!("\"{}\"", "1".repeat(33)), format!("\"{}\"", "1".repeat(1000)), ] { - test(&encoded, "invalid length"); + test(encoded, "invalid length"); } } } diff --git a/core/primitives-core/src/serialize.rs b/core/primitives-core/src/serialize.rs index 233eaf1abd5..22da996f9d2 100644 --- a/core/primitives-core/src/serialize.rs +++ b/core/primitives-core/src/serialize.rs @@ -98,6 +98,10 @@ pub mod dec_format { use serde::de; use serde::{Deserializer, Serializer}; + #[derive(thiserror::Error, Debug)] + #[error("cannot parse from unit")] + pub struct ParseUnitError; + /// Abstraction between integers that we serialise. pub trait DecType: Sized { /// Formats number as a decimal string; passes `None` as is. @@ -105,8 +109,8 @@ pub mod dec_format { /// Constructs Self from a `null` value. Returns error if this type /// does not accept `null` values. - fn try_from_unit() -> Result { - Err(()) + fn try_from_unit() -> Result { + Err(ParseUnitError) } /// Tries to parse decimal string as an integer. @@ -144,7 +148,7 @@ pub mod dec_format { fn serialize(&self) -> Option { self.as_ref().and_then(DecType::serialize) } - fn try_from_unit() -> Result { + fn try_from_unit() -> Result { Ok(None) } fn try_from_str(value: &str) -> Result { @@ -165,7 +169,7 @@ pub mod dec_format { } fn visit_unit(self) -> Result { - T::try_from_unit().map_err(|()| de::Error::invalid_type(de::Unexpected::Option, &self)) + T::try_from_unit().map_err(|_| de::Error::invalid_type(de::Unexpected::Option, &self)) } fn visit_u64(self, value: u64) -> Result { diff --git a/core/primitives-core/src/types.rs b/core/primitives-core/src/types.rs index ad3acab8ec6..9689d756d0e 100644 --- a/core/primitives-core/src/types.rs +++ b/core/primitives-core/src/types.rs @@ -27,13 +27,13 @@ pub type Gas = u64; /// Weight of unused gas to distribute to scheduled function call actions. /// Used in `promise_batch_action_function_call_weight` host function. -#[derive(Clone, Debug, PartialEq)] +#[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)] +#[derive(Debug, PartialEq, Eq)] pub enum GasDistribution { /// All remaining gas was distributed to functions. All, From abe7c13992e53d874fc391c07c1b611b80dc4719 Mon Sep 17 00:00:00 2001 From: Marcelo Diop-Gonzalez Date: Thu, 5 Jan 2023 11:20:22 -0500 Subject: [PATCH 142/188] fix(integration-tests): fix tests after tx status RPC changes (#8289) https://github.com/near/nearcore/pull/7928 changed `Chain::get_recursive_transaction_results()` to skip refund receipts, so the several checks for receipts outcome length in `standard_cases::rpc` are now wrong, because they expect to find the refunds. Fix it by changing all these asserts, and by changing `RuntimeUser::get_recursive_transaction_results` to match `Chain::get_recursive_transaction_results` failed tests (the ones that start with `standard_cases::rpc::`): https://nayduck.near.org/#/run/2802 to be safe, can check that these are indeed refunds by printing rceipts out like: ``` for r in transaction_result.receipts_outcome.iter() { let b = node_user.get_block(r.block_hash).unwrap(); let mut found = false; 'outer: for c in b.chunks { let ch = node_user.get_chunk_by_height(b.header.height, c.shard_id).unwrap(); for receipt in ch.receipts { if receipt.receipt_id == r.id { dbg!(receipt); found = true; break 'outer; } } } if !found { eprintln!("cant find {}", r.id); } } ``` --- .../src/tests/runtime/deployment.rs | 2 +- .../src/tests/runtime/sanity_checks.rs | 2 +- ...__sanity_checks__receipts_gas_profile.snap | 13 ----- ..._checks__receipts_gas_profile_nightly.snap | 13 ----- ...receipts_gas_profile_nondeterministic.snap | 1 - ...ntime__sanity_checks__receipts_status.snap | 15 +----- ...cks__receipts_status_nondeterministic.snap | 2 - .../src/tests/runtime/test_evil_contracts.rs | 2 +- .../src/tests/standard_cases/mod.rs | 48 +++++++++---------- integration-tests/src/user/runtime_user.rs | 6 +++ 10 files changed, 34 insertions(+), 70 deletions(-) diff --git a/integration-tests/src/tests/runtime/deployment.rs b/integration-tests/src/tests/runtime/deployment.rs index ccc0b6d5283..f8f9ef87a31 100644 --- a/integration-tests/src/tests/runtime/deployment.rs +++ b/integration-tests/src/tests/runtime/deployment.rs @@ -51,7 +51,7 @@ fn test_deploy_max_size_contract() { ) .unwrap(); assert_eq!(transaction_result.status, FinalExecutionStatus::SuccessValue(Vec::new())); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); // Deploy contract let wasm_binary = near_test_contracts::sized_contract(contract_size as usize); diff --git a/integration-tests/src/tests/runtime/sanity_checks.rs b/integration-tests/src/tests/runtime/sanity_checks.rs index 2eaa7a30933..3e2956a248f 100644 --- a/integration-tests/src/tests/runtime/sanity_checks.rs +++ b/integration-tests/src/tests/runtime/sanity_checks.rs @@ -63,7 +63,7 @@ fn setup_runtime_node_with_contract(wasm_binary: &[u8]) -> RuntimeNode { ) .unwrap(); assert_eq!(tx_result.status, FinalExecutionStatus::SuccessValue(Vec::new())); - assert_eq!(tx_result.receipts_outcome.len(), 2); + assert_eq!(tx_result.receipts_outcome.len(), 1); let tx_result = node_user.deploy_contract(test_contract_account(), wasm_binary.to_vec()).unwrap(); diff --git a/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile.snap b/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile.snap index 4ef88917129..516c8ba19a0 100644 --- a/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile.snap +++ b/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile.snap @@ -307,7 +307,6 @@ expression: receipts_gas_profile gas_used: 0, }, ], - [], [ CostGasUsed { cost_category: "WASM_HOST_COST", @@ -350,7 +349,6 @@ expression: receipts_gas_profile gas_used: 0, }, ], - [], [ CostGasUsed { cost_category: "WASM_HOST_COST", @@ -363,7 +361,6 @@ expression: receipts_gas_profile gas_used: 0, }, ], - [], [ CostGasUsed { cost_category: "WASM_HOST_COST", @@ -376,7 +373,6 @@ expression: receipts_gas_profile gas_used: 0, }, ], - [], [ CostGasUsed { cost_category: "WASM_HOST_COST", @@ -391,9 +387,6 @@ expression: receipts_gas_profile ], [], [], - [], - [], - [], [ CostGasUsed { cost_category: "WASM_HOST_COST", @@ -407,9 +400,6 @@ expression: receipts_gas_profile }, ], [], - [], - [], - [], [ CostGasUsed { cost_category: "WASM_HOST_COST", @@ -422,7 +412,6 @@ expression: receipts_gas_profile gas_used: 0, }, ], - [], [ CostGasUsed { cost_category: "WASM_HOST_COST", @@ -450,6 +439,4 @@ expression: receipts_gas_profile gas_used: 2865522486, }, ], - [], - [], ] diff --git a/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile_nightly.snap b/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile_nightly.snap index 4ef88917129..516c8ba19a0 100644 --- a/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile_nightly.snap +++ b/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile_nightly.snap @@ -307,7 +307,6 @@ expression: receipts_gas_profile gas_used: 0, }, ], - [], [ CostGasUsed { cost_category: "WASM_HOST_COST", @@ -350,7 +349,6 @@ expression: receipts_gas_profile gas_used: 0, }, ], - [], [ CostGasUsed { cost_category: "WASM_HOST_COST", @@ -363,7 +361,6 @@ expression: receipts_gas_profile gas_used: 0, }, ], - [], [ CostGasUsed { cost_category: "WASM_HOST_COST", @@ -376,7 +373,6 @@ expression: receipts_gas_profile gas_used: 0, }, ], - [], [ CostGasUsed { cost_category: "WASM_HOST_COST", @@ -391,9 +387,6 @@ expression: receipts_gas_profile ], [], [], - [], - [], - [], [ CostGasUsed { cost_category: "WASM_HOST_COST", @@ -407,9 +400,6 @@ expression: receipts_gas_profile }, ], [], - [], - [], - [], [ CostGasUsed { cost_category: "WASM_HOST_COST", @@ -422,7 +412,6 @@ expression: receipts_gas_profile gas_used: 0, }, ], - [], [ CostGasUsed { cost_category: "WASM_HOST_COST", @@ -450,6 +439,4 @@ expression: receipts_gas_profile gas_used: 2865522486, }, ], - [], - [], ] diff --git a/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile_nondeterministic.snap b/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile_nondeterministic.snap index b9616f30a16..958a4ca8bef 100644 --- a/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile_nondeterministic.snap +++ b/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile_nondeterministic.snap @@ -20,5 +20,4 @@ expression: receipts_gas_profile gas_used: 1645512, }, ], - [], ] diff --git a/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_status.snap b/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_status.snap index 906eafd23d7..0b96ef9da43 100644 --- a/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_status.snap +++ b/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_status.snap @@ -1,7 +1,6 @@ --- source: integration-tests/src/tests/runtime/sanity_checks.rs expression: receipts_status - --- - SuccessReceiptId: "11111111111111111111111111111111" - Failure: @@ -10,7 +9,6 @@ expression: receipts_status kind: FunctionCallError: ExecutionError: "Smart contract panicked: explicit guest panic" -- SuccessValue: "" - Failure: ActionError: index: 0 @@ -26,15 +24,4 @@ expression: receipts_status - SuccessValue: "" - SuccessValue: "" - SuccessValue: "" -- SuccessValue: "" -- SuccessValue: "" -- SuccessValue: "" -- SuccessValue: "" -- SuccessValue: "" -- SuccessValue: "" -- SuccessValue: "" -- SuccessValue: "" -- SuccessValue: "" -- SuccessValue: "" -- SuccessValue: "" -- SuccessValue: "" + diff --git a/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_status_nondeterministic.snap b/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_status_nondeterministic.snap index b0d1e43f5f5..349ba5b68f8 100644 --- a/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_status_nondeterministic.snap +++ b/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_status_nondeterministic.snap @@ -1,8 +1,6 @@ --- source: integration-tests/src/tests/runtime/sanity_checks.rs expression: receipts_status - --- - SuccessValue: "" -- SuccessValue: "" diff --git a/integration-tests/src/tests/runtime/test_evil_contracts.rs b/integration-tests/src/tests/runtime/test_evil_contracts.rs index 2c9862a5153..f8e77e8878f 100644 --- a/integration-tests/src/tests/runtime/test_evil_contracts.rs +++ b/integration-tests/src/tests/runtime/test_evil_contracts.rs @@ -28,7 +28,7 @@ fn setup_test_contract(wasm_binary: &[u8]) -> RuntimeNode { ) .unwrap(); assert_eq!(transaction_result.status, FinalExecutionStatus::SuccessValue(Vec::new())); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let transaction_result = node_user.deploy_contract("test_contract".parse().unwrap(), wasm_binary.to_vec()).unwrap(); diff --git a/integration-tests/src/tests/standard_cases/mod.rs b/integration-tests/src/tests/standard_cases/mod.rs index 314751fb02e..3b4d05bd68e 100644 --- a/integration-tests/src/tests/standard_cases/mod.rs +++ b/integration-tests/src/tests/standard_cases/mod.rs @@ -67,7 +67,7 @@ pub fn test_smart_contract_simple(node: impl Node) { transaction_result.status, FinalExecutionStatus::SuccessValue(10i32.to_le_bytes().to_vec()) ); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let new_root = node_user.get_state_root(); assert_ne!(root, new_root); } @@ -96,7 +96,7 @@ pub fn test_smart_contract_panic(node: impl Node) { .into() ) ); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); } pub fn test_smart_contract_self_call(node: impl Node) { @@ -110,7 +110,7 @@ pub fn test_smart_contract_self_call(node: impl Node) { transaction_result.status, FinalExecutionStatus::SuccessValue(10i32.to_le_bytes().to_vec()) ); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let new_root = node_user.get_state_root(); assert_ne!(root, new_root); } @@ -134,7 +134,7 @@ pub fn test_smart_contract_bad_method_name(node: impl Node) { .into() ) ); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let new_root = node_user.get_state_root(); assert_ne!(root, new_root); } @@ -158,7 +158,7 @@ pub fn test_smart_contract_empty_method_name_with_no_tokens(node: impl Node) { .into() ) ); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let new_root = node_user.get_state_root(); assert_ne!(root, new_root); } @@ -182,7 +182,7 @@ pub fn test_smart_contract_empty_method_name_with_tokens(node: impl Node) { .into() ) ); - assert_eq!(transaction_result.receipts_outcome.len(), 3); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let new_root = node_user.get_state_root(); assert_ne!(root, new_root); } @@ -205,7 +205,7 @@ pub fn test_smart_contract_with_args(node: impl Node) { transaction_result.status, FinalExecutionStatus::SuccessValue(5u64.to_le_bytes().to_vec()) ); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let new_root = node_user.get_state_root(); assert_ne!(root, new_root); } @@ -218,7 +218,7 @@ pub fn test_async_call_with_logs(node: impl Node) { .function_call(account_id.clone(), bob_account(), "log_something", vec![], 10u64.pow(14), 0) .unwrap(); assert_eq!(transaction_result.status, FinalExecutionStatus::SuccessValue(Vec::new())); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let new_root = node_user.get_state_root(); assert_ne!(root, new_root); assert_eq!(transaction_result.receipts_outcome[0].outcome.logs[0], "hello".to_string()); @@ -261,7 +261,7 @@ pub fn test_upload_contract(node: impl Node) { ) .unwrap(); assert_eq!(transaction_result.status, FinalExecutionStatus::SuccessValue(Vec::new())); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); node_user.view_contract_code(&eve_dot_alice_account()).expect_err( "RpcError { code: -32000, message: \"Server error\", data: Some(String(\"contract code of account eve.alice.near does not exist while viewing\")) }"); @@ -307,7 +307,7 @@ pub fn test_send_money(node: impl Node) { let transaction_result = node_user.send_money(account_id.clone(), bob_account(), money_used).unwrap(); assert_eq!(transaction_result.status, FinalExecutionStatus::SuccessValue(Vec::new())); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let new_root = node_user.get_state_root(); assert_ne!(root, new_root); assert_eq!(node_user.get_access_key_nonce_for_signer(account_id).unwrap(), 1); @@ -340,7 +340,7 @@ pub fn transfer_tokens_implicit_account(node: impl Node) { let transaction_result = node_user.send_money(account_id.clone(), receiver_id.clone(), tokens_used).unwrap(); assert_eq!(transaction_result.status, FinalExecutionStatus::SuccessValue(Vec::new())); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let new_root = node_user.get_state_root(); assert_ne!(root, new_root); assert_eq!(node_user.get_access_key_nonce_for_signer(account_id).unwrap(), 1); @@ -364,7 +364,7 @@ pub fn transfer_tokens_implicit_account(node: impl Node) { node_user.send_money(account_id.clone(), receiver_id.clone(), tokens_used).unwrap(); assert_eq!(transaction_result.status, FinalExecutionStatus::SuccessValue(Vec::new())); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let new_root = node_user.get_state_root(); assert_ne!(root, new_root); assert_eq!(node_user.get_access_key_nonce_for_signer(account_id).unwrap(), 2); @@ -423,7 +423,7 @@ pub fn trying_to_create_implicit_account(node: impl Node) { .into() ) ); - assert_eq!(transaction_result.receipts_outcome.len(), 3); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let new_root = node_user.get_state_root(); assert_ne!(root, new_root); assert_eq!(node_user.get_access_key_nonce_for_signer(account_id).unwrap(), 1); @@ -450,7 +450,7 @@ pub fn test_smart_contract_reward(node: impl Node) { transaction_result.status, FinalExecutionStatus::SuccessValue(10i32.to_le_bytes().to_vec()) ); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let new_root = node_user.get_state_root(); assert_ne!(root, new_root); @@ -504,7 +504,7 @@ pub fn test_refund_on_send_money_to_non_existent_account(node: impl Node) { .into() ) ); - assert_eq!(transaction_result.receipts_outcome.len(), 3); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let new_root = node_user.get_state_root(); assert_ne!(root, new_root); let result1 = node_user.view_account(account_id).unwrap(); @@ -535,7 +535,7 @@ pub fn test_create_account(node: impl Node) { let create_account_cost = fee_helper.create_account_transfer_full_key_cost(); assert_eq!(transaction_result.status, FinalExecutionStatus::SuccessValue(Vec::new())); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let new_root = node_user.get_state_root(); assert_ne!(root, new_root); assert_eq!(node_user.get_access_key_nonce_for_signer(account_id).unwrap(), 1); @@ -568,7 +568,7 @@ pub fn test_create_account_again(node: impl Node) { .unwrap(); assert_eq!(transaction_result.status, FinalExecutionStatus::SuccessValue(Vec::new())); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let fee_helper = fee_helper(&node); let create_account_cost = fee_helper.create_account_transfer_full_key_cost(); @@ -600,7 +600,7 @@ pub fn test_create_account_again(node: impl Node) { .into() ) ); - assert_eq!(transaction_result.receipts_outcome.len(), 3); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let new_root = node_user.get_state_root(); assert_ne!(root, new_root); assert_eq!(node_user.get_access_key_nonce_for_signer(account_id).unwrap(), 2); @@ -655,7 +655,7 @@ pub fn test_create_account_failure_already_exists(node: impl Node) { .into() ) ); - assert_eq!(transaction_result.receipts_outcome.len(), 3); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let new_root = node_user.get_state_root(); assert_ne!(root, new_root); assert_eq!(node_user.get_access_key_nonce_for_signer(account_id).unwrap(), 1); @@ -998,7 +998,7 @@ pub fn test_access_key_smart_contract(node: impl Node) { prepaid_gas + exec_gas - transaction_result.receipts_outcome[0].outcome.gas_burnt, ); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let new_root = node_user.get_state_root(); assert_ne!(root, new_root); @@ -1158,7 +1158,7 @@ pub fn test_unstake_while_not_staked(node: impl Node) { ) .unwrap(); assert_eq!(transaction_result.status, FinalExecutionStatus::SuccessValue(Vec::new())); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let transaction_result = node_user.stake(eve_dot_alice_account(), node.block_signer().public_key(), 0).unwrap(); assert_eq!( @@ -1256,7 +1256,7 @@ pub fn test_delete_account_fail(node: impl Node) { .into() ) ); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); assert!(node.user().view_account(&bob_account()).is_ok()); assert_eq!( node.user().view_account(&node.account_id().unwrap()).unwrap().amount, @@ -1278,7 +1278,7 @@ pub fn test_delete_account_no_account(node: impl Node) { .into() ) ); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); } pub fn test_delete_account_while_staking(node: impl Node) { @@ -1381,7 +1381,7 @@ pub fn test_contract_write_key_value_cost(node: impl Node) { ) .unwrap(); assert_matches!(transaction_result.status, FinalExecutionStatus::SuccessValue(_)); - assert_eq!(transaction_result.receipts_outcome.len(), 2); + assert_eq!(transaction_result.receipts_outcome.len(), 1); let trie_nodes_count = get_trie_nodes_count( &transaction_result.receipts_outcome[0].outcome.metadata, diff --git a/integration-tests/src/user/runtime_user.rs b/integration-tests/src/user/runtime_user.rs index 9d95be1a3ca..26019272429 100644 --- a/integration-tests/src/user/runtime_user.rs +++ b/integration-tests/src/user/runtime_user.rs @@ -170,6 +170,12 @@ impl RuntimeUser { block_hash: Default::default(), }]; for hash in &receipt_ids { + if let Some(receipt) = self.receipts.borrow().get(hash) { + let is_refund = receipt.predecessor_id.is_system(); + if is_refund { + continue; + } + } transactions.extend(self.get_recursive_transaction_results(hash).into_iter()); } transactions From a56420c594f2230932c84ec856ea528b0812abf0 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Thu, 5 Jan 2023 19:14:49 +0100 Subject: [PATCH 143/188] o11y: avoid temporary String allocations when formatting log messages (#8235) * o11y: avoid temporary String allocations when formatting log messages * review * undo kv change * epoch_id --- chain/chain/src/blocks_delay_tracker.rs | 83 ------------ chain/client/src/client_actor.rs | 74 +--------- chain/client/src/info.rs | 173 +++++++++++++++++++++++- core/o11y/src/io_tracer.rs | 26 ++-- core/primitives/src/utils.rs | 4 +- 5 files changed, 187 insertions(+), 173 deletions(-) diff --git a/chain/chain/src/blocks_delay_tracker.rs b/chain/chain/src/blocks_delay_tracker.rs index 6c0827ecada..99989788166 100644 --- a/chain/chain/src/blocks_delay_tracker.rs +++ b/chain/chain/src/blocks_delay_tracker.rs @@ -1,6 +1,4 @@ use chrono::DateTime; -use near_chain_configs::LogSummaryStyle; -use near_chain_primitives::Error; use near_primitives::block::{Block, Tip}; use near_primitives::borsh::maybestd::collections::hash_map::Entry; use near_primitives::hash::CryptoHash; @@ -482,85 +480,4 @@ impl Chain { floating_chunks_info, } } - - pub fn print_chain_processing_info_to_string( - &self, - log_summary_style: LogSummaryStyle, - ) -> Result { - let chain_info = self.get_chain_processing_info(); - let blocks_info = chain_info.blocks_info; - let use_colour = matches!(log_summary_style, LogSummaryStyle::Colored); - let paint = |colour: ansi_term::Colour, text: Option| match text { - None => ansi_term::Style::default().paint(""), - Some(text) if use_colour => colour.bold().paint(text), - Some(text) => ansi_term::Style::default().paint(text), - }; - - // Returns a status line for each block - also prints what is happening to its chunks. - let next_blocks_log = blocks_info - .into_iter() - .flat_map(|block_info| { - let mut all_chunks_received = true; - let chunk_status_str = block_info - .chunks_info - .iter() - .map(|chunk_info| { - if let Some(chunk_info) = chunk_info { - all_chunks_received &= - matches!(chunk_info.status, ChunkProcessingStatus::Completed); - match chunk_info.status { - ChunkProcessingStatus::Completed => "✔", - ChunkProcessingStatus::Requested => "⬇", - ChunkProcessingStatus::NeedToRequest => ".", - } - } else { - "X" - } - }) - .collect::>() - .join(""); - - let chunk_status_color = if all_chunks_received { - ansi_term::Colour::Green - } else { - ansi_term::Colour::White - }; - - let chunk_status_str = paint(chunk_status_color, Some(chunk_status_str)); - - let in_progress_str = format!("in progress for {:?}ms", block_info.in_progress_ms); - - let in_orphan_str = match block_info.orphaned_ms { - Some(duration) => format!("orphan for {:?}ms", duration), - None => "".to_string(), - }; - - let missing_chunks_str = match block_info.missing_chunks_ms { - Some(duration) => format!("missing chunks for {:?}ms", duration), - None => "".to_string(), - }; - - Some(format!( - "{} {} {:?} {} {} {} Chunks:({}))", - block_info.height, - block_info.hash, - block_info.block_status, - in_progress_str, - in_orphan_str, - missing_chunks_str, - chunk_status_str, - )) - }) - .collect::>(); - - Ok(format!( - "{:?} Orphans: {} With missing chunks: {} In processing {}{}{}", - self.head()?.epoch_id, - self.orphans_len(), - self.blocks_with_missing_chunks_len(), - self.blocks_in_processing_len(), - if next_blocks_log.len() > 0 { "\n" } else { "" }, - next_blocks_log.join("\n") - )) - } } diff --git a/chain/client/src/client_actor.rs b/chain/client/src/client_actor.rs index e33ed0de315..fc8f5b4fefc 100644 --- a/chain/client/src/client_actor.rs +++ b/chain/client/src/client_actor.rs @@ -12,9 +12,7 @@ use crate::adapter::{ }; use crate::client::{Client, EPOCH_START_INFO_BLOCKS}; use crate::debug::new_network_info_view; -use crate::info::{ - display_sync_status, get_validator_epoch_stats, InfoHelper, ValidatorInfoHelper, -}; +use crate::info::{display_sync_status, InfoHelper}; use crate::metrics::PARTIAL_ENCODED_CHUNK_RESPONSE_DELAY; use crate::sync::state::{StateSync, StateSyncResult}; use crate::{metrics, StatusResponse}; @@ -57,7 +55,7 @@ use near_primitives::network::{AnnounceAccount, PeerId}; use near_primitives::state_part::PartId; use near_primitives::syncing::StatePartKey; use near_primitives::time::{Clock, Utc}; -use near_primitives::types::{BlockHeight, ValidatorInfoIdentifier}; +use near_primitives::types::BlockHeight; use near_primitives::unwrap_or_return; use near_primitives::utils::{from_timestamp, MaybeValidated}; use near_primitives::validator_signer::ValidatorSigner; @@ -1718,73 +1716,7 @@ impl ClientActor { fn log_summary(&mut self) { let _span = tracing::debug_span!(target: "client", "log_summary").entered(); let _d = delay_detector::DelayDetector::new(|| "client log summary".into()); - let is_syncing = self.client.sync_status.is_syncing(); - let head = unwrap_or_return!(self.client.chain.head()); - let validator_info = if !is_syncing { - let validators = unwrap_or_return!(self - .client - .runtime_adapter - .get_epoch_block_producers_ordered(&head.epoch_id, &head.last_block_hash)); - let num_validators = validators.len(); - let account_id = self.client.validator_signer.as_ref().map(|x| x.validator_id()); - let is_validator = if let Some(account_id) = account_id { - match self.client.runtime_adapter.get_validator_by_account_id( - &head.epoch_id, - &head.last_block_hash, - account_id, - ) { - Ok((_, is_slashed)) => !is_slashed, - Err(_) => false, - } - } else { - false - }; - Some(ValidatorInfoHelper { is_validator, num_validators }) - } else { - None - }; - - let header_head = unwrap_or_return!(self.client.chain.header_head()); - let validator_epoch_stats = if is_syncing { - // EpochManager::get_validator_info method (which is what runtime - // adapter calls) is expensive when node is syncing so we’re simply - // not collecting the statistics. The statistics are used to update - // a few Prometheus metrics only so we prefer to leave the metrics - // unset until node finishes synchronising. TODO(#6763): If we - // manage to get get_validator_info fasts again (or return an error - // if computation would be too slow), remove the ‘if is_syncing’ - // check. - Default::default() - } else { - let epoch_identifier = ValidatorInfoIdentifier::BlockHash(header_head.last_block_hash); - self.client - .runtime_adapter - .get_validator_info(epoch_identifier) - .map(get_validator_epoch_stats) - .unwrap_or_default() - }; - let statistics = if self.client.config.enable_statistics_export { - self.client.chain.store().get_store_statistics() - } else { - None - }; - self.info_helper.info( - &head, - &self.client.sync_status, - self.client.get_catchup_status().unwrap_or_default(), - &self.node_id, - &self.network_info, - validator_info, - validator_epoch_stats, - self.client - .runtime_adapter - .get_estimated_protocol_upgrade_block_height(head.last_block_hash) - .unwrap_or(None) - .unwrap_or(0), - statistics, - &self.client.config, - ); - debug!(target: "stats", "{}", self.client.chain.print_chain_processing_info_to_string(self.client.config.log_summary_style).unwrap_or(String::from("Upcoming block info failed."))); + self.info_helper.log_summary(&self.client, &self.node_id, &self.network_info) } } diff --git a/chain/client/src/info.rs b/chain/client/src/info.rs index 53ba1237ec7..d53d5bdfc8a 100644 --- a/chain/client/src/info.rs +++ b/chain/client/src/info.rs @@ -10,12 +10,15 @@ use near_primitives::telemetry::{ }; use near_primitives::time::{Clock, Instant}; use near_primitives::types::{ - AccountId, Balance, BlockHeight, EpochHeight, Gas, NumBlocks, ShardId, + AccountId, Balance, BlockHeight, EpochHeight, EpochId, Gas, NumBlocks, ShardId, + ValidatorInfoIdentifier, }; +use near_primitives::unwrap_or_return; use near_primitives::validator_signer::ValidatorSigner; use near_primitives::version::Version; use near_primitives::views::{ - CatchupStatusView, CurrentEpochValidatorInfo, EpochValidatorInfo, ValidatorKickoutView, + CatchupStatusView, ChunkProcessingStatus, CurrentEpochValidatorInfo, EpochValidatorInfo, + ValidatorKickoutView, }; use near_store::db::StoreStatistics; use near_telemetry::{telemetry, TelemetryActor}; @@ -27,7 +30,7 @@ use tracing::info; const TERAGAS: f64 = 1_000_000_000_000_f64; -pub struct ValidatorInfoHelper { +struct ValidatorInfoHelper { pub is_validator: bool, pub num_validators: usize, } @@ -116,7 +119,82 @@ impl InfoHelper { metrics::EPOCH_HEIGHT.set(epoch_height as i64); } - pub fn info( + /// Print current summary. + pub fn log_summary( + &mut self, + client: &crate::client::Client, + node_id: &PeerId, + network_info: &NetworkInfo, + ) { + let is_syncing = client.sync_status.is_syncing(); + let head = unwrap_or_return!(client.chain.head()); + let validator_info = if !is_syncing { + let validators = unwrap_or_return!(client + .runtime_adapter + .get_epoch_block_producers_ordered(&head.epoch_id, &head.last_block_hash)); + let num_validators = validators.len(); + let account_id = client.validator_signer.as_ref().map(|x| x.validator_id()); + let is_validator = if let Some(account_id) = account_id { + match client.runtime_adapter.get_validator_by_account_id( + &head.epoch_id, + &head.last_block_hash, + account_id, + ) { + Ok((_, is_slashed)) => !is_slashed, + Err(_) => false, + } + } else { + false + }; + Some(ValidatorInfoHelper { is_validator, num_validators }) + } else { + None + }; + + let header_head = unwrap_or_return!(client.chain.header_head()); + let validator_epoch_stats = if is_syncing { + // EpochManager::get_validator_info method (which is what runtime + // adapter calls) is expensive when node is syncing so we’re simply + // not collecting the statistics. The statistics are used to update + // a few Prometheus metrics only so we prefer to leave the metrics + // unset until node finishes synchronising. TODO(#6763): If we + // manage to get get_validator_info fasts again (or return an error + // if computation would be too slow), remove the ‘if is_syncing’ + // check. + Default::default() + } else { + let epoch_identifier = ValidatorInfoIdentifier::BlockHash(header_head.last_block_hash); + client + .runtime_adapter + .get_validator_info(epoch_identifier) + .map(get_validator_epoch_stats) + .unwrap_or_default() + }; + let statistics = if client.config.enable_statistics_export { + client.chain.store().get_store_statistics() + } else { + None + }; + self.info( + &head, + &client.sync_status, + client.get_catchup_status().unwrap_or_default(), + node_id, + network_info, + validator_info, + validator_epoch_stats, + client + .runtime_adapter + .get_estimated_protocol_upgrade_block_height(head.last_block_hash) + .unwrap_or(None) + .unwrap_or(0), + statistics, + &client.config, + ); + self.log_chain_processing_info(client, &head.epoch_id); + } + + fn info( &mut self, head: &Tip, sync_status: &SyncStatus, @@ -302,6 +380,22 @@ impl InfoHelper { serde_json::to_value(&info).expect("Telemetry must serialize to json") } } + + fn log_chain_processing_info(&mut self, client: &crate::Client, epoch_id: &EpochId) { + let chain = &client.chain; + let use_colour = matches!(self.log_summary_style, LogSummaryStyle::Colored); + let info = chain.get_chain_processing_info(); + let blocks_info = BlocksInfo { blocks_info: info.blocks_info, use_colour }; + tracing::debug!( + target: "stats", + "{:?} Orphans: {} With missing chunks: {} In processing {}{}", + epoch_id, + info.num_orphans, + info.num_blocks_missing_chunks, + info.num_blocks_in_processing, + blocks_info, + ); + } } fn extra_telemetry_info(client_config: &ClientConfig) -> serde_json::Value { @@ -394,6 +488,75 @@ pub fn display_sync_status(sync_status: &SyncStatus, head: &Tip) -> String { } } +/// Displays ` {} for {}ms` if second item is `Some`. +struct FormatMillis(&'static str, Option); + +impl std::fmt::Display for FormatMillis { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.1.map_or(Ok(()), |ms| write!(fmt, " {} for {ms}ms", self.0)) + } +} + +/// Formats information about each block. Each information line is *preceded* +/// by a new line character. There’s no final new line character. This is +/// meant to be used in logging where final new line is not desired. +struct BlocksInfo { + blocks_info: Vec, + use_colour: bool, +} + +impl std::fmt::Display for BlocksInfo { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let paint = |colour: ansi_term::Colour, text: String| { + if self.use_colour { + colour.bold().paint(text) + } else { + ansi_term::Style::default().paint(text) + } + }; + + for block_info in self.blocks_info.iter() { + let mut all_chunks_received = true; + let chunk_status = block_info + .chunks_info + .iter() + .map(|chunk_info| { + if let Some(chunk_info) = chunk_info { + all_chunks_received &= + matches!(chunk_info.status, ChunkProcessingStatus::Completed); + match chunk_info.status { + ChunkProcessingStatus::Completed => '✔', + ChunkProcessingStatus::Requested => '⬇', + ChunkProcessingStatus::NeedToRequest => '.', + } + } else { + 'X' + } + }) + .collect::(); + + let chunk_status_color = if all_chunks_received { + ansi_term::Colour::Green + } else { + ansi_term::Colour::White + }; + + let chunk_status = paint(chunk_status_color, chunk_status); + let in_progress = FormatMillis("in progress", Some(block_info.in_progress_ms)); + let in_orphan = FormatMillis("orphan", block_info.orphaned_ms); + let missing_chunks = FormatMillis("missing chunks", block_info.missing_chunks_ms); + + write!( + fmt, + "\n {} {} {:?}{in_progress}{in_orphan}{missing_chunks} Chunks:({chunk_status}))", + block_info.height, block_info.hash, block_info.block_status, + )?; + } + + Ok(()) + } +} + /// Format number using SI prefixes. struct PrettyNumber(u64, &'static str); @@ -472,7 +635,7 @@ impl ValidatorProductionStats { } /// Converts EpochValidatorInfo into a vector of ValidatorProductionStats. -pub fn get_validator_epoch_stats( +fn get_validator_epoch_stats( current_validator_epoch_info: EpochValidatorInfo, ) -> Vec { let mut stats = vec![]; diff --git a/core/o11y/src/io_tracer.rs b/core/o11y/src/io_tracer.rs index c0c898fca4f..bf27b86789d 100644 --- a/core/o11y/src/io_tracer.rs +++ b/core/o11y/src/io_tracer.rs @@ -187,18 +187,24 @@ impl IoTraceLayer { event: &tracing::Event, ctx: tracing_subscriber::layer::Context, ) { + /// `Display`s ` size=` if wrapped value is Some; nothing + /// otherwise. + struct FormattedSize(Option); + + impl std::fmt::Display for FormattedSize { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.map_or(Ok(()), |size| write!(fmt, " size={size}")) + } + } + let mut visitor = IoEventVisitor::default(); event.record(&mut visitor); match visitor.t { Some(IoEventType::DbOp(db_op)) => { let col = visitor.col.as_deref().unwrap_or("?"); let key = visitor.key.as_deref().unwrap_or("?"); - let formatted_size = if let Some(size) = visitor.size { - format!(" size={size}") - } else { - String::new() - }; - let output_line = format!("{db_op} {col} {key:?}{formatted_size}"); + let size = FormattedSize(visitor.size); + let output_line = format!("{db_op} {col} {key:?}{size}"); if let Some(span) = ctx.event_span(event) { span.extensions_mut() .get_mut::() @@ -212,16 +218,12 @@ impl IoTraceLayer { } Some(IoEventType::StorageOp(storage_op)) => { let key = visitor.key.as_deref().unwrap_or("?"); - let formatted_size = if let Some(size) = visitor.size { - format!(" size={size}") - } else { - String::new() - }; + let size = FormattedSize(visitor.size); let tn_db_reads = visitor.tn_db_reads.unwrap(); let tn_mem_reads = visitor.tn_mem_reads.unwrap(); let span_info = - format!("{storage_op} key={key}{formatted_size} tn_db_reads={tn_db_reads} tn_mem_reads={tn_mem_reads}"); + format!("{storage_op} key={key}{size} tn_db_reads={tn_db_reads} tn_mem_reads={tn_mem_reads}"); let span = ctx.event_span(event).expect("storage operations must happen inside span"); diff --git a/core/primitives/src/utils.rs b/core/primitives/src/utils.rs index 4f90698eb6b..20d930f1c84 100644 --- a/core/primitives/src/utils.rs +++ b/core/primitives/src/utils.rs @@ -391,7 +391,7 @@ macro_rules! unwrap_or_return { match $obj { Ok(value) => value, Err(err) => { - error!(target: "client", "Unwrap error: {}", err); + tracing::error!(target: "client", "Unwrap error: {}", err); return $ret; } } @@ -400,7 +400,7 @@ macro_rules! unwrap_or_return { match $obj { Ok(value) => value, Err(err) => { - error!(target: "client", "Unwrap error: {}", err); + tracing::error!(target: "client", "Unwrap error: {}", err); return; } } From 1d3e7632e84edb51313663046bc502bae88acfbe Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Fri, 6 Jan 2023 14:58:46 +0100 Subject: [PATCH 144/188] chore: upgrade rust-version to 1.66.0 (#8284) https://blog.rust-lang.org/2022/12/15/Rust-1.66.0.html Updated the same files as in the previous version bump: https://github.com/near/nearcore/pull/7993 This enables https://github.com/near/nearcore/issues/7650 (see [this comment](https://github.com/near/nearcore/issues/7650#issuecomment-1337180030)). Also includes fixes for the following warning: ``` warning: for loop over an `Option`. This is more readably written as an `if let` statement ... = note: `#[warn(for_loops_over_fallibles)]` on by default ``` --- Cargo.toml | 2 +- chain/chain/src/missing_chunks.rs | 2 +- chain/chunks/src/lib.rs | 2 +- chain/jsonrpc/jsonrpc-tests/tests/rpc_transactions.rs | 2 +- runtime/runtime-params-estimator/emu-cost/Dockerfile | 2 +- rust-toolchain.toml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 181f79e1f2b..b47e1b272f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -240,4 +240,4 @@ opt-level = 3 [workspace.package] edition = "2021" authors = ["Near Inc "] -rust-version = "1.65.0" +rust-version = "1.66.0" diff --git a/chain/chain/src/missing_chunks.rs b/chain/chain/src/missing_chunks.rs index 9ae37efe4f3..d0cf1c958ec 100644 --- a/chain/chain/src/missing_chunks.rs +++ b/chain/chain/src/missing_chunks.rs @@ -149,7 +149,7 @@ impl MissingChunksPool { } fn mark_block_as_ready(&mut self, block_hash: &BlockHash) { - for block in self.blocks_waiting_for_chunks.remove(block_hash) { + if let Some(block) = self.blocks_waiting_for_chunks.remove(block_hash) { let height = block.height(); if let btree_map::Entry::Occupied(mut entry) = self.height_idx.entry(height) { let blocks_at_height = entry.get_mut(); diff --git a/chain/chunks/src/lib.rs b/chain/chunks/src/lib.rs index 686b025cf79..8ccacef1d48 100644 --- a/chain/chunks/src/lib.rs +++ b/chain/chunks/src/lib.rs @@ -1900,7 +1900,7 @@ impl ShardsManager { /// complete when a new block is accepted. pub fn check_incomplete_chunks(&mut self, prev_block_hash: &CryptoHash) { let mut chunks_to_process = vec![]; - for chunk_hashes in self.encoded_chunks.get_incomplete_chunks(prev_block_hash) { + if let Some(chunk_hashes) = self.encoded_chunks.get_incomplete_chunks(prev_block_hash) { for chunk_hash in chunk_hashes { if let Some(entry) = self.encoded_chunks.get(chunk_hash) { chunks_to_process.push(entry.header.clone()); diff --git a/chain/jsonrpc/jsonrpc-tests/tests/rpc_transactions.rs b/chain/jsonrpc/jsonrpc-tests/tests/rpc_transactions.rs index b6f65b7c39e..8ac13d25ca1 100644 --- a/chain/jsonrpc/jsonrpc-tests/tests/rpc_transactions.rs +++ b/chain/jsonrpc/jsonrpc-tests/tests/rpc_transactions.rs @@ -128,7 +128,7 @@ fn test_refunds_not_in_receipts() { } } } - for receipt in tx_status.get("receipts") { + if let Some(receipt) = tx_status.get("receipts") { if !receipt.as_array().unwrap().is_empty() { let receipt_predecessor_id = receipt.get("predecessor_id"); if receipt_predecessor_id.is_some() { diff --git a/runtime/runtime-params-estimator/emu-cost/Dockerfile b/runtime/runtime-params-estimator/emu-cost/Dockerfile index 0b556640d1f..7a1ec13a921 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.65.0 +FROM rust:1.66.0 LABEL description="Container for builds" diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 7e89e6b037a..46ce70e1cfa 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.65.0" +channel = "1.66.0" components = [ "rustfmt" ] targets = [ "wasm32-unknown-unknown" ] From 28b21914a7d07a509e674029e47e84a6ecd22df1 Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Fri, 6 Jan 2023 16:48:29 +0100 Subject: [PATCH 145/188] refactor: fix clippy issues for near-o11y (#8291) Fix the issues reported by `cargo clippy -p near-o11y` as part of #8145 --- core/o11y/benches/metrics.rs | 10 +++++----- core/o11y/src/lib.rs | 8 +++----- core/o11y/src/pretty.rs | 12 +++++------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/core/o11y/benches/metrics.rs b/core/o11y/benches/metrics.rs index 32c52d206d0..b5dd6f10b57 100644 --- a/core/o11y/benches/metrics.rs +++ b/core/o11y/benches/metrics.rs @@ -24,7 +24,7 @@ fn inc_counter_vec_with_label_values_itoa(bench: &mut Bencher) { for shard_id in 0..NUM_SHARDS { let mut buffer = itoa::Buffer::new(); let printed = buffer.format(shard_id); - COUNTERS.with_label_values(&[&printed]).inc(); + COUNTERS.with_label_values(&[printed]).inc(); } }); } @@ -80,8 +80,8 @@ fn inc_counter_vec_cached(bench: &mut Bencher) { .map(|shard_id| COUNTERS.with_label_values(&[&shard_id.to_string()])) .collect(); bench.iter(|| { - for shard_id in 0..NUM_SHARDS { - counters[shard_id].inc(); + for counter in &counters { + counter.inc(); } }); } @@ -90,8 +90,8 @@ fn inc_counter_vec_cached_str(bench: &mut Bencher) { const NUM_SHARDS: usize = 8; let shard_ids: Vec = (0..NUM_SHARDS).map(|shard_id| shard_id.to_string()).collect(); bench.iter(|| { - for shard_id in 0..NUM_SHARDS { - COUNTERS.with_label_values(&[&shard_ids[shard_id]]).inc(); + for shard_id in &shard_ids { + COUNTERS.with_label_values(&[shard_id]).inc(); } }); } diff --git a/core/o11y/src/lib.rs b/core/o11y/src/lib.rs index f432c825382..7674237de0d 100644 --- a/core/o11y/src/lib.rs +++ b/core/o11y/src/lib.rs @@ -79,7 +79,7 @@ type TracingLayer = Layered< static DEFAULT_OTLP_LEVEL: OnceCell = OnceCell::new(); /// The default value for the `RUST_LOG` environment variable if one isn't specified otherwise. -pub const DEFAULT_RUST_LOG: &'static str = "tokio_reactor=info,\ +pub const DEFAULT_RUST_LOG: &str = "tokio_reactor=info,\ near=info,\ recompress=info,\ stats=info,\ @@ -457,10 +457,8 @@ pub fn reload( let log_reload_result = LOG_LAYER_RELOAD_HANDLE.get().map_or( Err(ReloadError::NoLogReloadHandle), |reload_handle| { - let mut builder = rust_log.map_or_else( - || EnvFilterBuilder::from_env(), - |rust_log| EnvFilterBuilder::new(rust_log), - ); + let mut builder = + rust_log.map_or_else(EnvFilterBuilder::from_env, EnvFilterBuilder::new); if let Some(module) = verbose_module { builder = builder.verbose(Some(module)); } diff --git a/core/o11y/src/pretty.rs b/core/o11y/src/pretty.rs index 697e6eeb138..b855cd742e2 100644 --- a/core/o11y/src/pretty.rs +++ b/core/o11y/src/pretty.rs @@ -171,14 +171,12 @@ fn truncated_bytes_format(bytes: &[u8], fmt: &mut std::fmt::Formatter<'_>) -> st let value = unsafe { std::str::from_utf8_unchecked(bytes) }; write!(fmt, "({len})'{value}'…") } + } else if bytes.len() <= LIMIT / 4 * 3 { + std::fmt::Display::fmt(&base64_display(bytes), fmt) } else { - if bytes.len() <= LIMIT / 4 * 3 { - std::fmt::Display::fmt(&base64_display(bytes), fmt) - } else { - let bytes = &bytes[..(LIMIT - 8) / 4 * 3]; - let value = base64_display(bytes); - write!(fmt, "({len}){value}…") - } + let bytes = &bytes[..(LIMIT - 8) / 4 * 3]; + let value = base64_display(bytes); + write!(fmt, "({len}){value}…") } } From 97c5bca720d06ddc0f5c33a9e7e7ab5bcbb68fb3 Mon Sep 17 00:00:00 2001 From: nikurt <86772482+nikurt@users.noreply.github.com> Date: Sat, 7 Jan 2023 09:43:36 +0100 Subject: [PATCH 146/188] refactor: Sort state-viewer commands and add missing documentation (#8294) No code changes. --- tools/state-viewer/src/cli.rs | 512 +++++++++++------------ tools/state-viewer/src/commands.rs | 636 ++++++++++++++--------------- 2 files changed, 578 insertions(+), 570 deletions(-) diff --git a/tools/state-viewer/src/cli.rs b/tools/state-viewer/src/cli.rs index 6a12f2278f4..b3b0d428bdc 100644 --- a/tools/state-viewer/src/cli.rs +++ b/tools/state-viewer/src/cli.rs @@ -16,63 +16,69 @@ use std::str::FromStr; #[derive(Subcommand)] #[clap(subcommand_required = true, arg_required_else_help = true)] pub enum StateViewerSubCommand { - Peers, - State, - /// Generate a genesis file from the current state of the DB. - #[clap(alias = "dump_state")] - DumpState(DumpStateCmd), - #[clap(alias = "dump_state_redis")] - DumpStateRedis(DumpStateRedisCmd), - /// Generate a file that contains all transactions from a block. - #[clap(alias = "dump_tx")] - DumpTx(DumpTxCmd), - /// Print chain from start_index to end_index. - Chain(ChainCmd), - /// Replay headers from chain. - Replay(ReplayCmd), + /// Apply block at some height for shard. + Apply(ApplyCmd), + /// Apply a chunk, even if it's not included in any block on disk + #[clap(alias = "apply_chunk")] + ApplyChunk(ApplyChunkCmd), /// Apply blocks at a range of heights for a single shard. #[clap(alias = "apply_range")] ApplyRange(ApplyRangeCmd), - /// Apply block at some height for shard. - Apply(ApplyCmd), - /// View head of the storage. - #[clap(alias = "view_chain")] - ViewChain(ViewChainCmd), + /// Apply a receipt if it occurs in some chunk we know about, + /// even if it's not included in any block on disk + #[clap(alias = "apply_receipt")] + ApplyReceipt(ApplyReceiptCmd), + /// Apply a transaction if it occurs in some chunk we know about, + /// even if it's not included in any block on disk + #[clap(alias = "apply_tx")] + ApplyTx(ApplyTxCmd), + /// Print chain from start_index to end_index. + Chain(ChainCmd), /// Check whether the node has all the blocks up to its head. #[clap(alias = "check_block")] CheckBlock, - /// Dump deployed contract code of given account to wasm file. - #[clap(alias = "dump_code")] - DumpCode(DumpCodeCmd), + /// Looks up a certain chunk. + Chunks(ChunksCmd), /// Dump contract data in storage of given account to binary file. #[clap(alias = "dump_account_storage")] DumpAccountStorage(DumpAccountStorageCmd), + /// Dump deployed contract code of given account to wasm file. + #[clap(alias = "dump_code")] + DumpCode(DumpCodeCmd), + /// Generate a genesis file from the current state of the DB. + #[clap(alias = "dump_state")] + DumpState(DumpStateCmd), + /// Dump all or a single state part of a shard. + DumpStateParts(DumpStatePartsCmd), + /// Writes state to a remote redis server. + #[clap(alias = "dump_state_redis")] + DumpStateRedis(DumpStateRedisCmd), + /// Generate a file that contains all transactions from a block. + #[clap(alias = "dump_tx")] + DumpTx(DumpTxCmd), /// Print `EpochInfo` of an epoch given by `--epoch_id` or by `--epoch_height`. #[clap(alias = "epoch_info")] EpochInfo(EpochInfoCmd), + /// Looks up a certain partial chunk. + #[clap(alias = "partial_chunks")] + PartialChunks(PartialChunksCmd), + /// Prints stored peers information from the DB. + Peers, + /// Looks up a certain receipt. + Receipts(ReceiptsCmd), + /// Replay headers from chain. + Replay(ReplayCmd), /// Dump stats for the RocksDB storage. #[clap(name = "rocksdb-stats", alias = "rocksdb_stats")] RocksDBStats(RocksDBStatsCmd), - Receipts(ReceiptsCmd), - Chunks(ChunksCmd), - #[clap(alias = "partial_chunks")] - PartialChunks(PartialChunksCmd), - /// Apply a chunk, even if it's not included in any block on disk - #[clap(alias = "apply_chunk")] - ApplyChunk(ApplyChunkCmd), - /// Apply a transaction if it occurs in some chunk we know about, - /// even if it's not included in any block on disk - #[clap(alias = "apply_tx")] - ApplyTx(ApplyTxCmd), - /// Apply a receipt if it occurs in some chunk we know about, - /// even if it's not included in any block on disk - #[clap(alias = "apply_receipt")] - ApplyReceipt(ApplyReceiptCmd), + /// Iterates over a trie and prints the StateRecords. + State, + /// View head of the storage. + #[clap(alias = "view_chain")] + ViewChain(ViewChainCmd), /// View trie structure. #[clap(alias = "view_trie")] ViewTrie(ViewTrieCmd), - /// Dump all or a single state part of a shard. - DumpStateParts(DumpStatePartsCmd), } impl StateViewerSubCommand { @@ -84,119 +90,120 @@ impl StateViewerSubCommand { let store = store_opener.open_in_mode(mode).unwrap(); let hot = store.get_store(near_store::Temperature::Hot); match self { - StateViewerSubCommand::Peers => peers(store), - StateViewerSubCommand::State => state(home_dir, near_config, hot), + StateViewerSubCommand::Apply(cmd) => cmd.run(home_dir, near_config, hot), + StateViewerSubCommand::ApplyChunk(cmd) => cmd.run(home_dir, near_config, hot), + StateViewerSubCommand::ApplyRange(cmd) => cmd.run(home_dir, near_config, hot), + StateViewerSubCommand::ApplyReceipt(cmd) => cmd.run(home_dir, near_config, hot), + StateViewerSubCommand::ApplyTx(cmd) => cmd.run(home_dir, near_config, hot), + StateViewerSubCommand::Chain(cmd) => cmd.run(home_dir, near_config, hot), + StateViewerSubCommand::CheckBlock => check_block_chunk_existence(near_config, hot), + StateViewerSubCommand::Chunks(cmd) => cmd.run(near_config, hot), + StateViewerSubCommand::DumpAccountStorage(cmd) => cmd.run(home_dir, near_config, hot), + StateViewerSubCommand::DumpCode(cmd) => cmd.run(home_dir, near_config, hot), StateViewerSubCommand::DumpState(cmd) => cmd.run(home_dir, near_config, hot), StateViewerSubCommand::DumpStateParts(cmd) => cmd.run(home_dir, near_config, hot), StateViewerSubCommand::DumpStateRedis(cmd) => cmd.run(home_dir, near_config, hot), StateViewerSubCommand::DumpTx(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::Chain(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::Replay(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::ApplyRange(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::Apply(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::ViewChain(cmd) => cmd.run(near_config, hot), - StateViewerSubCommand::CheckBlock => check_block_chunk_existence(near_config, hot), - StateViewerSubCommand::DumpCode(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::DumpAccountStorage(cmd) => cmd.run(home_dir, near_config, hot), StateViewerSubCommand::EpochInfo(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::RocksDBStats(cmd) => cmd.run(store_opener.path()), - StateViewerSubCommand::Receipts(cmd) => cmd.run(near_config, hot), - StateViewerSubCommand::Chunks(cmd) => cmd.run(near_config, hot), StateViewerSubCommand::PartialChunks(cmd) => cmd.run(near_config, hot), - StateViewerSubCommand::ApplyChunk(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::ApplyTx(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::ApplyReceipt(cmd) => cmd.run(home_dir, near_config, hot), + StateViewerSubCommand::Peers => peers(store), + StateViewerSubCommand::Receipts(cmd) => cmd.run(near_config, hot), + StateViewerSubCommand::Replay(cmd) => cmd.run(home_dir, near_config, hot), + StateViewerSubCommand::RocksDBStats(cmd) => cmd.run(store_opener.path()), + StateViewerSubCommand::State => state(home_dir, near_config, hot), + StateViewerSubCommand::ViewChain(cmd) => cmd.run(near_config, hot), StateViewerSubCommand::ViewTrie(cmd) => cmd.run(hot), } } } #[derive(Parser)] -pub struct DumpStateCmd { - /// Optionally, can specify at which height to dump state. +pub struct ApplyCmd { #[clap(long)] - height: Option, - /// Dumps state records and genesis config into separate files. - /// Has reasonable RAM requirements. - /// Use for chains with large state, such as mainnet and testnet. - /// If false - writes all information into a single file, which is useful for smaller networks, - /// such as betanet. + height: BlockHeight, #[clap(long)] - stream: bool, - /// Location of the dumped state. - /// This is a directory if --stream is set, and a file otherwise. + shard_id: ShardId, +} + +impl ApplyCmd { + pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { + apply_block_at_height(self.height, self.shard_id, home_dir, near_config, store); + } +} + +#[derive(Parser)] +pub struct ApplyChunkCmd { + #[clap(long)] + chunk_hash: String, + #[clap(long)] + target_height: Option, +} + +impl ApplyChunkCmd { + pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { + let hash = ChunkHash::from(CryptoHash::from_str(&self.chunk_hash).unwrap()); + apply_chunk(home_dir, near_config, store, hash, self.target_height).unwrap() + } +} + +#[derive(Parser)] +pub struct ApplyRangeCmd { + #[clap(long)] + start_index: Option, + #[clap(long)] + end_index: Option, + #[clap(long, default_value = "0")] + shard_id: ShardId, + #[clap(long)] + verbose_output: bool, #[clap(long, parse(from_os_str))] - file: Option, - /// List of account IDs to dump. - /// Note: validators will always be dumped. - /// If not set, all account IDs will be dumped. + csv_file: Option, #[clap(long)] - account_ids: Option>, - /// List of validators to remain validators. - /// All other validators will be kicked, but still dumped. - /// Their stake will be returned to balance. + only_contracts: bool, #[clap(long)] - include_validators: Option>, + sequential: bool, } -impl DumpStateCmd { +impl ApplyRangeCmd { pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { - dump_state( - self.height, - self.stream, - self.file, + apply_range( + self.start_index, + self.end_index, + self.shard_id, + self.verbose_output, + self.csv_file, home_dir, near_config, store, - &GenesisChangeConfig::default() - .with_select_account_ids(self.account_ids) - .with_whitelist_validators(self.include_validators), + self.only_contracts, + self.sequential, ); } } #[derive(Parser)] -pub struct DumpStateRedisCmd { - /// Optionally, can specify at which height to dump state. +pub struct ApplyReceiptCmd { #[clap(long)] - height: Option, + hash: String, } -impl DumpStateRedisCmd { +impl ApplyReceiptCmd { pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { - dump_state_redis(self.height, home_dir, near_config, store); + let hash = CryptoHash::from_str(&self.hash).unwrap(); + apply_receipt(home_dir, near_config, store, hash).unwrap(); } } #[derive(Parser)] -pub struct DumpTxCmd { - /// Specify the start block by height to begin dumping transactions from, inclusive. - #[clap(long)] - start_height: BlockHeight, - /// Specify the end block by height to stop dumping transactions at, inclusive. - #[clap(long)] - end_height: BlockHeight, - /// List of account IDs to dump. - /// If not set, all account IDs will be dumped. - #[clap(long)] - account_ids: Option>, - /// Optionally, can specify the path of the output. +pub struct ApplyTxCmd { #[clap(long)] - output_path: Option, + hash: String, } -impl DumpTxCmd { +impl ApplyTxCmd { pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { - dump_tx( - self.start_height, - self.end_height, - home_dir, - near_config, - store, - self.account_ids.as_deref(), - self.output_path, - ) - .expect("Failed to dump transaction...") + let hash = CryptoHash::from_str(&self.hash).unwrap(); + apply_tx(home_dir, near_config, store, hash).unwrap(); } } @@ -226,123 +233,177 @@ impl ChainCmd { } #[derive(Parser)] -pub struct ReplayCmd { - #[clap(long)] - start_index: BlockHeight, +pub struct ChunksCmd { #[clap(long)] - end_index: BlockHeight, + chunk_hash: String, } -impl ReplayCmd { - pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { - replay_chain(self.start_index, self.end_index, home_dir, near_config, store); +impl ChunksCmd { + pub fn run(self, near_config: NearConfig, store: Store) { + let chunk_hash = ChunkHash::from(CryptoHash::from_str(&self.chunk_hash).unwrap()); + get_chunk(chunk_hash, near_config, store) } } #[derive(Parser)] -pub struct ApplyRangeCmd { - #[clap(long)] - start_index: Option, +pub struct DumpAccountStorageCmd { #[clap(long)] - end_index: Option, - #[clap(long, default_value = "0")] - shard_id: ShardId, + account_id: String, #[clap(long)] - verbose_output: bool, + storage_key: String, #[clap(long, parse(from_os_str))] - csv_file: Option, - #[clap(long)] - only_contracts: bool, + output: PathBuf, #[clap(long)] - sequential: bool, + block_height: String, } -impl ApplyRangeCmd { +impl DumpAccountStorageCmd { pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { - apply_range( - self.start_index, - self.end_index, - self.shard_id, - self.verbose_output, - self.csv_file, + dump_account_storage( + self.account_id, + self.storage_key, + &self.output, + self.block_height, home_dir, near_config, store, - self.only_contracts, - self.sequential, ); } } #[derive(Parser)] -pub struct ApplyCmd { - #[clap(long)] - height: BlockHeight, +pub struct DumpCodeCmd { #[clap(long)] - shard_id: ShardId, + account_id: String, + #[clap(long, parse(from_os_str))] + output: PathBuf, } -impl ApplyCmd { +impl DumpCodeCmd { pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { - apply_block_at_height(self.height, self.shard_id, home_dir, near_config, store); + dump_code(self.account_id, &self.output, home_dir, near_config, store); } } #[derive(Parser)] -pub struct ViewChainCmd { +pub struct DumpStateCmd { + /// Optionally, can specify at which height to dump state. #[clap(long)] height: Option, + /// Dumps state records and genesis config into separate files. + /// Has reasonable RAM requirements. + /// Use for chains with large state, such as mainnet and testnet. + /// If false - writes all information into a single file, which is useful for smaller networks, + /// such as betanet. #[clap(long)] - block: bool, + stream: bool, + /// Location of the dumped state. + /// This is a directory if --stream is set, and a file otherwise. + #[clap(long, parse(from_os_str))] + file: Option, + /// List of account IDs to dump. + /// Note: validators will always be dumped. + /// If not set, all account IDs will be dumped. #[clap(long)] - chunk: bool, + account_ids: Option>, + /// List of validators to remain validators. + /// All other validators will be kicked, but still dumped. + /// Their stake will be returned to balance. + #[clap(long)] + include_validators: Option>, } -impl ViewChainCmd { - pub fn run(self, near_config: NearConfig, store: Store) { - view_chain(self.height, self.block, self.chunk, near_config, store); +impl DumpStateCmd { + pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { + dump_state( + self.height, + self.stream, + self.file, + home_dir, + near_config, + store, + &GenesisChangeConfig::default() + .with_select_account_ids(self.account_ids) + .with_whitelist_validators(self.include_validators), + ); } } #[derive(Parser)] -pub struct DumpCodeCmd { +pub struct DumpStatePartsCmd { + /// Selects an epoch. The dump will be of the state at the beginning of this epoch. + #[clap(subcommand)] + epoch_selection: dump_state_parts::EpochSelection, + /// Shard id. #[clap(long)] - account_id: String, - #[clap(long, parse(from_os_str))] - output: PathBuf, + shard_id: ShardId, + /// State part id. Leave empty to go through every part in the shard. + #[clap(long)] + part_id: Option, + /// Where to write the state parts to. + #[clap(long)] + output_dir: PathBuf, } -impl DumpCodeCmd { +impl DumpStatePartsCmd { pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { - dump_code(self.account_id, &self.output, home_dir, near_config, store); + dump_state_parts( + self.epoch_selection, + self.shard_id, + self.part_id, + home_dir, + near_config, + store, + &self.output_dir, + ); } } #[derive(Parser)] -pub struct DumpAccountStorageCmd { +pub struct DumpStateRedisCmd { + /// Optionally, can specify at which height to dump state. #[clap(long)] - account_id: String, + height: Option, +} + +impl DumpStateRedisCmd { + pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { + dump_state_redis(self.height, home_dir, near_config, store); + } +} + +#[derive(Parser)] +pub struct DumpTxCmd { + /// Specify the start block by height to begin dumping transactions from, inclusive. #[clap(long)] - storage_key: String, - #[clap(long, parse(from_os_str))] - output: PathBuf, + start_height: BlockHeight, + /// Specify the end block by height to stop dumping transactions at, inclusive. #[clap(long)] - block_height: String, + end_height: BlockHeight, + /// List of account IDs to dump. + /// If not set, all account IDs will be dumped. + #[clap(long)] + account_ids: Option>, + /// Optionally, can specify the path of the output. + #[clap(long)] + output_path: Option, } -impl DumpAccountStorageCmd { +impl DumpTxCmd { pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { - dump_account_storage( - self.account_id, - self.storage_key, - &self.output, - self.block_height, + dump_tx( + self.start_height, + self.end_height, home_dir, near_config, store, - ); + self.account_ids.as_deref(), + self.output_path, + ) + .expect("Failed to dump transaction...") } } + #[derive(Args)] pub struct EpochInfoCmd { #[clap(subcommand)] @@ -365,15 +426,16 @@ impl EpochInfoCmd { } #[derive(Parser)] -pub struct RocksDBStatsCmd { - /// Location of the dumped Rocks DB stats. - #[clap(long, parse(from_os_str))] - file: Option, +pub struct PartialChunksCmd { + #[clap(long)] + partial_chunk_hash: String, } -impl RocksDBStatsCmd { - pub fn run(self, store_dir: &Path) { - get_rocksdb_stats(store_dir, self.file).expect("Couldn't get RocksDB stats"); +impl PartialChunksCmd { + pub fn run(self, near_config: NearConfig, store: Store) { + let partial_chunk_hash = + ChunkHash::from(CryptoHash::from_str(&self.partial_chunk_hash).unwrap()); + get_partial_chunk(partial_chunk_hash, near_config, store) } } @@ -390,69 +452,45 @@ impl ReceiptsCmd { } #[derive(Parser)] -pub struct ChunksCmd { - #[clap(long)] - chunk_hash: String, -} - -impl ChunksCmd { - pub fn run(self, near_config: NearConfig, store: Store) { - let chunk_hash = ChunkHash::from(CryptoHash::from_str(&self.chunk_hash).unwrap()); - get_chunk(chunk_hash, near_config, store) - } -} -#[derive(Parser)] -pub struct PartialChunksCmd { - #[clap(long)] - partial_chunk_hash: String, -} - -impl PartialChunksCmd { - pub fn run(self, near_config: NearConfig, store: Store) { - let partial_chunk_hash = - ChunkHash::from(CryptoHash::from_str(&self.partial_chunk_hash).unwrap()); - get_partial_chunk(partial_chunk_hash, near_config, store) - } -} - -#[derive(Parser)] -pub struct ApplyChunkCmd { +pub struct ReplayCmd { #[clap(long)] - chunk_hash: String, + start_index: BlockHeight, #[clap(long)] - target_height: Option, + end_index: BlockHeight, } -impl ApplyChunkCmd { +impl ReplayCmd { pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { - let hash = ChunkHash::from(CryptoHash::from_str(&self.chunk_hash).unwrap()); - apply_chunk(home_dir, near_config, store, hash, self.target_height).unwrap() + replay_chain(self.start_index, self.end_index, home_dir, near_config, store); } } #[derive(Parser)] -pub struct ApplyTxCmd { - #[clap(long)] - hash: String, +pub struct RocksDBStatsCmd { + /// Location of the dumped Rocks DB stats. + #[clap(long, parse(from_os_str))] + file: Option, } -impl ApplyTxCmd { - pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { - let hash = CryptoHash::from_str(&self.hash).unwrap(); - apply_tx(home_dir, near_config, store, hash).unwrap(); +impl RocksDBStatsCmd { + pub fn run(self, store_dir: &Path) { + get_rocksdb_stats(store_dir, self.file).expect("Couldn't get RocksDB stats"); } } #[derive(Parser)] -pub struct ApplyReceiptCmd { +pub struct ViewChainCmd { #[clap(long)] - hash: String, + height: Option, + #[clap(long)] + block: bool, + #[clap(long)] + chunk: bool, } -impl ApplyReceiptCmd { - pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { - let hash = CryptoHash::from_str(&self.hash).unwrap(); - apply_receipt(home_dir, near_config, store, hash).unwrap(); +impl ViewChainCmd { + pub fn run(self, near_config: NearConfig, store: Store) { + view_chain(self.height, self.block, self.chunk, near_config, store); } } @@ -474,33 +512,3 @@ impl ViewTrieCmd { view_trie(store, hash, self.shard_id, self.shard_version, self.max_depth).unwrap(); } } - -#[derive(Parser)] -pub struct DumpStatePartsCmd { - /// Selects an epoch. The dump will be of the state at the beginning of this epoch. - #[clap(subcommand)] - epoch_selection: dump_state_parts::EpochSelection, - /// Shard id. - #[clap(long)] - shard_id: ShardId, - /// State part id. Leave empty to go through every part in the shard. - #[clap(long)] - part_id: Option, - /// Where to write the state parts to. - #[clap(long)] - output_dir: PathBuf, -} - -impl DumpStatePartsCmd { - pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { - dump_state_parts( - self.epoch_selection, - self.shard_id, - self.part_id, - home_dir, - near_config, - store, - &self.output_dir, - ); - } -} diff --git a/tools/state-viewer/src/commands.rs b/tools/state-viewer/src/commands.rs index 5f83eaa922b..8b2e16a1319 100644 --- a/tools/state-viewer/src/commands.rs +++ b/tools/state-viewer/src/commands.rs @@ -38,26 +38,258 @@ use std::io::Write; use std::path::{Path, PathBuf}; use std::sync::Arc; -pub(crate) fn peers(store: NodeStorage) { - iter_peers_from_store(store, |(peer_id, peer_info)| { - println!("{} {:?}", peer_id, peer_info); - }) +pub(crate) fn apply_block( + block_hash: CryptoHash, + shard_id: ShardId, + runtime_adapter: &dyn RuntimeAdapter, + chain_store: &mut ChainStore, +) -> (Block, ApplyTransactionResult) { + let block = chain_store.get_block(&block_hash).unwrap(); + let height = block.header().height(); + let shard_uid = runtime_adapter.shard_id_to_uid(shard_id, block.header().epoch_id()).unwrap(); + let apply_result = if block.chunks()[shard_id as usize].height_included() == height { + let chunk = chain_store.get_chunk(&block.chunks()[shard_id as usize].chunk_hash()).unwrap(); + let prev_block = chain_store.get_block(block.header().prev_hash()).unwrap(); + let chain_store_update = ChainStoreUpdate::new(chain_store); + let receipt_proof_response = chain_store_update + .get_incoming_receipts_for_shard( + shard_id, + block_hash, + prev_block.chunks()[shard_id as usize].height_included(), + ) + .unwrap(); + let receipts = collect_receipts_from_response(&receipt_proof_response); + + let chunk_inner = chunk.cloned_header().take_inner(); + let is_first_block_with_chunk_of_version = check_if_block_is_first_with_chunk_of_version( + chain_store, + runtime_adapter, + block.header().prev_hash(), + shard_id, + ) + .unwrap(); + + runtime_adapter + .apply_transactions( + shard_id, + chunk_inner.prev_state_root(), + height, + block.header().raw_timestamp(), + block.header().prev_hash(), + block.hash(), + &receipts, + chunk.transactions(), + chunk_inner.validator_proposals(), + prev_block.header().gas_price(), + chunk_inner.gas_limit(), + block.header().challenges_result(), + *block.header().random_value(), + true, + is_first_block_with_chunk_of_version, + Default::default(), + false, + ) + .unwrap() + } else { + let chunk_extra = + chain_store.get_chunk_extra(block.header().prev_hash(), &shard_uid).unwrap(); + + runtime_adapter + .apply_transactions( + shard_id, + chunk_extra.state_root(), + block.header().height(), + block.header().raw_timestamp(), + block.header().prev_hash(), + block.hash(), + &[], + &[], + chunk_extra.validator_proposals(), + block.header().gas_price(), + chunk_extra.gas_limit(), + block.header().challenges_result(), + *block.header().random_value(), + false, + false, + Default::default(), + false, + ) + .unwrap() + }; + (block, apply_result) } -pub(crate) fn state(home_dir: &Path, near_config: NearConfig, store: Store) { - let (runtime, state_roots, header) = load_trie(store, home_dir, &near_config); - println!("Storage roots are {:?}, block height is {}", state_roots, header.height()); +pub(crate) fn apply_block_at_height( + height: BlockHeight, + shard_id: ShardId, + home_dir: &Path, + near_config: NearConfig, + store: Store, +) { + let mut chain_store = ChainStore::new( + store.clone(), + near_config.genesis.config.genesis_height, + near_config.client_config.save_trie_changes, + ); + let runtime_adapter: Arc = + Arc::new(NightshadeRuntime::from_config(home_dir, store, &near_config)); + let block_hash = chain_store.get_block_hash_by_height(height).unwrap(); + let (block, apply_result) = + apply_block(block_hash, shard_id, runtime_adapter.as_ref(), &mut chain_store); + print_apply_block_result( + &block, + &apply_result, + runtime_adapter.as_ref(), + &mut chain_store, + shard_id, + ); +} + +pub(crate) fn apply_chunk( + home_dir: &Path, + near_config: NearConfig, + store: Store, + chunk_hash: ChunkHash, + target_height: Option, +) -> anyhow::Result<()> { + let runtime = NightshadeRuntime::from_config(home_dir, store.clone(), &near_config); + let mut chain_store = ChainStore::new( + store, + near_config.genesis.config.genesis_height, + near_config.client_config.save_trie_changes, + ); + let (apply_result, gas_limit) = + apply_chunk::apply_chunk(&runtime, &mut chain_store, chunk_hash, target_height, None)?; + println!("resulting chunk extra:\n{:?}", resulting_chunk_extra(&apply_result, gas_limit)); + Ok(()) +} + +pub(crate) fn apply_range( + start_index: Option, + end_index: Option, + shard_id: ShardId, + verbose_output: bool, + csv_file: Option, + home_dir: &Path, + near_config: NearConfig, + store: Store, + only_contracts: bool, + sequential: bool, +) { + let mut csv_file = csv_file.map(|filename| std::fs::File::create(filename).unwrap()); + + let runtime = NightshadeRuntime::from_config(home_dir, store.clone(), &near_config); + apply_chain_range( + store, + &near_config.genesis, + start_index, + end_index, + shard_id, + runtime, + verbose_output, + csv_file.as_mut(), + only_contracts, + sequential, + ); +} + +pub(crate) fn apply_receipt( + home_dir: &Path, + near_config: NearConfig, + store: Store, + hash: CryptoHash, +) -> anyhow::Result<()> { + let runtime = NightshadeRuntime::from_config(home_dir, store.clone(), &near_config); + apply_chunk::apply_receipt(near_config.genesis.config.genesis_height, &runtime, store, hash) + .map(|_| ()) +} + +pub(crate) fn apply_tx( + home_dir: &Path, + near_config: NearConfig, + store: Store, + hash: CryptoHash, +) -> anyhow::Result<()> { + let runtime = NightshadeRuntime::from_config(home_dir, store.clone(), &near_config); + apply_chunk::apply_tx(near_config.genesis.config.genesis_height, &runtime, store, hash) + .map(|_| ()) +} + +pub(crate) fn dump_account_storage( + account_id: String, + storage_key: String, + output: &Path, + block_height: String, + home_dir: &Path, + near_config: NearConfig, + store: Store, +) { + let block_height = if block_height == "latest" { + LoadTrieMode::Latest + } else if let Ok(height) = block_height.parse::() { + LoadTrieMode::Height(height) + } else { + panic!("block_height should be either number or \"latest\"") + }; + let (runtime, state_roots, header) = + load_trie_stop_at_height(store, home_dir, &near_config, block_height); for (shard_id, state_root) in state_roots.iter().enumerate() { let trie = runtime .get_trie_for_shard(shard_id as u64, header.prev_hash(), state_root.clone(), false) .unwrap(); - for item in trie.iter().unwrap() { - let (key, value) = item.unwrap(); - if let Some(state_record) = StateRecord::from_raw_key_value(key, value) { - println!("{}", state_record); + let key = TrieKey::ContractData { + account_id: account_id.parse().unwrap(), + key: storage_key.as_bytes().to_vec(), + }; + let item = trie.get(&key.to_vec()); + let value = item.unwrap(); + if let Some(value) = value { + let record = StateRecord::from_raw_key_value(key.to_vec(), value).unwrap(); + match record { + StateRecord::Data { account_id: _, data_key: _, value } => { + fs::write(output, &value).unwrap(); + println!( + "Dump contract storage under key {} of account {} into file {}", + storage_key, + account_id, + output.display() + ); + std::process::exit(0); + } + _ => unreachable!(), } } } + println!("Storage under key {} of account {} not found", storage_key, account_id); + std::process::exit(1); +} + +pub(crate) fn dump_code( + account_id: String, + output: &Path, + home_dir: &Path, + near_config: NearConfig, + store: Store, +) { + let (runtime, state_roots, header) = load_trie(store, home_dir, &near_config); + let epoch_id = &runtime.get_epoch_id(header.hash()).unwrap(); + + for (shard_id, state_root) in state_roots.iter().enumerate() { + let shard_uid = runtime.shard_id_to_uid(shard_id as u64, epoch_id).unwrap(); + if let Ok(contract_code) = + runtime.view_contract_code(&shard_uid, *state_root, &account_id.parse().unwrap()) + { + let mut file = File::create(output).unwrap(); + file.write_all(contract_code.code()).unwrap(); + println!("Dump contract of account {} into file {}", account_id, output.display()); + + std::process::exit(0); + } + } + println!( + "Account {} does not exist or do not have contract deployed in all shards", + account_id + ); } pub(crate) fn dump_state( @@ -149,110 +381,71 @@ pub(crate) fn dump_tx( return Ok(()); } -pub(crate) fn apply_range( - start_index: Option, - end_index: Option, - shard_id: ShardId, - verbose_output: bool, - csv_file: Option, - home_dir: &Path, - near_config: NearConfig, - store: Store, - only_contracts: bool, - sequential: bool, -) { - let mut csv_file = csv_file.map(|filename| std::fs::File::create(filename).unwrap()); - - let runtime = NightshadeRuntime::from_config(home_dir, store.clone(), &near_config); - apply_chain_range( +pub(crate) fn get_chunk(chunk_hash: ChunkHash, near_config: NearConfig, store: Store) { + let chain_store = ChainStore::new( store, - &near_config.genesis, - start_index, - end_index, - shard_id, - runtime, - verbose_output, - csv_file.as_mut(), - only_contracts, - sequential, + near_config.genesis.config.genesis_height, + near_config.client_config.save_trie_changes, ); + let chunk = chain_store.get_chunk(&chunk_hash); + println!("Chunk: {:#?}", chunk); } -pub(crate) fn dump_code( - account_id: String, - output: &Path, - home_dir: &Path, +pub(crate) fn get_partial_chunk( + partial_chunk_hash: ChunkHash, near_config: NearConfig, store: Store, ) { - let (runtime, state_roots, header) = load_trie(store, home_dir, &near_config); - let epoch_id = &runtime.get_epoch_id(header.hash()).unwrap(); - - for (shard_id, state_root) in state_roots.iter().enumerate() { - let shard_uid = runtime.shard_id_to_uid(shard_id as u64, epoch_id).unwrap(); - if let Ok(contract_code) = - runtime.view_contract_code(&shard_uid, *state_root, &account_id.parse().unwrap()) - { - let mut file = File::create(output).unwrap(); - file.write_all(contract_code.code()).unwrap(); - println!("Dump contract of account {} into file {}", account_id, output.display()); + let chain_store = ChainStore::new( + store, + near_config.genesis.config.genesis_height, + near_config.client_config.save_trie_changes, + ); + let partial_chunk = chain_store.get_partial_chunk(&partial_chunk_hash); + println!("Partial chunk: {:#?}", partial_chunk); +} - std::process::exit(0); - } - } - println!( - "Account {} does not exist or do not have contract deployed in all shards", - account_id +pub(crate) fn get_receipt(receipt_id: CryptoHash, near_config: NearConfig, store: Store) { + let chain_store = ChainStore::new( + store, + near_config.genesis.config.genesis_height, + near_config.client_config.save_trie_changes, ); + let receipt = chain_store.get_receipt(&receipt_id); + println!("Receipt: {:#?}", receipt); } -pub(crate) fn dump_account_storage( - account_id: String, - storage_key: String, - output: &Path, - block_height: String, - home_dir: &Path, - near_config: NearConfig, - store: Store, +pub(crate) fn peers(store: NodeStorage) { + iter_peers_from_store(store, |(peer_id, peer_info)| { + println!("{} {:?}", peer_id, peer_info); + }) +} + +pub(crate) fn print_apply_block_result( + block: &Block, + apply_result: &ApplyTransactionResult, + runtime_adapter: &dyn RuntimeAdapter, + chain_store: &mut ChainStore, + shard_id: ShardId, ) { - let block_height = if block_height == "latest" { - LoadTrieMode::Latest - } else if let Ok(height) = block_height.parse::() { - LoadTrieMode::Height(height) - } else { - panic!("block_height should be either number or \"latest\"") - }; - let (runtime, state_roots, header) = - load_trie_stop_at_height(store, home_dir, &near_config, block_height); - for (shard_id, state_root) in state_roots.iter().enumerate() { - let trie = runtime - .get_trie_for_shard(shard_id as u64, header.prev_hash(), state_root.clone(), false) - .unwrap(); - let key = TrieKey::ContractData { - account_id: account_id.parse().unwrap(), - key: storage_key.as_bytes().to_vec(), - }; - let item = trie.get(&key.to_vec()); - let value = item.unwrap(); - if let Some(value) = value { - let record = StateRecord::from_raw_key_value(key.to_vec(), value).unwrap(); - match record { - StateRecord::Data { account_id: _, data_key: _, value } => { - fs::write(output, &value).unwrap(); - println!( - "Dump contract storage under key {} of account {} into file {}", - storage_key, - account_id, - output.display() - ); - std::process::exit(0); - } - _ => unreachable!(), - } + let height = block.header().height(); + let block_hash = block.header().hash(); + println!( + "apply chunk for shard {} at height {}, resulting chunk extra {:?}", + shard_id, + height, + resulting_chunk_extra(apply_result, block.chunks()[shard_id as usize].gas_limit()) + ); + let shard_uid = runtime_adapter.shard_id_to_uid(shard_id, block.header().epoch_id()).unwrap(); + if block.chunks()[shard_id as usize].height_included() == height { + if let Ok(chunk_extra) = chain_store.get_chunk_extra(&block_hash, &shard_uid) { + println!("Existing chunk extra: {:?}", chunk_extra); + } else { + println!("No existing chunk extra available"); } + } else { + println!("No existing chunk extra available"); } - println!("Storage under key {} of account {} not found", storage_key, account_id); - std::process::exit(1); } pub(crate) fn print_chain( @@ -399,140 +592,22 @@ pub(crate) fn resulting_chunk_extra(result: &ApplyTransactionResult, gas_limit: ) } -pub(crate) fn apply_block( - block_hash: CryptoHash, - shard_id: ShardId, - runtime_adapter: &dyn RuntimeAdapter, - chain_store: &mut ChainStore, -) -> (Block, ApplyTransactionResult) { - let block = chain_store.get_block(&block_hash).unwrap(); - let height = block.header().height(); - let shard_uid = runtime_adapter.shard_id_to_uid(shard_id, block.header().epoch_id()).unwrap(); - let apply_result = if block.chunks()[shard_id as usize].height_included() == height { - let chunk = chain_store.get_chunk(&block.chunks()[shard_id as usize].chunk_hash()).unwrap(); - let prev_block = chain_store.get_block(block.header().prev_hash()).unwrap(); - let chain_store_update = ChainStoreUpdate::new(chain_store); - let receipt_proof_response = chain_store_update - .get_incoming_receipts_for_shard( - shard_id, - block_hash, - prev_block.chunks()[shard_id as usize].height_included(), - ) +pub(crate) fn state(home_dir: &Path, near_config: NearConfig, store: Store) { + let (runtime, state_roots, header) = load_trie(store, home_dir, &near_config); + println!("Storage roots are {:?}, block height is {}", state_roots, header.height()); + for (shard_id, state_root) in state_roots.iter().enumerate() { + let trie = runtime + .get_trie_for_shard(shard_id as u64, header.prev_hash(), state_root.clone(), false) .unwrap(); - let receipts = collect_receipts_from_response(&receipt_proof_response); - - let chunk_inner = chunk.cloned_header().take_inner(); - let is_first_block_with_chunk_of_version = check_if_block_is_first_with_chunk_of_version( - chain_store, - runtime_adapter, - block.header().prev_hash(), - shard_id, - ) - .unwrap(); - - runtime_adapter - .apply_transactions( - shard_id, - chunk_inner.prev_state_root(), - height, - block.header().raw_timestamp(), - block.header().prev_hash(), - block.hash(), - &receipts, - chunk.transactions(), - chunk_inner.validator_proposals(), - prev_block.header().gas_price(), - chunk_inner.gas_limit(), - block.header().challenges_result(), - *block.header().random_value(), - true, - is_first_block_with_chunk_of_version, - Default::default(), - false, - ) - .unwrap() - } else { - let chunk_extra = - chain_store.get_chunk_extra(block.header().prev_hash(), &shard_uid).unwrap(); - - runtime_adapter - .apply_transactions( - shard_id, - chunk_extra.state_root(), - block.header().height(), - block.header().raw_timestamp(), - block.header().prev_hash(), - block.hash(), - &[], - &[], - chunk_extra.validator_proposals(), - block.header().gas_price(), - chunk_extra.gas_limit(), - block.header().challenges_result(), - *block.header().random_value(), - false, - false, - Default::default(), - false, - ) - .unwrap() - }; - (block, apply_result) -} - -pub(crate) fn print_apply_block_result( - block: &Block, - apply_result: &ApplyTransactionResult, - runtime_adapter: &dyn RuntimeAdapter, - chain_store: &mut ChainStore, - shard_id: ShardId, -) { - let height = block.header().height(); - let block_hash = block.header().hash(); - println!( - "apply chunk for shard {} at height {}, resulting chunk extra {:?}", - shard_id, - height, - resulting_chunk_extra(apply_result, block.chunks()[shard_id as usize].gas_limit()) - ); - let shard_uid = runtime_adapter.shard_id_to_uid(shard_id, block.header().epoch_id()).unwrap(); - if block.chunks()[shard_id as usize].height_included() == height { - if let Ok(chunk_extra) = chain_store.get_chunk_extra(&block_hash, &shard_uid) { - println!("Existing chunk extra: {:?}", chunk_extra); - } else { - println!("No existing chunk extra available"); + for item in trie.iter().unwrap() { + let (key, value) = item.unwrap(); + if let Some(state_record) = StateRecord::from_raw_key_value(key, value) { + println!("{}", state_record); + } } - } else { - println!("No existing chunk extra available"); } } -pub(crate) fn apply_block_at_height( - height: BlockHeight, - shard_id: ShardId, - home_dir: &Path, - near_config: NearConfig, - store: Store, -) { - let mut chain_store = ChainStore::new( - store.clone(), - near_config.genesis.config.genesis_height, - near_config.client_config.save_trie_changes, - ); - let runtime_adapter: Arc = - Arc::new(NightshadeRuntime::from_config(home_dir, store, &near_config)); - let block_hash = chain_store.get_block_hash_by_height(height).unwrap(); - let (block, apply_result) = - apply_block(block_hash, shard_id, runtime_adapter.as_ref(), &mut chain_store); - print_apply_block_result( - &block, - &apply_result, - runtime_adapter.as_ref(), - &mut chain_store, - shard_id, - ); -} - pub(crate) fn view_chain( height: Option, view_block: bool, @@ -659,38 +734,20 @@ pub(crate) fn print_epoch_info( ); } -pub(crate) fn get_receipt(receipt_id: CryptoHash, near_config: NearConfig, store: Store) { - let chain_store = ChainStore::new( - store, - near_config.genesis.config.genesis_height, - near_config.client_config.save_trie_changes, - ); - let receipt = chain_store.get_receipt(&receipt_id); - println!("Receipt: {:#?}", receipt); -} - -pub(crate) fn get_chunk(chunk_hash: ChunkHash, near_config: NearConfig, store: Store) { - let chain_store = ChainStore::new( - store, - near_config.genesis.config.genesis_height, - near_config.client_config.save_trie_changes, - ); - let chunk = chain_store.get_chunk(&chunk_hash); - println!("Chunk: {:#?}", chunk); -} - -pub(crate) fn get_partial_chunk( - partial_chunk_hash: ChunkHash, - near_config: NearConfig, +pub(crate) fn view_trie( store: Store, -) { - let chain_store = ChainStore::new( - store, - near_config.genesis.config.genesis_height, - near_config.client_config.save_trie_changes, - ); - let partial_chunk = chain_store.get_partial_chunk(&partial_chunk_hash); - println!("Partial chunk: {:#?}", partial_chunk); + hash: CryptoHash, + shard_id: u32, + shard_version: u32, + max_depth: u32, +) -> anyhow::Result<()> { + let shard_uid = ShardUId { version: shard_version, shard_id }; + let trie_config: TrieConfig = Default::default(); + let shard_cache = TrieCache::new(&trie_config, shard_uid, true); + let trie_storage = TrieCachingStorage::new(store, shard_cache, shard_uid, true, None); + let trie = Trie::new(Box::new(trie_storage), Trie::EMPTY_ROOT, None); + trie.print_recursive(&mut std::io::stdout().lock(), &hash, max_depth); + Ok(()) } #[allow(unused)] @@ -772,60 +829,3 @@ fn format_hash(h: CryptoHash, show_full_hashes: bool) -> String { pub fn chunk_mask_to_str(mask: &[bool]) -> String { mask.iter().map(|f| if *f { '.' } else { 'X' }).collect() } - -pub(crate) fn apply_chunk( - home_dir: &Path, - near_config: NearConfig, - store: Store, - chunk_hash: ChunkHash, - target_height: Option, -) -> anyhow::Result<()> { - let runtime = NightshadeRuntime::from_config(home_dir, store.clone(), &near_config); - let mut chain_store = ChainStore::new( - store, - near_config.genesis.config.genesis_height, - near_config.client_config.save_trie_changes, - ); - let (apply_result, gas_limit) = - apply_chunk::apply_chunk(&runtime, &mut chain_store, chunk_hash, target_height, None)?; - println!("resulting chunk extra:\n{:?}", resulting_chunk_extra(&apply_result, gas_limit)); - Ok(()) -} - -pub(crate) fn apply_tx( - home_dir: &Path, - near_config: NearConfig, - store: Store, - hash: CryptoHash, -) -> anyhow::Result<()> { - let runtime = NightshadeRuntime::from_config(home_dir, store.clone(), &near_config); - apply_chunk::apply_tx(near_config.genesis.config.genesis_height, &runtime, store, hash) - .map(|_| ()) -} - -pub(crate) fn apply_receipt( - home_dir: &Path, - near_config: NearConfig, - store: Store, - hash: CryptoHash, -) -> anyhow::Result<()> { - let runtime = NightshadeRuntime::from_config(home_dir, store.clone(), &near_config); - apply_chunk::apply_receipt(near_config.genesis.config.genesis_height, &runtime, store, hash) - .map(|_| ()) -} - -pub(crate) fn view_trie( - store: Store, - hash: CryptoHash, - shard_id: u32, - shard_version: u32, - max_depth: u32, -) -> anyhow::Result<()> { - let shard_uid = ShardUId { version: shard_version, shard_id }; - let trie_config: TrieConfig = Default::default(); - let shard_cache = TrieCache::new(&trie_config, shard_uid, true); - let trie_storage = TrieCachingStorage::new(store, shard_cache, shard_uid, true, None); - let trie = Trie::new(Box::new(trie_storage), Trie::EMPTY_ROOT, None); - trie.print_recursive(&mut std::io::stdout().lock(), &hash, max_depth); - Ok(()) -} From b9df6f2e8fe9c6b36e832486dc91610c677f4c43 Mon Sep 17 00:00:00 2001 From: Marcelo Diop-Gonzalez Date: Sat, 7 Jan 2023 11:02:03 -0500 Subject: [PATCH 147/188] fix(pytest): fix assert in switch_node_key.py (#8302) status1 isn't defined, so use the validator RPC response to print out the info --- pytest/tests/sanity/switch_node_key.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest/tests/sanity/switch_node_key.py b/pytest/tests/sanity/switch_node_key.py index 2e5af5cbe35..ca77786ea5f 100755 --- a/pytest/tests/sanity/switch_node_key.py +++ b/pytest/tests/sanity/switch_node_key.py @@ -53,4 +53,4 @@ validators = nodes[1].get_validators() assert len( validators['result']['next_validators'] -) == 2, f'unexpected number of validators, current validators: {status1["validators"]}' +) == 2, f'unexpected number of validators, next validators: {validators["result"]["next_validators"]}' From 715db206f7ac10af71bdcf62e7c32f717fecbf54 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Sun, 8 Jan 2023 13:59:36 +0100 Subject: [PATCH 148/188] fix: estimator generates too long account names (#8297) In the estimation for CreateAccountAction, we appended a long hash to a random account to generate a new and unique account ID. This could trigger ParseAccountError { kind: TooLong, char: None }'. New approach: Append a constant suffix to a random unused account. This also guarantees the sender account is only used once. This reduces randomness on whether the sender account is already cached due to previous iterations, which was another source of measurement noise. --- runtime/runtime-params-estimator/src/lib.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/runtime/runtime-params-estimator/src/lib.rs b/runtime/runtime-params-estimator/src/lib.rs index f31766d1a3c..2d24a425864 100644 --- a/runtime/runtime-params-estimator/src/lib.rs +++ b/runtime/runtime-params-estimator/src/lib.rs @@ -103,7 +103,6 @@ use near_primitives::version::PROTOCOL_VERSION; use near_vm_logic::mocks::mock_external::MockedExternal; use near_vm_logic::{ExtCosts, VMConfig}; use near_vm_runner::MockCompiledContractCache; -use rand::Rng; use serde_json::json; use utils::{ average_cost, fn_cost, fn_cost_count, fn_cost_in_contract, fn_cost_with_setup, @@ -376,9 +375,9 @@ fn action_transfer(ctx: &mut EstimatorContext) -> GasCost { fn action_create_account(ctx: &mut EstimatorContext) -> GasCost { let total_cost = { let mut make_transaction = |tb: &mut TransactionBuilder| -> SignedTransaction { - let sender = tb.random_account(); - let new_account = - AccountId::try_from(format!("{}_{}", sender, tb.rng().gen::())).unwrap(); + let sender = tb.random_unused_account(); + // derive a non-existing account id + let new_account = AccountId::try_from(format!("{sender}_x")).unwrap(); let actions = vec![ Action::CreateAccount(CreateAccountAction {}), From 55e48b33c7e0bcce74c1342cd81c53597e6ba76f Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Mon, 9 Jan 2023 10:05:55 +0100 Subject: [PATCH 149/188] chore: remove outdated constant ATTO_NEAR (#8290) This constant is: - wrong: it should be yocto near (10^-24) and not atto (10^-18) - outdated: we haven't used atto near since 2019 (#1827) - unused: there are no code references to it Better we get rid of it. --- nearcore/src/config.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/nearcore/src/config.rs b/nearcore/src/config.rs index 93582c8d1dd..75a4ddfc5a2 100644 --- a/nearcore/src/config.rs +++ b/nearcore/src/config.rs @@ -55,9 +55,6 @@ pub const NEAR_BASE: Balance = 1_000_000_000_000_000_000_000_000; /// Millinear, 1/1000 of NEAR. pub const MILLI_NEAR: Balance = NEAR_BASE / 1000; -/// Attonear, 1/10^18 of NEAR. -pub const ATTO_NEAR: Balance = 1; - /// Block production tracking delay. pub const BLOCK_PRODUCTION_TRACKING_DELAY: u64 = 100; From 4b922dc669d5371912ad065fed2e54c5ae2231ef Mon Sep 17 00:00:00 2001 From: Andrei Kashin Date: Mon, 9 Jan 2023 10:15:40 +0000 Subject: [PATCH 150/188] doc: Fix typo and broken link in documentation (#8308) Co-authored-by: Akhilesh Singhania --- core/primitives/src/views.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index 6b10b268729..a1b4652e491 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -1989,7 +1989,7 @@ pub type MaintenanceWindowsView = Vec>; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct RuntimeConfigView { /// Amount of yN per byte required to have on the account. See - /// for details. + /// for details. #[serde(with = "dec_format")] pub storage_amount_per_byte: Balance, /// Costs of different actions that need to be performed when sending and @@ -2255,7 +2255,7 @@ impl From for VMConfig { } } -/// Typed view of ExtCostsConfig t preserve JSON output field names in protocol +/// Typed view of ExtCostsConfig to preserve JSON output field names in protocol /// config RPC output. #[derive(Debug, Serialize, Deserialize, Clone, Hash, PartialEq, Eq)] pub struct ExtCostsConfigView { From 073395c82f6ca0f913c068a4511e5321419b516b Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Mon, 9 Jan 2023 12:43:42 +0100 Subject: [PATCH 151/188] refactor: remove noop code around abusive peer banning (#8300) #1586 was closed as "won't do", see [this comment](https://github.com/near/nearcore/issues/1586#issuecomment-872849765) for more details. This PR removes the left over code. Part of #8145 since this code also produces clippy error: ``` error: this comparison involving the minimum or maximum element for this type contains a case that is always true or always false ``` --- chain/network/src/peer/peer_actor.rs | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/chain/network/src/peer/peer_actor.rs b/chain/network/src/peer/peer_actor.rs index dd90e18bd1d..539f8fd376c 100644 --- a/chain/network/src/peer/peer_actor.rs +++ b/chain/network/src/peer/peer_actor.rs @@ -40,9 +40,6 @@ use std::sync::atomic::Ordering; use std::sync::Arc; use tracing::Instrument as _; -/// Maximum number of messages per minute from single peer. -// TODO(#5453): current limit is way to high due to us sending lots of messages during sync. -const MAX_PEER_MSG_PER_MIN: usize = usize::MAX; /// How often to request peers from active peers. const REQUEST_PEERS_INTERVAL: time::Duration = time::Duration::seconds(60); @@ -620,25 +617,6 @@ impl PeerActor { .received_bytes_per_sec .store(received.bytes_per_min / 60, Ordering::Relaxed); conn.stats.sent_bytes_per_sec.store(sent.bytes_per_min / 60, Ordering::Relaxed); - // Whether the peer is considered abusive due to sending too many messages. - // I am allowing this for now because I assume `MAX_PEER_MSG_PER_MIN` will - // some day be less than `u64::MAX`. - let is_abusive = received.count_per_min > MAX_PEER_MSG_PER_MIN - || sent.count_per_min > MAX_PEER_MSG_PER_MIN; - if is_abusive { - tracing::trace!( - target: "network", - peer_id = ?conn.peer_info.id, - sent = sent.count_per_min, - recv = received.count_per_min, - "Banning peer for abuse"); - // TODO(MarX, #1586): Ban peer if we found them abusive. Fix issue with heavy - // network traffic that flags honest peers. - // Send ban signal to peer instance. It should send ban signal back and stop the instance. - // if let Some(connected_peer) = act.connected_peers.get(&peer_id1) { - // connected_peer.addr.do_send(PeerManagerRequest::BanPeer(ReasonForBan::Abusive)); - // } - } } }) }); From 2cec9c90d1ce0ca67809b30c85f0b08ad5950861 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Mon, 9 Jan 2023 16:50:45 +0100 Subject: [PATCH 152/188] chore: tokio patch version update to 1.18.4 (#8309) replaces #8304 dependabot made us aware of bugfixes and wanted to update to 1.20.3 Instead I propose we update only the patch version and keep major/minor. 1.18.x is a LTS version and it is what we are currently using. 1.18.4 contains the same fixes the bot wants. Updating further should be done as a conscious decision. `version = "~1.18"` means we want a fixed 1.18 minor version with the newest patch version. The old `"1.16.1"` is equivalent to `^1.16.1"` but in the lock file it was fixed to `"1.18.2"`. Since we check in the lock file and only update versions occasionally and consciously, I suggest we use `~` which is less confusing. --- Cargo.lock | 5 ++--- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 16bae69b109..3e94381baeb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5769,16 +5769,15 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.18.2" +version = "1.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395" +checksum = "8bfb875c82dc0a4f1f37a30e720dee181a2b3a06a428b0fc6873ea38d6407850" dependencies = [ "bytes", "libc", "memchr", "mio", "num_cpus", - "once_cell", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", diff --git a/Cargo.toml b/Cargo.toml index b47e1b272f3..a28f130a534 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -184,7 +184,7 @@ tempfile = "3.3" thiserror = "1.0.30" tikv-jemallocator = "0.5.0" time = "0.3.9" -tokio = { version = "1.16.1", features = ["fs", "macros", "net", "rt-multi-thread", "sync", "time"] } +tokio = { version = "~1.18", features = ["fs", "macros", "net", "rt-multi-thread", "sync", "time"] } tokio-stream = { version = "0.1.2", features = ["net"] } tokio-util = { version = "0.7.1", features = ["codec", "io"] } toml = "0.5.8" From cf294c940bd976bb26b224dab6933b339f9e861d Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Mon, 9 Jan 2023 17:07:16 +0100 Subject: [PATCH 153/188] doc: meta transactions (#8257) * doc: meta transactions gas and balance flow * document everything else about meta txs * address review comments * expand slightly more on complicated gas flow --- docs/SUMMARY.md | 1 + docs/architecture/how/meta-tx.md | 227 +++++++++++++++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 docs/architecture/how/meta-tx.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 6887a3b4579..fa59ff45782 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) + - [Meta transactions](./architecture/how/meta-tx.md) - [Serialization: Borsh, Json, ProtoBuf](./architecture/how/serialization.md) - [Proofs](./architecture/how/proofs.md) - [How neard will work](./architecture/next/README.md) diff --git a/docs/architecture/how/meta-tx.md b/docs/architecture/how/meta-tx.md new file mode 100644 index 00000000000..616fbe549be --- /dev/null +++ b/docs/architecture/how/meta-tx.md @@ -0,0 +1,227 @@ +# Meta Transactions + +[NEP-366](https://github.com/near/NEPs/pull/366) introduced the concept of meta +transactions to Near Protocol. This feature allows users to execute transactions +on NEAR without owning any gas or tokens. In order to enable this, users +construct and sign transactions off-chain. A third party (the relayer) is used +to cover the fees of submitting and executing the transaction. + +The MVP for meta transactions is currently in the stabilization process. +Naturally, the MVP has some limitations, which are discussed in separate +sections below. Future iterations have the potential to make meta transactions +more flexible. + +## Overview + +![Flow chart of meta +transactions](https://raw.githubusercontent.com/near/NEPs/003e589e6aba24fc70dd91c9cf7ef0007ca50735/neps/assets/nep-0366/NEP-DelegateAction.png) +_Credits for the diagram go to the NEP authors Alexander Fadeev and Egor +Uleyskiy._ + + +The graphic shows an example use case for meta transactions. Alice owns an +amount of the fungible token $FT. She wants to transfer some to John. To do +that, she needs to call `ft_transfer("john", 10)` on an account named `FT`. + +In technical terms, ownership of $FT is an entry in the `FT` contract's storage +that tracks the balance for her account. Note that this is on the application +layer and thus not a part of Near Protocol itself. But `FT` relies on the +protocol to verify that the `ft_transfer` call actually comes from Alice. The +contract code checks that `predecessor_id` is `"Alice"` and if that is the case +then the call is legitimately from Alice, as only she could create such a +receipt according to the Near Protocol specification. + +The problem is, Alice has no NEAR tokens. She only has a NEAR account that +someone else funded for her and she owns the private keys. She could create a +signed transaction that would make the `ft_transfer("john", 10)` call. But +validator nodes will not accept it, because she does not have the necessary Near +token balance to purchase the gas. + +With meta transactions, Alice can create a `DelegateAction`, which is very +similar to a transaction. It also contains a list of actions to execute and a +single receiver for those actions. She signs the `DelegateAction` and forwards +it (off-chain) to a relayer. The relayer wraps it in a transaction, of which the +relayer is the signer and therefore pays the gas costs. If the inner actions +have an attached token balance, this is also paid for by the relayer. + +On chain, the `SignedDelegateAction` inside the transaction is converted to an +action receipt with the same `SignedDelegateAction` on the relayer's shard. The +receipt is forwarded to the account from `Alice`, which will unpacked the +`SignedDelegateAction` and verify that it is signed by Alice with a valid Nonce +etc. If all checks are successful, a new action receipt with the inner actions +as body is sent to `FT`. There, the `ft_transfer` call finally executes. + +## Relayer + +Meta transactions only work with a relayer. This is an application layer +concept, implemented off-chain. Think of it as a server that accepts a +`SignedDelegateAction`, does some checks on them and eventually forwards it +inside a transaction to the blockchain network. + +A relayer may chose to offer their service for free but that's not going to be +financially viable long-term. But they could easily have the user pay using +other means, outside of Near blockchain. And with some tricks, it can even be +paid using fungible tokens on Near. + +In the example visualized above, the payment is done using $FT. Together with +the transfer to John, Alice also adds an action to pay 0.1 $FT to the relayer. +The relayer checks the content of the `SignedDelegateAction` and only processes +it if this payment is included as the first action. In this way, the relayer +will be paid in the same transaction as John. + +Note that the payment to the relayer is still not guaranteed. It could be that +Alice does not have sufficient $FT and the transfer fails. To mitigate, the +relayer should check the $FT balance of Alice first. + +Unfortunately, this still does not guarantee that the balance will be high +enough once the meta transaction executes. The relayer could waste NEAR gas +without compensation if Alice somehow reduces her $FT balance in just the right +moment. Some level of trust between the relayer and its user is therefore +required. + +The vision here is that there will be mostly application-specific relayers. A +general-purpose relayer is difficult to implement with just the MVP. See +limitations below. + +## Limitation: Single receiver + +A meta transaction, like a normal transaction, can only have one receiver. It's +possible to chain additional receipts afterwards. But crucially, there is no +atomicity guarantee and no roll-back mechanism. + +For normal transactions, this has been widely accepted as a fact for how Near +Protocol works. For meta transactions, there was a discussion around allowing +multiple receivers with separate lists of actions per receiver. While this could +be implemented, it would only create a false sense of atomicity. Since each +receiver would require a separate action receipt, there is no atomicity, the +same as with chains of receipts. + +Unfortunately, this means the trick to compensate the relayer in the same meta +transaction as the serviced actions only works if both happen on the same +receiver. In the example, both happen on `FT` and this case works well. But it +would not be possible to send $FT1 and pay the relayer in $FT2. Nor could one +deploy a contract code on `Alice` and pay in $FT in one meta transaction. It +would require two separate meta transactions to do that. Due to timing problems, +this again requires some level of trust between the relayer and Alice. + +A potential solution could involve linear dependencies between the action +receipts spawned from a single meta transaction. Only if the first succeeds, +will the second start executing,and so on. But this quickly gets too complicated +for the MVP and is therefore left open for future improvements. + +## Limitation: Accounts must be initialized + +Any transaction, including meta transactions, must use NONCEs to avoid replay +attacks. The NONCE must be chosen by Alice and compared to a NONCE stored on +chain. This NONCE is stored on the access key information that gets initialized +when creating an account. + +Implicit accounts don't need to be initialized in order to receive NEAR tokens, +or even $FT. This means users could own $FT but no NONCE is stored on chain for +them. This is problematic because we want to enable this exact use case with +meta transactions, but we have no NONCE to create a meta transaction. + +For the MVP, the proposed solution, or work-around, is that the relayer will +have to initialize the account of Alice once if it does not exist. Note that +this cannot be done as part of the meta transaction. Instead, it will be a +separate transaction that executes first. Only then can Alice even create a +`SignedDelegateAction` with a valid NONCE. + +Once again, some trust is required. If Alice wanted to abuse the relayer's +helpful service, she could ask the relayer to initialize her account. +Afterwards, she does not sign a meta transaction, instead she deletes her +account and cashes in the small token balance reserved for storage. If this +attack is repeated, a significant amount of tokens could be stolen from the +relayer. + +One partial solution suggested here was to remove the storage staking cost from +accounts. This means there is no financial incentive for Alice to delete her +account. But it does not solve the problem that the relayer has to pay for the +account creation and Alice can simply refuse to send a meta transaction +afterwards. In particular, anyone creating an account would have financial +incentive to let a relayer create it for them instead of paying out of the own +pockets. This would still be better than Alice stealing tokens but +fundamentally, there still needs to be some trust. + +An alternative solution discussed is to do NONCE checks on the relayer's access +key. This prevents replay attacks and allows implicit accounts to be used in +meta transactions without even initializing them. The downside is that meta +transactions share the same NONCE counter(s). That means, a meta transaction +sent by Bob may invalidate a meta transaction signed by Alice that was created +and sent to the relayer at the same time. Multiple access keys by the relayer +and coordination between relayer and user could potentially alleviate this +problem. But for the MVP, nothing along those lines has been approved. + +## Gas costs for meta transactions + +Meta transactions challenge the traditional ways of charging gas for actions. To +see why, let's first list the normal flow of gas, outside of meta transactions. + +1. Gas is purchased (by deducting NEAR from the transaction signer account), + when the transaction is converted into a receipt. The amount of gas is + implicitly defined by the content of the receipt. For function calls, the + caller decides explicitly how much gas is attached on top of the minimum + required amount. The NEAR token price per gas unit is dynamically adjusted on + the blockchain. In today's nearcore code base, this happens as part of + [`verify_and_charge_transaction`](https://github.com/near/nearcore/blob/4510472d69c059644bb2d2579837c6bd6d94f190/runtime/runtime/src/verifier.rs#L69) + which gets called in + [`process_transaction`](https://github.com/near/nearcore/blob/4510472d69c059644bb2d2579837c6bd6d94f190/runtime/runtime/src/lib.rs#L218). +2. For all actions listed inside the transaction, the `SEND` cost is burned + immediately. Depending on the condition `sender == receiver`, one of two + possible `SEND` costs is chosen. The `EXEC` cost is not burned, yet. But it + is implicitly part of the transaction cost. The third and last part of the + transaction cost is the gas attached to function calls. The attached gas is + also called prepaid gas. (Not to be confused with `total_prepaid_exec_fees` + which is the implicitly prepaid gas for `EXEC` action costs.) +3. On the receiver shard, `EXEC` costs are burned before the execution of an + action starts. Should the execution fail and abort the transaction, the + remaining gas will be refunded to the signer of the transaction. + +Ok, now adapt for meta transactions. Let's assume Alice uses a relayer to +execute actions with Bob as the receiver. + +1. The relayer purchases the gas for all inner actions, plus the gas for the + delegate action wrapping them. +2. The cost of sending the inner actions and the delegate action from the + relayer to Alice's shard will be burned immediately. The condition `relayer + == Alice` determines which action `SEND` cost is taken (`sir` or `not_sir`). + Let's call this `SEND(1)`. +3. On Alice's shard, the delegate action is executed, thus the `EXEC` gas cost + for it is burned. Alice sends the inner actions to Bob's shard. Therefore, we + burn the `SEND` fee again. This time based on `Alice == Bob` to figure out + `sir` or `not_sir`. Let's call this `SEND(2)`. +4. On Bob's shard, we execute all inner actions and burn their `EXEC` cost. + +Each of these steps should make sense and not be too surprising. But the +consequence is that the implicit costs paid at the relayer's shard are +`SEND(1)` + `SEND(2)` + `EXEC` for all inner actions plus `SEND(1)` + `EXEC` for +the delegate action. This might be surprising but hopefully with this +explanation it makes sense now! + +## Gas refunds in meta transactions + +Gas refund receipts work exactly like for normal transaction. At every step, the +difference between the pessimistic gas price and the actual gas price at that +height is computed and refunded. At the end of the last step, additionally all +remaining gas is also refunded at the original purchasing price. The gas refunds +go to the signer of the original transaction, in this case the relayer. This is +only fair, since the relayer also paid for it. + +## Balance refunds in meta transactions + +Unlike gas refunds, the protocol sends balance refunds to the predecessor +(a.k.a. sender) of the receipt. This makes sense, as we deposit the attached +balance to the receiver, who has to explicitly reattach a new balance to new +receipts they might spawn. + +In the world of meta transactions, this assumption is also challenged. If an +inner action requires an attached balance (for example a transfer action) then +this balance is taken from the relayer. + +The relayer can see what the cost will be before submitting the meta transaction +and agrees to pay for it, so nothing wrong so far. But what if the transaction +fails execution on Bob's shard? At this point, the predecessor is `Alice` and +therefore she receives the token balance refunded, not the relayer. This is +something relayer implementations must be aware of since there is a financial +incentive for Alice to submit meta transactions that have high balances attached +but will fail on Bob's shard. \ No newline at end of file From 3d93a90edf10c684085d4171600020b9652957e4 Mon Sep 17 00:00:00 2001 From: robin-near <111538878+robin-near@users.noreply.github.com> Date: Mon, 9 Jan 2023 09:18:21 -0800 Subject: [PATCH 154/188] [Debug UI] Last blocks: Fix rendering of genesis block (#7960) Genesis block is not stored in the normal way, so we condition on the default hash to query genesis block from the chain. --- chain/client/src/debug.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/chain/client/src/debug.rs b/chain/client/src/debug.rs index 3e47949d578..ca046633276 100644 --- a/chain/client/src/debug.rs +++ b/chain/client/src/debug.rs @@ -432,8 +432,16 @@ impl ClientActor { if blocks.contains_key(&block_hash) { continue; } - let block_header = self.client.chain.get_block_header(&block_hash)?; - let block = self.client.chain.get_block(&block_hash).ok(); + let block_header = if block_hash == CryptoHash::default() { + self.client.chain.genesis().clone() + } else { + self.client.chain.get_block_header(&block_hash)? + }; + let block = if block_hash == CryptoHash::default() { + Some(self.client.chain.genesis_block().clone()) + } else { + self.client.chain.get_block(&block_hash).ok() + }; let is_on_canonical_chain = match self.client.chain.get_block_by_height(block_header.height()) { Ok(block) => block.hash() == &block_hash, From 7ca69b8c3ee5375209ec47a32d35fe69bcc5a2c2 Mon Sep 17 00:00:00 2001 From: wacban Date: Mon, 9 Jan 2023 17:51:38 +0000 Subject: [PATCH 155/188] fix: fix wasmer build error (#8295) --- runtime/near-vm-runner/src/tests.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/runtime/near-vm-runner/src/tests.rs b/runtime/near-vm-runner/src/tests.rs index dad2ada3f31..d2285456ef7 100644 --- a/runtime/near-vm-runner/src/tests.rs +++ b/runtime/near-vm-runner/src/tests.rs @@ -70,8 +70,11 @@ fn prepaid_loading_gas(bytes: usize) -> u64 { /// memory must be configured for indices `0..WASM_PAGE_SIZE` to be valid. /// /// Panics if any of the tests fails. +#[allow(dead_code)] pub(crate) fn test_memory_like(factory: impl FnOnce() -> Box) { - const PAGE: u64 = wasmer_types::WASM_PAGE_SIZE as u64; + // Hardcoded to work around build errors when wasmer_types is not available. + // Set to value of wasmer_types::WASM_PAGE_SIZE + const PAGE: u64 = 0x10000; struct TestContext { mem: Box, From 4820b04aec53337bd3ed6fc00b274fa804ae6f5f Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Mon, 9 Jan 2023 19:51:16 +0100 Subject: [PATCH 156/188] fix: only add '-Ctarget-feature=+sse4.1,+sse4.2' flags for x86_64 (#8305) With #8284 we upgraded cargo version to `1.66`, so https://github.com/rust-lang/cargo/pull/11114 is available. This PR is the same as #7130, but we can safely merge it now. See #7650 for more context. In order to test it I've used the code from [`librocksdb-sys/build.rs`](https://github.com/rust-rocksdb/rust-rocksdb/blob/master/librocksdb-sys/build.rs#L111) as part of `neard/build.rs`: ``` fn main() { - if let Err(err) = try_main() { - eprintln!("{}", err); - std::process::exit(1); + let target_feature = std::env::var("CARGO_CFG_TARGET_FEATURE").unwrap(); + let target_features: Vec<_> = target_feature.split(',').collect(); + if target_features.contains(&"neon") { + panic!("FAIL: {target_feature}"); + } else { + panic!("OK: {target_feature}"); } } ``` It was was tested on M1 macbook, so I've changed the feature to `neon` (which is enabled by default), my `.cargo/config.toml` is as follows: ``` -[target.'cfg(target_arch = "x86_64")'] -rustflags = ["-Ctarget-feature=+sse4.1,+sse4.2", "-Cforce-unwind-tables=y"] +[target.'cfg(target_arch = "aarch64")'] +rustflags = ["-Ctarget-feature=-neon", "-Cforce-unwind-tables=y"] ``` `neon` was successfully removed when using `1.66` toolchain: ``` OK: crc,...,vh ``` while it was still present with `1.65`: ``` FAIL: aes,..,neon,...,vh ``` This proves that rustflags are passed as env variable to `build.rs` in `1.66`, which was not the case in `1.65` --- .cargo/config.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index d942d1b458a..5a4d9f339b8 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,4 +1,7 @@ [build] # We compile with `panic=abort`, so we need `-Cforce-unwind-tables=y` # to get a useful backtrace on panic. -rustflags = ["-Ctarget-feature=+sse4.1,+sse4.2", "-Cforce-unwind-tables=y"] \ No newline at end of file +rustflags = ["-Cforce-unwind-tables=y"] + +[target.'cfg(target_arch = "x86_64")'] +rustflags = ["-Ctarget-feature=+sse4.1,+sse4.2", "-Cforce-unwind-tables=y"] From 29acb587430eec43b230d37bc97364f378bf87e5 Mon Sep 17 00:00:00 2001 From: "R. N. West" <98110034+rnwst@users.noreply.github.com> Date: Tue, 10 Jan 2023 09:16:52 +0000 Subject: [PATCH 157/188] Update logo to invert colors when in dark mode (#8317) The logo in the README was hardly visible when in dark mode (black on dark gray). Add media query to invert colors when in dark mode (to produce white on dark gray). --- docs/images/logo.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/images/logo.svg b/docs/images/logo.svg index 18d1cf21c87..5ee78d1d7ee 100644 --- a/docs/images/logo.svg +++ b/docs/images/logo.svg @@ -1 +1 @@ - \ No newline at end of file + From c77970b593ed359bd64df81c0aec7f6ff4c83d40 Mon Sep 17 00:00:00 2001 From: Andrei Kashin Date: Tue, 10 Jan 2023 10:32:58 +0000 Subject: [PATCH 158/188] chore: update paperclip deps (#8319) This PR updates multiple paperclip crates: - paperclip: 0.7.0 -> 0.7.1 - paperclip-actix: 0.5.0 -> 0.5.1 - paperclip-core: 0.5.1 -> 0.5.2 - paperclip-macros: 0.6.0 -> 0.6.1 It is necessary to update serde_yaml to 0.9 for https://github.com/near/nearcore/pull/8310 Tested: passes local `cargo test` run --- Cargo.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3e94381baeb..b78479e1b53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2545,6 +2545,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" dependencies = [ "scopeguard", + "serde", ] [[package]] @@ -2555,7 +2556,6 @@ checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" dependencies = [ "autocfg", "scopeguard", - "serde", ] [[package]] @@ -4050,9 +4050,9 @@ dependencies = [ [[package]] name = "paperclip" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29edecb9b5de19fcdba789406bc39144de34c100e59151095aac1b97d2b4a25e" +checksum = "f399678683ec199ddca1dd54db957dd158dedb5fc90826eb2a7e6c0800c3a868" dependencies = [ "anyhow", "itertools", @@ -4060,8 +4060,8 @@ dependencies = [ "paperclip-actix", "paperclip-core", "paperclip-macros", - "parking_lot 0.12.1", - "semver 0.9.0", + "parking_lot 0.10.2", + "semver 1.0.9", "serde", "serde_derive", "serde_json", @@ -4072,9 +4072,9 @@ dependencies = [ [[package]] name = "paperclip-actix" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6acb344bfe7c8be8e140ad01dc2a8bc1a1b829014a29291174be64bc45f06e" +checksum = "29880bc57ef516c272d6fdd215ecaf96375d9a5dbac5412d849b9f9afd0d7298" dependencies = [ "actix-service", "actix-web", @@ -4082,21 +4082,21 @@ dependencies = [ "once_cell", "paperclip-core", "paperclip-macros", - "parking_lot 0.12.1", + "parking_lot 0.10.2", "serde_json", ] [[package]] name = "paperclip-core" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ba1b92909712a1186613a6ba6e1c48c59baba59672cff2b242e8e03e90101f" +checksum = "0bee516533b655ba63e41e788b49a2beb1139e1eebafb143e7cb56b8cabb5da1" dependencies = [ "actix-web", "mime", "once_cell", "paperclip-macros", - "parking_lot 0.12.1", + "parking_lot 0.10.2", "pin-project", "regex", "serde", @@ -4107,9 +4107,9 @@ dependencies = [ [[package]] name = "paperclip-macros" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "992e1f19f6a449c41e166a2336c86912eedc17f5167886ef09d601607d9be1f1" +checksum = "e89990be67318e3da29c92adb3377e0251a8eee10b4f91ff349cbf2da945e9d1" dependencies = [ "heck 0.4.0", "http", From 79a8de19ec849510b1e0978ca96535d42b3e918d Mon Sep 17 00:00:00 2001 From: mm-near <91919554+mm-near@users.noreply.github.com> Date: Tue, 10 Jan 2023 12:27:14 +0100 Subject: [PATCH 159/188] Added docs on how to run localnet on many machines (#8316) Co-authored-by: near-bulldozer[bot] <73298989+near-bulldozer[bot]@users.noreply.github.com> --- docs/SUMMARY.md | 1 + .../workflows/localnet_on_many_machines.md | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 docs/practices/workflows/localnet_on_many_machines.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index fa59ff45782..96d55f48252 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -33,6 +33,7 @@ - [Run a Node](./practices/workflows/run_a_node.md) - [Deploy a Contract](./practices/workflows/deploy_a_contract.md) - [Run Gas Estimations](./practices/workflows/gas_estimations.md) + - [Localnet on many machines](./practices/workflows/localnet_on_many_machines.md) - [Code Style](./practices/style.md) - [Documentation](./practices/docs.md) - [Tracking Issues](./practices/tracking_issues.md) diff --git a/docs/practices/workflows/localnet_on_many_machines.md b/docs/practices/workflows/localnet_on_many_machines.md new file mode 100644 index 00000000000..c6b322f0034 --- /dev/null +++ b/docs/practices/workflows/localnet_on_many_machines.md @@ -0,0 +1,67 @@ +# Running near localnet on 2 machines + +Quick instructions on how to run a localnet on 2 separate machines. + +## Setup + +* Machine1: "pc" - 192.168.0.1 +* Machine2: "laptop" - 192.168.0.2 + + +Run on both machines (make sure that they are using the same version of the code): +``` +cargo build -p neard +``` + +Then on machine1 run the command below, which will generate the configurations: + +``` +./target/debug/neard --home ~/.near/localnet_multi localnet --shards 3 --v 2 +``` + +This command has generated configuration for 3 shards and 2 validators (in directories ~/.near/localnet_multi/node0 and ~/.near/localnet_multi/node1). + +Now - copy the contents of node1 directory to the machine2 + +``` +rsync -r ~/.near/localnet_multi/node1 192.168.0.2:~/.near/localnet_multi/node1 +``` + +Now open the config.json file on both machines (node0/config.json on machine1 and node1/config.json on machine2) and: +* for rpc->addr and network->addr: + * Change the addres from 127.0.0.1 to 0.0.0.0 (this means that the port will be accessible from other computers) + * Remember the port numbers (they are generated randomly). +* Also write down the node0's node_key (it is probably: "ed25519:7PGseFbWxvYVgZ89K1uTJKYoKetWs7BJtbyXDzfbAcqX") + +## Running + +On machine1: +``` +./target/debug/neard --home ~/.near/localnet_multi/node0 run +``` + +On machine2: +``` +./target/debug/neard --home ~/.near/localnet_multi/node1 run --boot-nodes ed25519:7PGseFbWxvYVgZ89K1uTJKYoKetWs7BJtbyXDzfbAcqX@192.168.0.1:37665 +``` +The boot node address should be the IP of the machine1 + the network addr port **from the node0/config.json** + + +And if everything goes well, the nodes should communicate and start producing blocks. + +## Troubleshooting + +The debug mode is enabled by default, so you should be able to see what's going on by going to ``http://machine1:RPC_ADDR_PORT/debug`` + + +### If node keeps saying "waiting for peers" +See if you can see the machine1's debug page from machine2. (if not - there might be a firewall blocking the connection). + +Make sure that you set the right ports (it should use node0's NETWORK port) and that you set the ip add there to 0.0.0.0 + + +### Resetting the state +Simply stop both nodes, and remove the ``data`` subdirectory (~/.near/localnet_multi/node0/data and ~/.near/localnet_multi/node1/data). + +Then after restart, the nodes will start the blockchain from scratch. + From 52087fb24c8211f8c8770f31b6e3e427b7228799 Mon Sep 17 00:00:00 2001 From: wacban Date: Tue, 10 Jan 2023 15:26:21 +0000 Subject: [PATCH 160/188] feat: support hot/cold store in state viewer and add view-trie2 command (#8311) Added --store-temperature to view-state and implemented a new, pretty, output format for view-steate view-trie. --- .../src/peer_manager/peer_store/mod.rs | 8 +- core/store/src/lib.rs | 15 +++ core/store/src/trie/iterator.rs | 37 +++++- core/store/src/trie/mod.rs | 67 ++++++++-- neard/src/cli.rs | 6 +- tools/cold-store/src/cli.rs | 6 +- tools/state-viewer/Cargo.toml | 1 + tools/state-viewer/src/cli.rs | 116 +++++++++++++----- tools/state-viewer/src/commands.rs | 48 +++++--- 9 files changed, 236 insertions(+), 68 deletions(-) diff --git a/chain/network/src/peer_manager/peer_store/mod.rs b/chain/network/src/peer_manager/peer_store/mod.rs index 206d61c1bb8..9b61e3fec85 100644 --- a/chain/network/src/peer_manager/peer_store/mod.rs +++ b/chain/network/src/peer_manager/peer_store/mod.rs @@ -7,11 +7,13 @@ use anyhow::bail; use im::hashmap::Entry; use im::{HashMap, HashSet}; use near_primitives::network::PeerId; +use near_store::db::Database; use parking_lot::Mutex; use rand::seq::IteratorRandom; use rand::thread_rng; use std::net::SocketAddr; use std::ops::Not; +use std::sync::Arc; #[cfg(test)] mod testonly; @@ -592,12 +594,12 @@ impl PeerStore { } /// Public method used to iterate through all peers stored in the database. -pub fn iter_peers_from_store(store: near_store::NodeStorage, f: F) +pub fn iter_peers_from_store(db: Arc, f: F) where F: Fn((PeerId, KnownPeerState)), { - let db = store.into_inner(near_store::Temperature::Hot); - for x in crate::store::Store::from(db).list_peer_states().unwrap() { + let store = crate::store::Store::from(db); + for x in store.list_peer_states().unwrap() { f(x) } } diff --git a/core/store/src/lib.rs b/core/store/src/lib.rs index 783fa0d8449..4150cb8ab35 100644 --- a/core/store/src/lib.rs +++ b/core/store/src/lib.rs @@ -2,6 +2,7 @@ use std::fs::File; use std::io::{BufReader, BufWriter, Read, Write}; use std::marker::PhantomData; use std::path::Path; +use std::str::FromStr; use std::sync::Arc; use std::{fmt, io}; @@ -69,6 +70,20 @@ pub enum Temperature { Cold, } +impl FromStr for Temperature { + type Err = String; + + fn from_str(s: &str) -> Result { + let s = s.to_lowercase(); + match s.as_str() { + "hot" => Ok(Temperature::Hot), + #[cfg(feature = "cold_store")] + "cold" => Ok(Temperature::Cold), + _ => Err(String::from(format!("invalid temperature string {s}"))), + } + } +} + /// Node’s storage holding chain and all other necessary data. /// /// The eventual goal is to implement cold storage at which point this structure diff --git a/core/store/src/trie/iterator.rs b/core/store/src/trie/iterator.rs index 837b66db33d..0f7911e58a9 100644 --- a/core/store/src/trie/iterator.rs +++ b/core/store/src/trie/iterator.rs @@ -4,6 +4,7 @@ use crate::trie::nibble_slice::NibbleSlice; use crate::trie::{TrieNode, TrieNodeWithSize, ValueHandle}; use crate::{StorageError, Trie}; +/// Crumb is a piece of trie iteration state. It describes a node on the trail and processing status of that node. #[derive(Debug)] struct Crumb { node: TrieNodeWithSize, @@ -11,6 +12,9 @@ struct Crumb { prefix_boundary: bool, } +/// The status of processing of a node during trie iteration. +/// Each node is processed in the following order: +/// Entering -> At -> AtChild(0) -> ... -> AtChild(15) -> Exiting #[derive(Clone, Copy, Eq, PartialEq, Debug)] pub(crate) enum CrumbStatus { Entering, @@ -37,6 +41,14 @@ impl Crumb { } } +/// Trie iteration is done using a stack based approach. +/// There are two stacks that we track while iterating: the trail and the key_nibbles. +/// The trail is a vector of trie nodes on the path from root node to the node that is +/// currently being processed together with processing status - the Crumb. +/// The key_nibbles is a vector of nibbles from the state root not to the node that is +/// currently being processed. +/// The trail and the key_nibbles may have different lengths e.g. an extension trie node +/// will add only a single item to the trail but may add multiple nibbles to the key_nibbles. pub struct TrieIterator<'a> { trie: &'a Trie, trail: Vec, @@ -44,8 +56,12 @@ pub struct TrieIterator<'a> { /// If not `None`, a list of all nodes that the iterator has visited. visited_nodes: Option>>, + + /// Max depth of iteration. + max_depth: Option, } +/// The TrieTiem is a tuple of (key, value) of the node. pub type TrieItem = (Vec, Vec); /// Item extracted from Trie during depth first traversal, corresponding to some Trie node. @@ -59,12 +75,13 @@ pub struct TrieTraversalItem { impl<'a> TrieIterator<'a> { #![allow(clippy::new_ret_no_self)] /// Create a new iterator. - pub(super) fn new(trie: &'a Trie) -> Result { + pub(super) fn new(trie: &'a Trie, max_depth: Option) -> Result { let mut r = TrieIterator { trie, trail: Vec::with_capacity(8), key_nibbles: Vec::with_capacity(64), visited_nodes: None, + max_depth, }; r.descend_into_node(&trie.root)?; Ok(r) @@ -367,16 +384,24 @@ impl<'a> Iterator for TrieIterator<'a> { fn next(&mut self) -> Option { loop { let iter_step = self.iter_step()?; - match iter_step { - IterStep::PopTrail => { + + let can_process = match self.max_depth { + Some(max_depth) => self.key_nibbles.len() <= max_depth, + None => true, + }; + + match (iter_step, can_process) { + (IterStep::Continue, _) => {} + (IterStep::PopTrail, _) => { self.trail.pop(); } - IterStep::Descend(hash) => match self.descend_into_node(&hash) { + // Skip processing the node if can process is false. + (_, false) => {} + (IterStep::Descend(hash), true) => match self.descend_into_node(&hash) { Ok(_) => (), Err(err) => return Some(Err(err)), }, - IterStep::Continue => {} - IterStep::Value(hash) => { + (IterStep::Value(hash), true) => { return Some( self.trie .storage diff --git a/core/store/src/trie/mod.rs b/core/store/src/trie/mod.rs index 461066f7ad3..26753642ab7 100644 --- a/core/store/src/trie/mod.rs +++ b/core/store/src/trie/mod.rs @@ -1,6 +1,8 @@ use std::cell::RefCell; use std::collections::HashMap; +use std::fmt::Write; use std::io::Read; +use std::str; use borsh::{BorshDeserialize, BorshSerialize}; use byteorder::{LittleEndian, ReadBytesExt}; @@ -12,6 +14,7 @@ pub use near_primitives::shard_layout::ShardUId; use near_primitives::state::ValueRef; #[cfg(feature = "protocol_feature_flat_state")] use near_primitives::state_record::is_delayed_receipt_key; +use near_primitives::state_record::StateRecord; use near_primitives::trie_key::TrieKey; use near_primitives::types::{StateRoot, StateRootNode}; @@ -27,7 +30,6 @@ pub use crate::trie::trie_storage::{TrieCache, TrieCachingStorage, TrieDBStorage use crate::trie::trie_storage::{TrieMemoryPartialStorage, TrieRecordingStorage}; use crate::{FlatStateDelta, StorageError}; pub use near_primitives::types::TrieNodesCount; -use std::fmt::Write; mod config; mod insert_delete; @@ -556,6 +558,11 @@ pub struct ApplyStatePartResult { pub contract_codes: Vec, } +enum NodeOrValue { + Node(RawTrieNodeWithSize), + Value(std::sync::Arc<[u8]>), +} + impl Trie { pub const EMPTY_ROOT: StateRoot = StateRoot::new(); @@ -661,14 +668,15 @@ 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) { - Ok(Ok(_)) => { + Ok(NodeOrValue::Node(_)) => { let mut prefix: Vec = Vec::new(); self.print_recursive_internal(f, hash, max_depth, &mut "".to_string(), &mut prefix) .expect("write failed"); } - Ok(Err(value_bytes)) => { + Ok(NodeOrValue::Value(value_bytes)) => { writeln!( f, "Given node is a value. Len: {}, Data: {:?} ", @@ -683,6 +691,38 @@ impl Trie { }; } + // Prints the trie leaves starting from the state root node, up to max_depth depth. + // This method can only iterate starting from the root node and it only prints the + // leaf nodes but it shows output in more human friendly way. + pub fn print_recursive_leaves(&self, f: &mut dyn std::io::Write, max_depth: u32) { + let iter = match self.iter_with_max_depth(max_depth as usize) { + Ok(iter) => iter, + Err(err) => { + writeln!(f, "Error when getting the trie iterator: {}", err).expect("write failed"); + return; + } + }; + for node in iter { + let (key, value) = match node { + Ok((key, value)) => (key, value), + Err(err) => { + writeln!(f, "Failed to iterate node with error: {err}").expect("write failed"); + continue; + } + }; + + // Try to parse the key in UTF8 which works only for the simplest keys (e.g. account), + // or get whitespace padding instead. + let key_string = match str::from_utf8(&key) { + Ok(value) => String::from(value), + Err(_) => " ".repeat(key.len()), + }; + let state_record = StateRecord::from_raw_key_value(key.clone(), value); + + writeln!(f, "{} {state_record:?}", key_string).expect("write failed"); + } + } + // Converts the list of Nibbles to a readable string. fn nibbles_to_string(&self, prefix: &[u8]) -> String { let (chunks, remainder) = stdx::as_chunks::<2, _>(prefix); @@ -795,12 +835,14 @@ impl Trie { } // Similar to retrieve_raw_node but handles the case where there is a Value (and not a Node) in the database. - fn retrieve_raw_node_or_value( - &self, - hash: &CryptoHash, - ) -> Result>, StorageError> { + // 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)?; - Ok(RawTrieNodeWithSize::decode(&bytes).map_err(|_| bytes)) + match RawTrieNodeWithSize::decode(&bytes) { + Ok(node) => Ok(NodeOrValue::Node(node)), + Err(_) => Ok(NodeOrValue::Value(bytes)), + } } fn move_node_to_mutable( @@ -984,7 +1026,14 @@ impl Trie { } pub fn iter<'a>(&'a self) -> Result, StorageError> { - TrieIterator::new(self) + TrieIterator::new(self, None) + } + + pub fn iter_with_max_depth<'a>( + &'a self, + max_depth: usize, + ) -> Result, StorageError> { + TrieIterator::new(self, Some(max_depth)) } pub fn get_trie_nodes_count(&self) -> TrieNodesCount { diff --git a/neard/src/cli.rs b/neard/src/cli.rs index 5de2b803350..acc9101d212 100644 --- a/neard/src/cli.rs +++ b/neard/src/cli.rs @@ -94,7 +94,7 @@ impl NeardCmd { NeardSubCommand::StateViewer(cmd) => { let mode = if cmd.readwrite { Mode::ReadWrite } else { Mode::ReadOnly }; - cmd.subcmd.run(&home_dir, genesis_validation, mode); + cmd.subcmd.run(&home_dir, genesis_validation, mode, cmd.store_temperature); } NeardSubCommand::RecompressStorage(cmd) => { @@ -131,6 +131,10 @@ pub(super) struct StateViewerCommand { /// In case an operation needs to write to caches, a read-write mode may be needed. #[clap(long, short = 'w')] readwrite: bool, + /// What store temperature should the state viewer open. Allowed values are hot and cold but + /// cold is only available when cold_store feature is enabled. + #[clap(long, short = 't', default_value = "hot")] + store_temperature: near_store::Temperature, #[clap(subcommand)] subcmd: StateViewerSubCommand, } diff --git a/tools/cold-store/src/cli.rs b/tools/cold-store/src/cli.rs index 41cd3417b90..5d1231a3974 100644 --- a/tools/cold-store/src/cli.rs +++ b/tools/cold-store/src/cli.rs @@ -45,15 +45,15 @@ fn check_open(store: &NodeStorage) { fn print_heads(store: &NodeStorage) { println!( - "HOT HEAD is at {:?}", + "HOT HEAD is at {:#?}", store.get_store(Temperature::Hot).get_ser::(DBCol::BlockMisc, HEAD_KEY) ); println!( - "HOT FINAL HEAD is at {:?}", + "HOT FINAL HEAD is at {:#?}", store.get_store(Temperature::Hot).get_ser::(DBCol::BlockMisc, FINAL_HEAD_KEY) ); println!( - "COLD HEAD is at {:?}", + "COLD HEAD is at {:#?}", store.get_store(Temperature::Cold).get_ser::(DBCol::BlockMisc, HEAD_KEY) ); } diff --git a/tools/state-viewer/Cargo.toml b/tools/state-viewer/Cargo.toml index a457d692c9c..f91d2398613 100644 --- a/tools/state-viewer/Cargo.toml +++ b/tools/state-viewer/Cargo.toml @@ -47,3 +47,4 @@ nightly = [ ] nightly_protocol = ["nearcore/nightly_protocol"] protocol_feature_flat_state = ["nearcore/protocol_feature_flat_state"] +cold_store = ["near-store/cold_store"] diff --git a/tools/state-viewer/src/cli.rs b/tools/state-viewer/src/cli.rs index b3b0d428bdc..49c2120be0d 100644 --- a/tools/state-viewer/src/cli.rs +++ b/tools/state-viewer/src/cli.rs @@ -8,7 +8,7 @@ use near_primitives::account::id::AccountId; use near_primitives::hash::CryptoHash; use near_primitives::sharding::ChunkHash; use near_primitives::types::{BlockHeight, ShardId}; -use near_store::{Mode, Store}; +use near_store::{Mode, NodeStorage, Store, Temperature}; use nearcore::{load_config, NearConfig}; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -82,37 +82,52 @@ pub enum StateViewerSubCommand { } impl StateViewerSubCommand { - pub fn run(self, home_dir: &Path, genesis_validation: GenesisValidationMode, mode: Mode) { + pub fn run( + self, + home_dir: &Path, + genesis_validation: GenesisValidationMode, + mode: Mode, + temperature: Temperature, + ) { let near_config = load_config(home_dir, genesis_validation) .unwrap_or_else(|e| panic!("Error loading config: {:#}", e)); + + #[cfg(feature = "cold_store")] + let cold_store_config: Option<&near_store::StoreConfig> = + near_config.config.cold_store.as_ref(); + #[cfg(not(feature = "cold_store"))] + let cold_store_config: Option = None; + let store_opener = - near_store::NodeStorage::opener(home_dir, &near_config.config.store, None); - let store = store_opener.open_in_mode(mode).unwrap(); - let hot = store.get_store(near_store::Temperature::Hot); + NodeStorage::opener(home_dir, &near_config.config.store, cold_store_config); + + let storage = store_opener.open_in_mode(mode).unwrap(); + let store = storage.get_store(temperature); + let db = storage.into_inner(temperature); match self { - StateViewerSubCommand::Apply(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::ApplyChunk(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::ApplyRange(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::ApplyReceipt(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::ApplyTx(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::Chain(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::CheckBlock => check_block_chunk_existence(near_config, hot), - StateViewerSubCommand::Chunks(cmd) => cmd.run(near_config, hot), - StateViewerSubCommand::DumpAccountStorage(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::DumpCode(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::DumpState(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::DumpStateParts(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::DumpStateRedis(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::DumpTx(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::EpochInfo(cmd) => cmd.run(home_dir, near_config, hot), - StateViewerSubCommand::PartialChunks(cmd) => cmd.run(near_config, hot), - StateViewerSubCommand::Peers => peers(store), - StateViewerSubCommand::Receipts(cmd) => cmd.run(near_config, hot), - StateViewerSubCommand::Replay(cmd) => cmd.run(home_dir, near_config, hot), + StateViewerSubCommand::Apply(cmd) => cmd.run(home_dir, near_config, store), + StateViewerSubCommand::ApplyChunk(cmd) => cmd.run(home_dir, near_config, store), + StateViewerSubCommand::ApplyRange(cmd) => cmd.run(home_dir, near_config, store), + StateViewerSubCommand::ApplyReceipt(cmd) => cmd.run(home_dir, near_config, store), + StateViewerSubCommand::ApplyTx(cmd) => cmd.run(home_dir, near_config, store), + StateViewerSubCommand::Chain(cmd) => cmd.run(home_dir, near_config, store), + StateViewerSubCommand::CheckBlock => check_block_chunk_existence(near_config, store), + StateViewerSubCommand::Chunks(cmd) => cmd.run(near_config, store), + StateViewerSubCommand::DumpAccountStorage(cmd) => cmd.run(home_dir, near_config, store), + StateViewerSubCommand::DumpCode(cmd) => cmd.run(home_dir, near_config, store), + StateViewerSubCommand::DumpState(cmd) => cmd.run(home_dir, near_config, store), + StateViewerSubCommand::DumpStateParts(cmd) => cmd.run(home_dir, near_config, store), + StateViewerSubCommand::DumpStateRedis(cmd) => cmd.run(home_dir, near_config, store), + StateViewerSubCommand::DumpTx(cmd) => cmd.run(home_dir, near_config, store), + StateViewerSubCommand::EpochInfo(cmd) => cmd.run(home_dir, near_config, store), + StateViewerSubCommand::PartialChunks(cmd) => cmd.run(near_config, store), + StateViewerSubCommand::Peers => peers(db), + StateViewerSubCommand::Receipts(cmd) => cmd.run(near_config, store), + StateViewerSubCommand::Replay(cmd) => cmd.run(home_dir, near_config, store), StateViewerSubCommand::RocksDBStats(cmd) => cmd.run(store_opener.path()), - StateViewerSubCommand::State => state(home_dir, near_config, hot), - StateViewerSubCommand::ViewChain(cmd) => cmd.run(near_config, hot), - StateViewerSubCommand::ViewTrie(cmd) => cmd.run(hot), + StateViewerSubCommand::State => state(home_dir, near_config, store), + StateViewerSubCommand::ViewChain(cmd) => cmd.run(near_config, store), + StateViewerSubCommand::ViewTrie(cmd) => cmd.run(store), } } } @@ -494,14 +509,50 @@ impl ViewChainCmd { } } +pub enum ViewTrieFormat { + Full, + Pretty, +} + +impl std::str::FromStr for ViewTrieFormat { + type Err = String; + + fn from_str(s: &str) -> Result { + let s = s.to_lowercase(); + match s.as_str() { + "full" => Ok(ViewTrieFormat::Full), + "pretty" => Ok(ViewTrieFormat::Pretty), + _ => Err(String::from(format!("invalid view trie format string {s}"))), + } + } +} + #[derive(Parser)] pub struct ViewTrieCmd { + /// The format of the output. This can be either `full` or `pretty`. + /// The full format will print all the trie nodes and can be rooted anywhere in the trie. + /// The pretty format will only print leaf nodes and must be rooted in the state root but is more human friendly. + #[clap(long, default_value = "pretty")] + format: ViewTrieFormat, + /// The hash of the trie node. + /// For format=full this can be any node in the trie. + /// For format=pretty this must the state root node. + /// You can find the state root hash using the `view-state view-chain` command. #[clap(long)] hash: String, + /// The id of the shard, a number between [0-NUM_SHARDS). When looking for particular + /// account you will need to know on which shard it's located. #[clap(long)] shard_id: u32, + /// The current shard version based on the shard layout. + /// You can find the shard version by using the `view-state view-chain` command. + /// It's typically equal to 0 for single shard localnet or the most recent near_primitives::shard_layout::ShardLayout for prod. #[clap(long)] shard_version: u32, + /// The max depth of trie iteration. It's recommended to keep that value small, + /// otherwise the output may be really large. + /// For format=full this measures depth in terms of number of trie nodes. + /// For format=pretty this measures depth in terms of key nibbles. #[clap(long)] max_depth: u32, } @@ -509,6 +560,15 @@ pub struct ViewTrieCmd { impl ViewTrieCmd { pub fn run(self, store: Store) { let hash = CryptoHash::from_str(&self.hash).unwrap(); - view_trie(store, hash, self.shard_id, self.shard_version, self.max_depth).unwrap(); + + match self.format { + ViewTrieFormat::Full => { + view_trie(store, hash, self.shard_id, self.shard_version, self.max_depth).unwrap(); + } + ViewTrieFormat::Pretty => { + view_trie_leaves(store, hash, self.shard_id, self.shard_version, self.max_depth) + .unwrap(); + } + } } } diff --git a/tools/state-viewer/src/commands.rs b/tools/state-viewer/src/commands.rs index 8b2e16a1319..9b8b53264ae 100644 --- a/tools/state-viewer/src/commands.rs +++ b/tools/state-viewer/src/commands.rs @@ -7,11 +7,9 @@ use ansi_term::Color::Red; use near_chain::chain::collect_receipts_from_response; use near_chain::migrations::check_if_block_is_first_with_chunk_of_version; use near_chain::types::{ApplyTransactionResult, BlockHeaderInfo}; -use near_chain::Error; -use near_chain::{ChainStore, ChainStoreAccess, ChainStoreUpdate, RuntimeAdapter}; +use near_chain::{ChainStore, ChainStoreAccess, ChainStoreUpdate, Error, RuntimeAdapter}; use near_chain_configs::GenesisChangeConfig; -use near_epoch_manager::EpochManager; -use near_epoch_manager::EpochManagerAdapter; +use near_epoch_manager::{EpochManager, EpochManagerAdapter}; use near_network::iter_peers_from_store; use near_primitives::account::id::AccountId; use near_primitives::block::{Block, BlockHeader}; @@ -20,15 +18,11 @@ use near_primitives::shard_layout::ShardUId; use near_primitives::sharding::ChunkHash; use near_primitives::state_record::StateRecord; use near_primitives::trie_key::TrieKey; -use near_primitives::types::chunk_extra::ChunkExtra; -use near_primitives::types::{BlockHeight, ShardId, StateRoot}; +use near_primitives::types::{chunk_extra::ChunkExtra, BlockHeight, ShardId, StateRoot}; use near_primitives_core::types::Gas; +use near_store::db::Database; use near_store::test_utils::create_test_store; -use near_store::Trie; -use near_store::TrieCache; -use near_store::TrieCachingStorage; -use near_store::TrieConfig; -use near_store::{NodeStorage, Store}; +use near_store::{Store, Trie, TrieCache, TrieCachingStorage, TrieConfig}; use nearcore::{NearConfig, NightshadeRuntime}; use node_runtime::adapter::ViewRuntimeAdapter; use serde_json::json; @@ -415,8 +409,8 @@ pub(crate) fn get_receipt(receipt_id: CryptoHash, near_config: NearConfig, store println!("Receipt: {:#?}", receipt); } -pub(crate) fn peers(store: NodeStorage) { - iter_peers_from_store(store, |(peer_id, peer_info)| { +pub(crate) fn peers(db: Arc) { + iter_peers_from_store(db, |(peer_id, peer_info)| { println!("{} {:?}", peer_id, peer_info); }) } @@ -668,6 +662,8 @@ pub(crate) fn view_chain( println!("block height {}, hash {}", block.header().height(), block.hash()); } + println!("shard layout {:#?}", shard_layout); + for (shard_id, chunk_extra) in chunk_extras { println!("shard {}, chunk extra: {:#?}", shard_id, chunk_extra); } @@ -734,6 +730,14 @@ pub(crate) fn print_epoch_info( ); } +fn get_trie(store: Store, hash: CryptoHash, shard_id: u32, shard_version: u32) -> Trie { + let shard_uid = ShardUId { version: shard_version, shard_id }; + let trie_config: TrieConfig = Default::default(); + let shard_cache = TrieCache::new(&trie_config, shard_uid, true); + let trie_storage = TrieCachingStorage::new(store, shard_cache, shard_uid, true, None); + Trie::new(Box::new(trie_storage), hash, None) +} + pub(crate) fn view_trie( store: Store, hash: CryptoHash, @@ -741,15 +745,23 @@ pub(crate) fn view_trie( shard_version: u32, max_depth: u32, ) -> anyhow::Result<()> { - let shard_uid = ShardUId { version: shard_version, shard_id }; - let trie_config: TrieConfig = Default::default(); - let shard_cache = TrieCache::new(&trie_config, shard_uid, true); - let trie_storage = TrieCachingStorage::new(store, shard_cache, shard_uid, true, None); - let trie = Trie::new(Box::new(trie_storage), Trie::EMPTY_ROOT, None); + let trie = get_trie(store, hash, shard_id, shard_version); trie.print_recursive(&mut std::io::stdout().lock(), &hash, max_depth); Ok(()) } +pub(crate) fn view_trie_leaves( + store: Store, + state_root_hash: CryptoHash, + shard_id: u32, + shard_version: u32, + max_depth: u32, +) -> anyhow::Result<()> { + let trie = get_trie(store, state_root_hash, shard_id, shard_version); + trie.print_recursive_leaves(&mut std::io::stdout().lock(), max_depth); + Ok(()) +} + #[allow(unused)] enum LoadTrieMode { /// Load latest state From 35f60544888a964972d2911aa972c244aebbb922 Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Wed, 11 Jan 2023 08:58:26 +0100 Subject: [PATCH 161/188] feat: enable clippy::correctness as part of CI (#8299) This enables clippy `clippy::correctness` checks as part of CI as discussed [here](https://github.com/near/nearcore/pull/8291#issuecomment-1372547523). It is added as part of `sanity check` step for consistency, since that is where `cargo check` is executed. This fixes the following issues in order to pass the added checks: * noop `.clone()` * this loop never actually loops * `0` is the minimum value for this type, the case where the two sides are not equal never occurs, consider using `self.burst == 0` instead This PR is a part of #8145 --- .buildkite/pipeline.yml | 2 + chain/chain/src/store_validator/validate.rs | 44 ++++++++++----------- chain/chunks/src/lib.rs | 3 ++ chain/network/src/concurrency/rate.rs | 2 +- core/store/src/trie/mod.rs | 2 +- scripts/run_clippy.sh | 5 ++- tools/state-viewer/src/apply_chunk.rs | 6 +-- 7 files changed, 36 insertions(+), 28 deletions(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 68f0f3a60bc..11710306c52 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -90,6 +90,8 @@ steps: ./scripts/formatting --check + ./scripts/run_clippy.sh + rm target/rpc_errors_schema.json cargo check -p near-jsonrpc --features dump_errors_schema if ! git --no-pager diff --no-index chain/jsonrpc/res/rpc_errors_schema.json target/rpc_errors_schema.json; then diff --git a/chain/chain/src/store_validator/validate.rs b/chain/chain/src/store_validator/validate.rs index e24e4eab69b..7b15a332e96 100644 --- a/chain/chain/src/store_validator/validate.rs +++ b/chain/chain/src/store_validator/validate.rs @@ -879,8 +879,8 @@ pub(crate) fn state_part_header_exists( pub(crate) fn block_height_cmp_tail_final( sv: &mut StoreValidator, ) -> Result<(), StoreValidatorError> { - if sv.inner.block_heights_less_tail.len() >= 2 { - let len = sv.inner.block_heights_less_tail.len(); + let len = sv.inner.block_heights_less_tail.len(); + if len >= 2 { let blocks = &sv.inner.block_heights_less_tail; err!("Found {:?} Blocks with height lower than Tail, {:?}", len, blocks) } @@ -888,37 +888,37 @@ pub(crate) fn block_height_cmp_tail_final( } pub(crate) fn tx_refcount_final(sv: &mut StoreValidator) -> Result<(), StoreValidatorError> { - let len = sv.inner.tx_refcount.len(); - if len > 0 { - for tx_refcount in sv.inner.tx_refcount.iter() { - err!("Found {:?} Txs that are not counted, i.e. {:?}", len, tx_refcount); - } + if let Some(tx_refcount) = sv.inner.tx_refcount.iter().next() { + err!( + "Found {:?} Txs that are not counted, e.g. {:?}", + sv.inner.tx_refcount.len(), + tx_refcount + ); } Ok(()) } pub(crate) fn receipt_refcount_final(sv: &mut StoreValidator) -> Result<(), StoreValidatorError> { - let len = sv.inner.receipt_refcount.len(); - if len > 0 { - for receipt_refcount in sv.inner.receipt_refcount.iter() { - err!("Found {:?} receipts that are not counted, i.e. {:?}", len, receipt_refcount); - } + if let Some(receipt_refcount) = sv.inner.receipt_refcount.iter().next() { + err!( + "Found {:?} receipts that are not counted, e.g. {:?}", + sv.inner.receipt_refcount.len(), + receipt_refcount + ); } Ok(()) } pub(crate) fn block_refcount_final(sv: &mut StoreValidator) -> Result<(), StoreValidatorError> { - if sv.inner.block_refcount.len() > 1 { - let len = sv.inner.block_refcount.len(); - for block_refcount in sv.inner.block_refcount.iter() { - err!("Found {:?} Blocks that are not counted, i.e. {:?}", len, block_refcount); - } + if let Some(block_refcount) = sv.inner.block_refcount.iter().next() { + err!( + "Found {:?} Blocks that are not counted, e.g. {:?}", + sv.inner.block_refcount.len(), + block_refcount + ); } - if sv.inner.genesis_blocks.len() > 1 { - let len = sv.inner.genesis_blocks.len(); - for tail_block in sv.inner.genesis_blocks.iter() { - err!("Found {:?} Genesis Blocks, i.e. {:?}", len, tail_block); - } + if let Some(tail_block) = sv.inner.genesis_blocks.first() { + err!("Found {:?} Genesis Blocks, e.g. {:?}", sv.inner.genesis_blocks.len(), tail_block); } Ok(()) } diff --git a/chain/chunks/src/lib.rs b/chain/chunks/src/lib.rs index 8ccacef1d48..18dec3ca689 100644 --- a/chain/chunks/src/lib.rs +++ b/chain/chunks/src/lib.rs @@ -612,6 +612,9 @@ impl ShardsManager { continue; } + // This is false positive, similar to what was reported here: + // https://github.com/rust-lang/rust-clippy/issues/5940 + #[allow(clippy::if_same_then_else)] let fetch_from = if request_from_archival { shard_representative_target.clone() } else if we_own_part { diff --git a/chain/network/src/concurrency/rate.rs b/chain/network/src/concurrency/rate.rs index b03d9a4e313..565c032a87f 100644 --- a/chain/network/src/concurrency/rate.rs +++ b/chain/network/src/concurrency/rate.rs @@ -19,7 +19,7 @@ impl Limit { if self.qps <= 0. { anyhow::bail!("qps has to be >0"); } - if self.burst <= 0 { + if self.burst == 0 { anyhow::bail!("burst has to be >0"); } Ok(()) diff --git a/core/store/src/trie/mod.rs b/core/store/src/trie/mod.rs index 26753642ab7..abd2e5ef353 100644 --- a/core/store/src/trie/mod.rs +++ b/core/store/src/trie/mod.rs @@ -950,7 +950,7 @@ impl Trie { key: &[u8], mode: KeyLookupMode, ) -> Result, StorageError> { - let key_nibbles = NibbleSlice::new(key.clone()); + let key_nibbles = NibbleSlice::new(key); let result = self.lookup(key_nibbles); // For now, to test correctness, flat storage does double the work and diff --git a/scripts/run_clippy.sh b/scripts/run_clippy.sh index ae79f24fb8b..31b7eee9ab6 100755 --- a/scripts/run_clippy.sh +++ b/scripts/run_clippy.sh @@ -1,3 +1,6 @@ #!/usr/bin/env bash -cargo clippy --all -- -A clippy::type-complexity -A clippy::needless-pass-by-value -A clippy::while-let-loop -A clippy::too-many-arguments -A clippy::unit_arg -A clippy::if_same_then_else -A clippy::collapsible_if -A clippy::useless-let-if-seq -A clippy::map-entry -D warnings -A clippy::implicit-hasher -A clippy::ptr-arg -A renamed-and-removed-lints -A clippy::needless-range-loop -A clippy::unused_io_amount -A clippy::wrong-self-convention +# clippy adoption is in progress, see https://github.com/near/nearcore/issues/8145 +cargo clippy -- \ + -A clippy::all \ + -D clippy::correctness diff --git a/tools/state-viewer/src/apply_chunk.rs b/tools/state-viewer/src/apply_chunk.rs index 99a3ff783af..ed518ec38b6 100644 --- a/tools/state-viewer/src/apply_chunk.rs +++ b/tools/state-viewer/src/apply_chunk.rs @@ -85,7 +85,7 @@ pub(crate) fn apply_chunk( let shard_id = chunk.shard_id(); let prev_state_root = chunk.prev_state_root(); - let transactions = chunk.transactions().clone(); + let transactions = chunk.transactions(); let prev_block = chain_store.get_block(&prev_block_hash).context("Failed getting chunk's prev block")?; let prev_height_included = prev_block.chunks()[shard_id as usize].height_included(); @@ -126,7 +126,7 @@ pub(crate) fn apply_chunk( &hash("nonsense block hash for testing purposes".as_ref()), ), &receipts, - &transactions, + transactions, chunk_header.validator_proposals(), gas_price, chunk_header.gas_limit(), @@ -380,7 +380,7 @@ fn apply_receipt_in_chunk( println!("Applying chunk at height {} in shard {}. Equivalent command (which will run faster than apply_receipt):\nview_state apply_chunk --chunk_hash {}\n", height, shard_id, chunk_hash.0); let (apply_result, gas_limit) = - apply_chunk(runtime.clone(), chain_store, chunk_hash.clone(), None, None)?; + apply_chunk(runtime, chain_store, chunk_hash.clone(), None, None)?; let chunk_extra = crate::commands::resulting_chunk_extra(&apply_result, gas_limit); println!("resulting chunk extra:\n{:?}", chunk_extra); results.push(apply_result); From c47320f74512cb56ea32ce49ad3b624730d4049a Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Wed, 11 Jan 2023 11:10:46 +0100 Subject: [PATCH 162/188] feat: enable clippy suspicious as part of CI (#8330) Enable `clippy::suspicious` checks as part of CI. Part of #8145. --- integration-tests/src/test_helpers.rs | 4 ++-- scripts/run_clippy.sh | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/integration-tests/src/test_helpers.rs b/integration-tests/src/test_helpers.rs index e060009830f..d2a5a90d423 100644 --- a/integration-tests/src/test_helpers.rs +++ b/integration-tests/src/test_helpers.rs @@ -21,9 +21,9 @@ pub fn check_result(output: Output) -> Result { if result.is_empty() { result = String::from_utf8_lossy(output.stderr.as_slice()); } - return Err(result.to_owned().to_string()); + return Err(result.into_owned()); } - Ok(result.to_owned().to_string()) + Ok(result.into_owned()) } pub fn wait(mut f: F, check_interval_ms: u64, max_wait_ms: u64) diff --git a/scripts/run_clippy.sh b/scripts/run_clippy.sh index 31b7eee9ab6..d2b80f5c38e 100755 --- a/scripts/run_clippy.sh +++ b/scripts/run_clippy.sh @@ -2,5 +2,6 @@ # clippy adoption is in progress, see https://github.com/near/nearcore/issues/8145 cargo clippy -- \ -A clippy::all \ - -D clippy::correctness + -D clippy::correctness \ + -D clippy::suspicious From 8da4503b61645c9a6c989e31904ec335fa888641 Mon Sep 17 00:00:00 2001 From: Andrei Kashin Date: Wed, 11 Jan 2023 11:52:51 +0000 Subject: [PATCH 163/188] refactor: Migrate parameter configs to YAML (#8310) This is a refactoring PR to enable https://github.com/near/nearcore/issues/8264. For full motivation see https://github.com/near/nearcore/issues/8264#issuecomment-1373628858 This PR only changes the disk format and parsing logic but doesn't change the in-memory representation, so it should not lead to any visible behaviour changes. The next step in this refactoring would be to investigate getting rid of `serde_json::Value` in favor of `serde_yaml::Value` or even better a specialized struct with much more restricted types to catch invalid config errors earlier. --- Cargo.lock | 11 +- Cargo.toml | 1 + core/primitives/Cargo.toml | 1 + core/primitives/res/README.md | 10 +- core/primitives/res/runtime_configs/42.txt | 1 - core/primitives/res/runtime_configs/42.yaml | 1 + core/primitives/res/runtime_configs/48.txt | 8 -- core/primitives/res/runtime_configs/48.yaml | 8 ++ core/primitives/res/runtime_configs/49.txt | 2 - core/primitives/res/runtime_configs/49.yaml | 2 + core/primitives/res/runtime_configs/50.txt | 1 - core/primitives/res/runtime_configs/50.yaml | 1 + core/primitives/res/runtime_configs/52.txt | 2 - core/primitives/res/runtime_configs/52.yaml | 2 + core/primitives/res/runtime_configs/53.txt | 4 - core/primitives/res/runtime_configs/53.yaml | 4 + core/primitives/res/runtime_configs/57.txt | 1 - core/primitives/res/runtime_configs/57.yaml | 1 + .../{parameters.txt => parameters.yaml} | 3 +- ...rs_testnet.txt => parameters_testnet.yaml} | 2 +- core/primitives/src/runtime/config_store.rs | 18 +-- .../primitives/src/runtime/parameter_table.rs | 129 +++++++++--------- docs/architecture/gas/parameter_definition.md | 6 +- 23 files changed, 112 insertions(+), 107 deletions(-) delete mode 100644 core/primitives/res/runtime_configs/42.txt create mode 100644 core/primitives/res/runtime_configs/42.yaml delete mode 100644 core/primitives/res/runtime_configs/48.txt create mode 100644 core/primitives/res/runtime_configs/48.yaml delete mode 100644 core/primitives/res/runtime_configs/49.txt create mode 100644 core/primitives/res/runtime_configs/49.yaml delete mode 100644 core/primitives/res/runtime_configs/50.txt create mode 100644 core/primitives/res/runtime_configs/50.yaml delete mode 100644 core/primitives/res/runtime_configs/52.txt create mode 100644 core/primitives/res/runtime_configs/52.yaml delete mode 100644 core/primitives/res/runtime_configs/53.txt create mode 100644 core/primitives/res/runtime_configs/53.yaml delete mode 100644 core/primitives/res/runtime_configs/57.txt create mode 100644 core/primitives/res/runtime_configs/57.yaml rename core/primitives/res/runtime_configs/{parameters.txt => parameters.yaml} (99%) rename core/primitives/res/runtime_configs/{parameters_testnet.txt => parameters_testnet.yaml} (99%) diff --git a/Cargo.lock b/Cargo.lock index b78479e1b53..62d2c0ce5e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2252,12 +2252,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.8.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", - "hashbrown 0.11.2", + "hashbrown 0.12.1", "serde", ] @@ -3396,6 +3396,7 @@ dependencies = [ "reed-solomon-erasure", "serde", "serde_json", + "serde_yaml", "smart-default", "strum", "thiserror", @@ -5229,9 +5230,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.8.24" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707d15895415db6628332b737c838b88c598522e4dc70647e59b72312924aebc" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ "indexmap", "ryu", diff --git a/Cargo.toml b/Cargo.toml index a28f130a534..69bad9d1353 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -170,6 +170,7 @@ serde = { version = "1.0.136", features = ["alloc", "derive", "rc"] } serde_ignored = "0.1" serde_json = "1.0.68" serde_repr = "0.1.8" +serde_yaml = "0.8.26" sha2 = "0.10" sha3 = "0.10" shell-escape = "0.1.5" diff --git a/core/primitives/Cargo.toml b/core/primitives/Cargo.toml index 1a249b991b1..2a6f756c41a 100644 --- a/core/primitives/Cargo.toml +++ b/core/primitives/Cargo.toml @@ -29,6 +29,7 @@ rand.workspace = true reed-solomon-erasure.workspace = true serde.workspace = true serde_json.workspace = true +serde_yaml.workspace = true smart-default.workspace = true stdx.workspace = true strum.workspace = true diff --git a/core/primitives/res/README.md b/core/primitives/res/README.md index f532fa0d16d..e0a824c5979 100644 --- a/core/primitives/res/README.md +++ b/core/primitives/res/README.md @@ -4,18 +4,18 @@ Stores resource data which is part of the protocol stable enough to be moved out ### `runtime_configs` -All parameter value to configure the runtime are defined in `parameters.txt`. +All parameter value to configure the runtime are defined in `parameters.yaml`. Parameters added or changed in protocol upgrades are defined in differential -config files with a naming scheme like `V.txt`, where `V` is the new version. +config files with a naming scheme like `V.yaml`, where `V` is the new version. The content of the base configuration file is one flat list of typed keys and untyped values. Key names are defined in `core/primitives-core/src/parameter.rs`. The format of the differential files is slightly different. Inserting new -parameters uses the same syntax as the base configuration file: `key: value`. -Parameters that change are specified like this: `key: old_value -> new_value`. +parameters uses the following syntax: `key: { new: value }`. +Parameters that change are specified like this: `key: { old: old_value, new: new_value }`. Removing a previously defined parameter for a new version is done as follows: -`key: old_value ->`. This causes the parameter value to be undefined in newer +`key: { old: old_value }`. This causes the parameter value to be undefined in newer versions which generally means the default value is used to fill in the `RuntimeConfig` object. diff --git a/core/primitives/res/runtime_configs/42.txt b/core/primitives/res/runtime_configs/42.txt deleted file mode 100644 index 875589ae8f4..00000000000 --- a/core/primitives/res/runtime_configs/42.txt +++ /dev/null @@ -1 +0,0 @@ -storage_amount_per_byte: 100_000_000_000_000_000_000 -> 10_000_000_000_000_000_000 diff --git a/core/primitives/res/runtime_configs/42.yaml b/core/primitives/res/runtime_configs/42.yaml new file mode 100644 index 00000000000..8f79cc5cd19 --- /dev/null +++ b/core/primitives/res/runtime_configs/42.yaml @@ -0,0 +1 @@ +storage_amount_per_byte: { old: 100_000_000_000_000_000_000, new: 10_000_000_000_000_000_000 } diff --git a/core/primitives/res/runtime_configs/48.txt b/core/primitives/res/runtime_configs/48.txt deleted file mode 100644 index 830213d8917..00000000000 --- a/core/primitives/res/runtime_configs/48.txt +++ /dev/null @@ -1,8 +0,0 @@ -wasm_regular_op_cost: 3_856_371 -> 2_207_874 -wasm_ecrecover_base: 3_365_369_625_000 -> 278_821_988_457 -data_receipt_creation_base_send_sir: 4_697_339_419_375 -> 36_486_732_312 -data_receipt_creation_base_send_not_sir: 4_697_339_419_375 -> 36_486_732_312 -data_receipt_creation_base_execution: 4_697_339_419_375 -> 36_486_732_312 -data_receipt_creation_per_byte_send_sir: 59_357_464 -> 17_212_011 -data_receipt_creation_per_byte_send_not_sir: 59_357_464 -> 17_212_011 -data_receipt_creation_per_byte_execution: 59_357_464 -> 17_212_011 diff --git a/core/primitives/res/runtime_configs/48.yaml b/core/primitives/res/runtime_configs/48.yaml new file mode 100644 index 00000000000..bb6807967a3 --- /dev/null +++ b/core/primitives/res/runtime_configs/48.yaml @@ -0,0 +1,8 @@ +wasm_regular_op_cost: { old: 3_856_371, new: 2_207_874 } +wasm_ecrecover_base: { old: 3_365_369_625_000, new: 278_821_988_457 } +data_receipt_creation_base_send_sir: { old: 4_697_339_419_375, new: 36_486_732_312 } +data_receipt_creation_base_send_not_sir: { old: 4_697_339_419_375, new: 36_486_732_312 } +data_receipt_creation_base_execution: { old: 4_697_339_419_375, new: 36_486_732_312 } +data_receipt_creation_per_byte_send_sir: { old: 59_357_464, new: 17_212_011 } +data_receipt_creation_per_byte_send_not_sir: { old: 59_357_464, new: 17_212_011 } +data_receipt_creation_per_byte_execution: { old: 59_357_464, new: 17_212_011 } diff --git a/core/primitives/res/runtime_configs/49.txt b/core/primitives/res/runtime_configs/49.txt deleted file mode 100644 index 141d0c82aa1..00000000000 --- a/core/primitives/res/runtime_configs/49.txt +++ /dev/null @@ -1,2 +0,0 @@ -wasm_regular_op_cost: 2_207_874 -> 822_756 -max_functions_number_per_contract: 10_000 diff --git a/core/primitives/res/runtime_configs/49.yaml b/core/primitives/res/runtime_configs/49.yaml new file mode 100644 index 00000000000..c88d0db7997 --- /dev/null +++ b/core/primitives/res/runtime_configs/49.yaml @@ -0,0 +1,2 @@ +wasm_regular_op_cost: { old: 2_207_874, new: 822_756 } +max_functions_number_per_contract: { new: 10_000 } diff --git a/core/primitives/res/runtime_configs/50.txt b/core/primitives/res/runtime_configs/50.txt deleted file mode 100644 index 807d57581dc..00000000000 --- a/core/primitives/res/runtime_configs/50.txt +++ /dev/null @@ -1 +0,0 @@ -stack_limiter_version: 0 -> 1 diff --git a/core/primitives/res/runtime_configs/50.yaml b/core/primitives/res/runtime_configs/50.yaml new file mode 100644 index 00000000000..9c266cff266 --- /dev/null +++ b/core/primitives/res/runtime_configs/50.yaml @@ -0,0 +1 @@ +stack_limiter_version: { old: 0, new: 1 } diff --git a/core/primitives/res/runtime_configs/52.txt b/core/primitives/res/runtime_configs/52.txt deleted file mode 100644 index e5f54a0351a..00000000000 --- a/core/primitives/res/runtime_configs/52.txt +++ /dev/null @@ -1,2 +0,0 @@ -max_gas_burnt: 200_000_000_000_000 -> 300_000_000_000_000 -max_gas_burnt_view: 200_000_000_000_000 -> 300_000_000_000_000 diff --git a/core/primitives/res/runtime_configs/52.yaml b/core/primitives/res/runtime_configs/52.yaml new file mode 100644 index 00000000000..cbd07cdaf62 --- /dev/null +++ b/core/primitives/res/runtime_configs/52.yaml @@ -0,0 +1,2 @@ +max_gas_burnt: { old: 200_000_000_000_000, new: 300_000_000_000_000 } +max_gas_burnt_view: { old: 200_000_000_000_000, new: 300_000_000_000_000 } diff --git a/core/primitives/res/runtime_configs/53.txt b/core/primitives/res/runtime_configs/53.txt deleted file mode 100644 index 13bd07041f3..00000000000 --- a/core/primitives/res/runtime_configs/53.txt +++ /dev/null @@ -1,4 +0,0 @@ -action_deploy_contract_per_byte_execution: 6_812_999 -> 64_572_944 -wasmer2_stack_limit: 204_800 -max_length_storage_key: 4_194_304 -> 2_048 -max_locals_per_contract: 1_000_000 diff --git a/core/primitives/res/runtime_configs/53.yaml b/core/primitives/res/runtime_configs/53.yaml new file mode 100644 index 00000000000..e322b4507cf --- /dev/null +++ b/core/primitives/res/runtime_configs/53.yaml @@ -0,0 +1,4 @@ +action_deploy_contract_per_byte_execution: { old: 6_812_999, new: 64_572_944 } +wasmer2_stack_limit: { new: 204_800 } +max_length_storage_key: { old: 4_194_304, new: 2_048 } +max_locals_per_contract: { new: 1_000_000 } diff --git a/core/primitives/res/runtime_configs/57.txt b/core/primitives/res/runtime_configs/57.txt deleted file mode 100644 index ad517496ca9..00000000000 --- a/core/primitives/res/runtime_configs/57.txt +++ /dev/null @@ -1 +0,0 @@ -account_id_validity_rules_version: 0 -> 1 diff --git a/core/primitives/res/runtime_configs/57.yaml b/core/primitives/res/runtime_configs/57.yaml new file mode 100644 index 00000000000..436ee3e0807 --- /dev/null +++ b/core/primitives/res/runtime_configs/57.yaml @@ -0,0 +1 @@ +account_id_validity_rules_version: { old: 0, new: 1 } diff --git a/core/primitives/res/runtime_configs/parameters.txt b/core/primitives/res/runtime_configs/parameters.yaml similarity index 99% rename from core/primitives/res/runtime_configs/parameters.txt rename to core/primitives/res/runtime_configs/parameters.yaml index c4ab8633ab6..f6224056621 100644 --- a/core/primitives/res/runtime_configs/parameters.txt +++ b/core/primitives/res/runtime_configs/parameters.yaml @@ -10,7 +10,7 @@ pessimistic_gas_price_inflation_denominator: 100 # Account creation config min_allowed_top_level_account_length: 32 -registrar_account_id: registrar +registrar_account_id: "registrar" # Storage usage config storage_amount_per_byte: 100_000_000_000_000_000_000 @@ -156,5 +156,4 @@ 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 -stack_limiter_version: 0 account_id_validity_rules_version: 0 diff --git a/core/primitives/res/runtime_configs/parameters_testnet.txt b/core/primitives/res/runtime_configs/parameters_testnet.yaml similarity index 99% rename from core/primitives/res/runtime_configs/parameters_testnet.txt rename to core/primitives/res/runtime_configs/parameters_testnet.yaml index 158cce82351..84c72591272 100644 --- a/core/primitives/res/runtime_configs/parameters_testnet.txt +++ b/core/primitives/res/runtime_configs/parameters_testnet.yaml @@ -6,7 +6,7 @@ pessimistic_gas_price_inflation_denominator: 100 # Account creation config min_allowed_top_level_account_length: 0 -registrar_account_id: registrar +registrar_account_id: "registrar" # Storage usage config storage_amount_per_byte: 100_000_000_000_000_000_000 diff --git a/core/primitives/src/runtime/config_store.rs b/core/primitives/src/runtime/config_store.rs index 46bdc750095..15d0cb185c0 100644 --- a/core/primitives/src/runtime/config_store.rs +++ b/core/primitives/src/runtime/config_store.rs @@ -13,25 +13,25 @@ macro_rules! include_config { /// The base config file with all initial parameter values defined. /// Later version are calculated by applying diffs to this base. -static BASE_CONFIG: &str = include_config!("parameters.txt"); +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)] = &[ - (42, include_config!("42.txt")), - (48, include_config!("48.txt")), - (49, include_config!("49.txt")), - (50, include_config!("50.txt")), + (42, include_config!("42.yaml")), + (48, include_config!("48.yaml")), + (49, include_config!("49.yaml")), + (50, include_config!("50.yaml")), // max_gas_burnt increased to 300 TGas - (52, include_config!("52.txt")), + (52, include_config!("52.yaml")), // Increased deployment costs, increased wasmer2 stack_limit, added limiting of contract locals, // set read_cached_trie_node cost, decrease storage key limit - (53, include_config!("53.txt")), - (57, include_config!("57.txt")), + (53, include_config!("53.yaml")), + (57, include_config!("57.yaml")), ]; /// Testnet parameters for versions <= 29, which (incorrectly) differed from mainnet parameters -pub static INITIAL_TESTNET_CONFIG: &str = include_config!("parameters_testnet.txt"); +pub static INITIAL_TESTNET_CONFIG: &str = include_config!("parameters_testnet.yaml"); /// Stores runtime config for each protocol version where it was updated. #[derive(Debug)] diff --git a/core/primitives/src/runtime/parameter_table.rs b/core/primitives/src/runtime/parameter_table.rs index d040df0d4be..a61ba17dd1d 100644 --- a/core/primitives/src/runtime/parameter_table.rs +++ b/core/primitives/src/runtime/parameter_table.rs @@ -25,10 +25,10 @@ pub(crate) enum InvalidConfigError { UnknownParameter(#[source] strum::ParseError, String), #[error("could not parse `{1}` as a value")] ValueParseError(#[source] serde_json::Error, String), - #[error("expected a `:` separator between name and value of a parameter `{1}` on line {0}")] - NoSeparator(usize, String), #[error("intermediate JSON created by parser does not match `RuntimeConfig`")] WrongStructure(#[source] serde_json::Error), + #[error("could not parse YAML that defines the structure of the config")] + InvalidYaml(#[source] serde_yaml::Error), #[error("config diff expected to contain old value `{1}` for parameter `{0}`")] OldValueExists(Parameter, String), #[error( @@ -46,12 +46,24 @@ pub(crate) enum InvalidConfigError { impl std::str::FromStr for ParameterTable { type Err = InvalidConfigError; fn from_str(arg: &str) -> Result { - let parameters = txt_to_key_values(arg) - .map(|result| { - let (typed_key, value) = result?; - Ok((typed_key, parse_parameter_txt_value(value.trim())?)) + // TODO(#8320): Remove this after migration to `serde_yaml` 0.9 that supports empty strings. + if arg.is_empty() { + return Ok(ParameterTable { parameters: BTreeMap::new() }); + } + + let yaml_map: BTreeMap = + serde_yaml::from_str(arg).map_err(|err| InvalidConfigError::InvalidYaml(err))?; + + let parameters = yaml_map + .iter() + .map(|(key, value)| { + let typed_key: Parameter = key + .parse() + .map_err(|err| InvalidConfigError::UnknownParameter(err, key.to_owned()))?; + Ok((typed_key, parse_parameter_txt_value(value)?)) }) .collect::, _>>()?; + Ok(ParameterTable { parameters }) } } @@ -206,58 +218,45 @@ impl ParameterTable { } } +/// Represents YAML values supported by parameter diff config. +#[derive(serde::Deserialize, Clone, Debug)] +struct ParameterDiffValue { + old: Option, + new: Option, +} + impl std::str::FromStr for ParameterTableDiff { type Err = InvalidConfigError; fn from_str(arg: &str) -> Result { - let parameters = txt_to_key_values(arg) - .map(|result| { - let (typed_key, value) = result?; - if let Some((before, after)) = value.split_once("->") { - Ok(( - typed_key, - ( - parse_parameter_txt_value(before.trim())?, - parse_parameter_txt_value(after.trim())?, - ), - )) + let yaml_map: BTreeMap = + serde_yaml::from_str(arg).map_err(|err| InvalidConfigError::InvalidYaml(err))?; + + let parameters = yaml_map + .iter() + .map(|(key, value)| { + let typed_key: Parameter = key + .parse() + .map_err(|err| InvalidConfigError::UnknownParameter(err, key.to_owned()))?; + + let old_value = if let Some(s) = &value.old { + parse_parameter_txt_value(s)? } else { - Ok(( - typed_key, - (serde_json::Value::Null, parse_parameter_txt_value(value.trim())?), - )) - } + serde_json::Value::Null + }; + + let new_value = if let Some(s) = &value.new { + parse_parameter_txt_value(s)? + } else { + serde_json::Value::Null + }; + + Ok((typed_key, (old_value, new_value))) }) .collect::, _>>()?; Ok(ParameterTableDiff { parameters }) } } -fn txt_to_key_values( - arg: &str, -) -> impl Iterator> { - arg.lines() - .enumerate() - .filter_map(|(nr, line)| { - // ignore comments and empty lines - let trimmed = line.trim(); - if trimmed.starts_with("#") || trimmed.is_empty() { - None - } else { - Some((nr, trimmed)) - } - }) - .map(|(nr, trimmed)| { - let (key, value) = trimmed - .split_once(":") - .ok_or(InvalidConfigError::NoSeparator(nr + 1, trimmed.to_owned()))?; - let typed_key: Parameter = key - .trim() - .parse() - .map_err(|err| InvalidConfigError::UnknownParameter(err, key.to_owned()))?; - Ok((typed_key, value)) - }) -} - /// Parses a value from the custom format for runtime parameter definitions. /// /// A value can be a positive integer or a string, both written without quotes. @@ -360,17 +359,17 @@ storage_num_extra_bytes_record : 40 static DIFF_0: &str = r#" # Comment line -registrar_account_id: registrar -> near -min_allowed_top_level_account_length: 32 -> 32_000 -wasm_regular_op_cost: 3_856_371 +registrar_account_id: { old: "registrar", new: "near" } +min_allowed_top_level_account_length: { old: 32, new: 32_000 } +wasm_regular_op_cost: { new: 3_856_371 } "#; static DIFF_1: &str = r#" # Comment line -registrar_account_id: near -> registrar -storage_num_extra_bytes_record: 40 -> 77 -wasm_regular_op_cost: 3_856_371 -> 0 -max_memory_pages: 512 +registrar_account_id: { old: "near", new: "registrar" } +storage_num_extra_bytes_record: { old: 40, new: 77 } +wasm_regular_op_cost: { old: 3_856_371, new: 0 } +max_memory_pages: { new: 512 } "#; // Tests synthetic small example configurations. For tests with "real" @@ -452,7 +451,7 @@ max_memory_pages: 512 #[test] fn test_parameter_table_with_empty_value() { - let diff_with_empty_value = "min_allowed_top_level_account_length: 32 -> "; + let diff_with_empty_value = "min_allowed_top_level_account_length: { old: 32 }"; check_parameter_table( BASE_0, &[diff_with_empty_value], @@ -478,7 +477,10 @@ max_memory_pages: 512 #[test] fn test_parameter_table_invalid_key_in_diff() { assert_matches!( - check_invalid_parameter_table("wasm_regular_op_cost: 100", &["invalid_key: 100"]), + check_invalid_parameter_table( + "wasm_regular_op_cost: 100", + &["invalid_key: { new: 100 }"] + ), InvalidConfigError::UnknownParameter(_, _) ); } @@ -487,6 +489,7 @@ max_memory_pages: 512 fn test_parameter_table_no_key() { assert_matches!( check_invalid_parameter_table(": 100", &[]), + // TODO(#8320): This must be invalid YAML after migration to `serde_yaml` 0.9. InvalidConfigError::UnknownParameter(_, _) ); } @@ -495,7 +498,7 @@ max_memory_pages: 512 fn test_parameter_table_no_key_in_diff() { assert_matches!( check_invalid_parameter_table("wasm_regular_op_cost: 100", &[": 100"]), - InvalidConfigError::UnknownParameter(_, _) + InvalidConfigError::InvalidYaml(_) ); } @@ -503,7 +506,7 @@ max_memory_pages: 512 fn test_parameter_table_wrong_separator() { assert_matches!( check_invalid_parameter_table("wasm_regular_op_cost=100", &[]), - InvalidConfigError::NoSeparator(1, _) + InvalidConfigError::InvalidYaml(_) ); } @@ -514,7 +517,7 @@ max_memory_pages: 512 "wasm_regular_op_cost: 100", &["wasm_regular_op_cost=100"] ), - InvalidConfigError::NoSeparator(1, _) + InvalidConfigError::InvalidYaml(_) ); } @@ -523,7 +526,7 @@ max_memory_pages: 512 assert_matches!( check_invalid_parameter_table( "min_allowed_top_level_account_length: 3_200_000_000", - &["min_allowed_top_level_account_length: 3_200_000 -> 1_600_000"] + &["min_allowed_top_level_account_length: { old: 3_200_000, new: 1_600_000 }"] ), InvalidConfigError::WrongOldValue( Parameter::MinAllowedTopLevelAccountLength, @@ -541,7 +544,7 @@ max_memory_pages: 512 assert_matches!( check_invalid_parameter_table( "min_allowed_top_level_account_length: 3_200_000_000", - &["min_allowed_top_level_account_length: 1_600_000"] + &["min_allowed_top_level_account_length: { new: 1_600_000 }"] ), InvalidConfigError::OldValueExists(Parameter::MinAllowedTopLevelAccountLength, expected) => { assert_eq!(expected, "3200000000"); @@ -554,7 +557,7 @@ max_memory_pages: 512 assert_matches!( check_invalid_parameter_table( "min_allowed_top_level_account_length: 3_200_000_000", - &["wasm_regular_op_cost: 3_200_000 -> 1_600_000"] + &["wasm_regular_op_cost: { old: 3_200_000, new: 1_600_000 }"] ), InvalidConfigError::NoOldValueExists(Parameter::WasmRegularOpCost, found) => { assert_eq!(found, "3200000"); diff --git a/docs/architecture/gas/parameter_definition.md b/docs/architecture/gas/parameter_definition.md index ee76ec8ff6b..3aa403cf866 100644 --- a/docs/architecture/gas/parameter_definition.md +++ b/docs/architecture/gas/parameter_definition.md @@ -1,7 +1,7 @@ # Parameter Definitions Gas parameters are a subset of runtime parameters that are defined in -[core/primitives/res/runtime_configs/parameters.txt](https://github.com/near/nearcore/blob/d0dc37bf81f7e7bde9c560403b085fae04108659/core/primitives/res/runtime_configs/parameters.txt). +[core/primitives/res/runtime_configs/parameters.yaml](https://github.com/near/nearcore/blob/d0dc37bf81f7e7bde9c560403b085fae04108659/core/primitives/res/runtime_configs/parameters.yaml). IMPORTANT: This is not the final list of parameters, it contains the base values which can be overwritten per protocol version. For example, [53.txt](https://github.com/near/nearcore/blob/d0dc37bf81f7e7bde9c560403b085fae04108659/core/primitives/res/runtime_configs/53.txt) @@ -57,7 +57,7 @@ parameter. ### Necessary Code Changes Adding the parameter in code involves several steps. -1. Define the parameter by adding it to the list in `core/primitives/res/runtime_configs/parameters.txt.` +1. Define the parameter by adding it to the list in `core/primitives/res/runtime_configs/parameters.yaml.` 2. Update the Rust view of parameters by adding a variant to `enum Parameter` in `core/primitives-core/src/parameter.rs`. In the same file, update `enum FeeParameter` if you add an action cost or update `ext_costs()` @@ -77,7 +77,7 @@ Adding the parameter in code involves several steps. behind a feature flag. Add the feature to the `Cargo.toml` of each crate touched in step 3 and 4 and hide the code behind `#[cfg(feature = "protocol_feature_MY_NEW_FEATURE")]`. Do not hide code in step 2 so that - non-nightly builds can still read `parameters.txt`. Also add your feature as + non-nightly builds can still read `parameters.yaml`. Also add your feature as a dependency on `nightly` in `core/primitives/Cargo.toml` to make sure it gets included when compiling for nightly. After that, check `cargo check` and `cargo test --no-run` with and without `features=nightly`. From 48cd54dc760ce27a534b1337e4bad612c5658042 Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Wed, 11 Jan 2023 13:12:10 +0100 Subject: [PATCH 164/188] feat: add db operation for removing range of keys (#8313) See #8200 for more context. --- core/store/src/db.rs | 6 ++++++ core/store/src/db/colddb.rs | 7 +++++++ core/store/src/db/rocksdb.rs | 26 ++++++++++++++++++++++++++ core/store/src/db/testdb.rs | 3 +++ core/store/src/lib.rs | 19 ++++++++++++++++++- core/store/src/trie/shard_tries.rs | 5 ++++- 6 files changed, 64 insertions(+), 2 deletions(-) diff --git a/core/store/src/db.rs b/core/store/src/db.rs index d7edbdf832a..9742886e16a 100644 --- a/core/store/src/db.rs +++ b/core/store/src/db.rs @@ -44,6 +44,8 @@ pub(crate) enum DBOp { Delete { col: DBCol, key: Vec }, /// Deletes all data from a column. DeleteAll { col: DBCol }, + /// Deletes [`from`, `to`) key range, i.e. including `from` and excluding `to` + DeleteRange { col: DBCol, from: Vec, to: Vec }, } impl DBTransaction { @@ -73,6 +75,10 @@ impl DBTransaction { self.ops.push(DBOp::DeleteAll { col }); } + pub fn delete_range(&mut self, col: DBCol, from: Vec, to: Vec) { + self.ops.push(DBOp::DeleteRange { col, from, to }); + } + pub fn merge(&mut self, other: DBTransaction) { self.ops.extend(other.ops) } diff --git a/core/store/src/db/colddb.rs b/core/store/src/db/colddb.rs index 591eec10cd8..d647a9b6928 100644 --- a/core/store/src/db/colddb.rs +++ b/core/store/src/db/colddb.rs @@ -292,6 +292,13 @@ fn adjust_op(op: &mut DBOp) -> bool { near_o11y::log_assert!(false, "Unexpected delete of {col} in cold store"); false } + DBOp::DeleteRange { col, from, to } => { + near_o11y::log_assert!( + false, + "Unexpected delete range from {col} in cold store: {from:?} {to:?}" + ); + false + } } } diff --git a/core/store/src/db/rocksdb.rs b/core/store/src/db/rocksdb.rs index 2766a7f75f1..ef8a8abe42b 100644 --- a/core/store/src/db/rocksdb.rs +++ b/core/store/src/db/rocksdb.rs @@ -315,6 +315,9 @@ impl Database for RocksDB { batch.delete_cf(cf_handle, range.end()) } } + DBOp::DeleteRange { col, from, to } => { + batch.delete_range_cf(self.cf_handle(col)?, from, to); + } } } self.db.write(batch).map_err(into_other) @@ -640,6 +643,7 @@ fn col_name(col: DBCol) -> &'static str { mod tests { use crate::db::{Database, StatsValue}; use crate::{DBCol, NodeStorage, StoreStatistics}; + use assert_matches::assert_matches; use super::*; @@ -735,4 +739,26 @@ mod tests { } ); } + + #[test] + fn test_delete_range() { + let store = NodeStorage::test_opener().1.open().unwrap().get_store(crate::Temperature::Hot); + let keys = [vec![0], vec![1], vec![2], vec![3]]; + let column = DBCol::Block; + + let mut store_update = store.store_update(); + for key in &keys { + store_update.insert(column, key, &vec![42]); + } + store_update.commit().unwrap(); + + let mut store_update = store.store_update(); + store_update.delete_range(column, &keys[1], &keys[3]); + store_update.commit().unwrap(); + + assert_matches!(store.exists(column, &keys[0]), Ok(true)); + assert_matches!(store.exists(column, &keys[1]), Ok(false)); + assert_matches!(store.exists(column, &keys[2]), Ok(false)); + assert_matches!(store.exists(column, &keys[3]), Ok(true)); + } } diff --git a/core/store/src/db/testdb.rs b/core/store/src/db/testdb.rs index 59570191a8b..598ce4723e6 100644 --- a/core/store/src/db/testdb.rs +++ b/core/store/src/db/testdb.rs @@ -80,6 +80,9 @@ impl Database for TestDB { db[col].remove(&key); } DBOp::DeleteAll { col } => db[col].clear(), + DBOp::DeleteRange { col, from, to } => { + db[col].retain(|key, _| !(&from..&to).contains(&key)); + } }; } Ok(()) diff --git a/core/store/src/lib.rs b/core/store/src/lib.rs index 4150cb8ab35..26443dc5ed3 100644 --- a/core/store/src/lib.rs +++ b/core/store/src/lib.rs @@ -537,6 +537,12 @@ impl StoreUpdate { self.transaction.delete_all(column); } + /// Deletes the given key range from the database including `from` + /// and excluding `to` keys. + pub fn delete_range(&mut self, column: DBCol, from: &[u8], to: &[u8]) { + self.transaction.delete_range(column, from.to_vec(), to.to_vec()); + } + /// Sets reference to the trie to clear cache on the commit. /// /// Panics if shard_tries are already set to a different object. @@ -596,7 +602,9 @@ impl StoreUpdate { DBOp::Set { col, key, .. } | DBOp::Insert { col, key, .. } | DBOp::Delete { col, key } => Some((*col as u8, key)), - DBOp::UpdateRefcount { .. } | DBOp::DeleteAll { .. } => None, + DBOp::UpdateRefcount { .. } + | DBOp::DeleteAll { .. } + | DBOp::DeleteRange { .. } => None, }) .collect::>(); non_refcount_keys.len() @@ -623,6 +631,9 @@ impl StoreUpdate { DBOp::DeleteAll { col } => { tracing::trace!(target: "store", db_op = "delete_all", col = %col) } + DBOp::DeleteRange { col, from, to } => { + tracing::trace!(target: "store", db_op = "delete_range", col = %col, from = %pretty::StorageKey(from), to = %pretty::StorageKey(to)) + } } } let storage = match &self.storage { @@ -657,6 +668,12 @@ impl fmt::Debug for StoreUpdate { } DBOp::Delete { col, key } => writeln!(f, " - {col} {}", pretty::StorageKey(key))?, DBOp::DeleteAll { col } => writeln!(f, " - {col} (all)")?, + DBOp::DeleteRange { col, from, to } => writeln!( + f, + " - {col} [{}, {})", + pretty::StorageKey(from), + pretty::StorageKey(to) + )?, } } writeln!(f, "}}") diff --git a/core/store/src/trie/shard_tries.rs b/core/store/src/trie/shard_tries.rs index 5bc9c603f25..29643abeec0 100644 --- a/core/store/src/trie/shard_tries.rs +++ b/core/store/src/trie/shard_tries.rs @@ -209,7 +209,10 @@ impl ShardTries { } } } - DBOp::Set { col, .. } | DBOp::Insert { col, .. } | DBOp::Delete { col, .. } => { + DBOp::Set { col, .. } + | DBOp::Insert { col, .. } + | DBOp::Delete { col, .. } + | DBOp::DeleteRange { col, .. } => { assert_ne!(*col, DBCol::State); } } From d4bb44b960baa5f1787f7a5236f613fcaa950214 Mon Sep 17 00:00:00 2001 From: posvyatokum Date: Wed, 11 Jan 2023 12:44:01 +0000 Subject: [PATCH 165/188] Add CopyNextBlocks to cold-store testing tool (#8125) * adding CopyNextBlocks to cold_store testing tool --- Cargo.lock | 1 + core/store/src/lib.rs | 2 +- tools/cold-store/Cargo.toml | 3 +- tools/cold-store/README.md | 7 ++- tools/cold-store/src/cli.rs | 116 ++++++++++++++++++++++++++++++++++++ 5 files changed, 124 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62d2c0ce5e9..73edd3059fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1003,6 +1003,7 @@ version = "0.0.0" dependencies = [ "clap 3.1.18", "near-chain-configs", + "near-epoch-manager", "near-primitives", "near-store", "nearcore", diff --git a/core/store/src/lib.rs b/core/store/src/lib.rs index 26443dc5ed3..7928dc6b211 100644 --- a/core/store/src/lib.rs +++ b/core/store/src/lib.rs @@ -63,7 +63,7 @@ pub use crate::opener::{StoreMigrator, StoreOpener, StoreOpenerError}; /// In the future, certain parts of the code may need to access hot or cold /// storage. Specifically, querying an old block will require reading it from /// the cold storage. -#[derive(Clone, Copy, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Temperature { Hot, #[cfg(feature = "cold_store")] diff --git a/tools/cold-store/Cargo.toml b/tools/cold-store/Cargo.toml index b333c4516c5..23d3a8a1e75 100644 --- a/tools/cold-store/Cargo.toml +++ b/tools/cold-store/Cargo.toml @@ -8,10 +8,11 @@ edition.workspace = true [dependencies] clap.workspace = true -near-store = { path = "../../core/store"} nearcore = { path = "../../nearcore"} near-chain-configs = { path = "../../core/chain-configs"} +near-epoch-manager = { path = "../../chain/epoch-manager" } near-primitives = { path = "../../core/primitives" } +near-store = { path = "../../core/store"} [features] cold_store = [ diff --git a/tools/cold-store/README.md b/tools/cold-store/README.md index 295b483cd70..48fa251ef1b 100644 --- a/tools/cold-store/README.md +++ b/tools/cold-store/README.md @@ -78,9 +78,10 @@ Print `HEAD` of cold storage, `FINAL_HEAD` from hot storage and also `HEAD` for hot storage. It is useful to check that some blocks has been copied/produced. -### (TODO) CopyOneBlock -Copy block at height "cold HEAD + 1" to cold storage. -Update cold storage `HEAD`. +### CopyNextBlocks +Does `num_of_blocks`(1 by default) iterations of +- Copy block at height "cold HEAD + 1" to cold storage. +- Update cold storage `HEAD`. ### (TODO) CopyAllBlocks Initial population of cold storage, where we copy all cold column diff --git a/tools/cold-store/src/cli.rs b/tools/cold-store/src/cli.rs index 5d1231a3974..dc438b8caef 100644 --- a/tools/cold-store/src/cli.rs +++ b/tools/cold-store/src/cli.rs @@ -1,8 +1,13 @@ +use near_epoch_manager::EpochManagerAdapter; use near_primitives::block::Tip; +use near_primitives::hash::CryptoHash; +use near_store::cold_storage::{update_cold_db, update_cold_head}; use near_store::{DBCol, NodeStorage, Temperature, FINAL_HEAD_KEY, HEAD_KEY}; +use nearcore::{NearConfig, NightshadeRuntime}; use clap::Parser; use std::path::Path; +use std::sync::Arc; #[derive(Parser)] pub struct ColdStoreCommand { @@ -17,6 +22,9 @@ enum SubCommand { Open, /// Open NodeStorage and print cold head, hot head and hot final head. Head, + /// Copy n blocks to cold storage and update cold HEAD. One by one. + /// Updating of HEAD happens in every iteration. + CopyNextBlocks(CopyNextBlocksCmd), } impl ColdStoreCommand { @@ -32,13 +40,30 @@ impl ColdStoreCommand { near_config.config.cold_store.as_ref(), ); let store = opener.open().unwrap_or_else(|e| panic!("Error opening storage: {:#}", e)); + + let hot_runtime = Arc::new(NightshadeRuntime::from_config( + home_dir, + store.get_store(Temperature::Hot), + &near_config, + )); match self.subcmd { SubCommand::Open => check_open(&store), SubCommand::Head => print_heads(&store), + SubCommand::CopyNextBlocks(cmd) => { + for _ in 0..cmd.number_of_blocks { + copy_next_block(&store, &near_config, &hot_runtime); + } + } } } } +#[derive(Parser)] +struct CopyNextBlocksCmd { + #[clap(short, long, default_value_t = 1)] + number_of_blocks: usize, +} + fn check_open(store: &NodeStorage) { assert!(store.has_cold()); } @@ -57,3 +82,94 @@ fn print_heads(store: &NodeStorage) { store.get_store(Temperature::Cold).get_ser::(DBCol::BlockMisc, HEAD_KEY) ); } + +fn copy_next_block(store: &NodeStorage, config: &NearConfig, hot_runtime: &Arc) { + // Cold HEAD can be not set in testing. + // It should be set before the copying of a block in prod, + // but we should default it to genesis height here. + let cold_head_height = store + .get_store(Temperature::Cold) + .get_ser::(DBCol::BlockMisc, HEAD_KEY) + .unwrap_or_else(|e| panic!("Error reading cold HEAD: {:#}", e)) + .map_or(config.genesis.config.genesis_height, |t| t.height); + + // If FINAL_HEAD is not set for hot storage though, we default it to 0. + // And subsequently fail in assert!(next_height <= hot_final_height). + let hot_final_head = store + .get_store(Temperature::Hot) + .get_ser::(DBCol::BlockMisc, FINAL_HEAD_KEY) + .unwrap_or_else(|e| panic!("Error reading hot FINAL_HEAD: {:#}", e)) + .map(|t| t.height) + .unwrap_or(0); + + let next_height = cold_head_height + 1; + println!("Height: {}", next_height); + assert!(next_height <= hot_final_head, "Should not copy non final blocks"); + + // Here it should be sufficient to just read from hot storage. + // Because BlockHeight is never garbage collectable and is not even copied to cold. + let cold_head_hash = get_ser_from_store::( + store, + Temperature::Hot, + DBCol::BlockHeight, + &cold_head_height.to_le_bytes(), + ) + .unwrap_or_else(|| panic!("No block hash in hot storage for height {}", cold_head_height)); + + // For copying block we need to have shard_layout. + // For that we need epoch_id. + // For that we might use prev_block_hash, and because next_hight = cold_head_height + 1, + // we use cold_head_hash. + update_cold_db( + &*store.cold_db().unwrap(), + &store.get_store(Temperature::Hot), + &hot_runtime + .get_shard_layout(&hot_runtime.get_epoch_id_from_prev_block(&cold_head_hash).unwrap()) + .unwrap(), + &next_height, + ) + .expect(&std::format!("Failed to copy block at height {} to cold db", next_height)); + + update_cold_head(&*store.cold_db().unwrap(), &store.get_store(Temperature::Hot), &next_height) + .expect(&std::format!("Failed to update cold HEAD to {}", next_height)); +} + +/// Calls get_ser on Store with provided temperature from provided NodeStorage. +/// Expects read to not result in errors. +fn get_ser_from_store( + store: &NodeStorage, + temperature: Temperature, + col: DBCol, + key: &[u8], +) -> Option { + store.get_store(temperature).get_ser(col, key).expect(&std::format!( + "Error reading {} {:?} from {:?} store", + col, + key, + temperature + )) +} + +/// First try to read col, key from hot storage. +/// If resulted Option is None, try to read col, key from cold storage. +/// Returns serialized inner value (not wrapped in option), +/// or panics, if value isn't found even in cold. +/// +/// Reads from database are expected to not result in errors. +/// +/// This function is currently unused, but will be important in the future, +/// when we start garbage collecting data from hot. +/// For some columns you can understand the temperature by key. +/// For others, though, read with fallback is the only working solution right now. +fn _get_with_fallback( + store: &NodeStorage, + col: DBCol, + key: &[u8], +) -> T { + let hot_option = get_ser_from_store(store, Temperature::Hot, col, key); + match hot_option { + Some(value) => value, + None => get_ser_from_store(store, Temperature::Cold, col, key) + .unwrap_or_else(|| panic!("No value for {} {:?} in any storage", col, key)), + } +} From 5011ff9188c88330cdeeea345f2435b826fe2e77 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Wed, 11 Jan 2023 15:51:40 +0100 Subject: [PATCH 166/188] feat: gas profile by parameter (#8148) This resolves #8033, completing the gas profile by parameter effort. The main achievement here is that we can now look at gas profiles and know exactly for which parameter it has been spent. Code wise, the old `enum Cost` used in profiles only is finally gone. The tricky parts in this PR are to make sure that old profiles stored on DB are still understood and presented correctly in RPC calls. --- CHANGELOG.md | 2 + Cargo.lock | 1 + core/primitives-core/Cargo.toml | 2 + core/primitives-core/src/config.rs | 155 +++---- core/primitives-core/src/profile.rs | 412 +++++++++--------- .../primitives-core/src/profile/profile_v2.rs | 265 +++++++++++ ..._profile_v2__test__profile_data_debug.snap | 78 ++++ ..._profile_v1__test__profile_data_debug.snap | 78 ++++ ...re__profile__test__profile_data_debug.snap | 83 ++++ ...__views__tests__exec_metadata_v1_view.snap | 8 + ...__views__tests__exec_metadata_v2_view.snap | 359 +++++++++++++++ ...__views__tests__exec_metadata_v3_view.snap | 374 ++++++++++++++++ ...es__views__tests__runtime_config_view.snap | 201 +++++++++ core/primitives/src/transaction.rs | 11 +- core/primitives/src/views.rs | 210 ++++++--- .../client/features/chunk_nodes_cache.rs | 3 +- ...__sanity_checks__receipts_gas_profile.snap | 34 +- ..._checks__receipts_gas_profile_nightly.snap | 34 +- runtime/near-vm-logic/src/gas_counter.rs | 6 +- runtime/near-vm-logic/src/logic.rs | 6 +- runtime/runtime/src/lib.rs | 6 +- 21 files changed, 1955 insertions(+), 373 deletions(-) create mode 100644 core/primitives-core/src/profile/profile_v2.rs create mode 100644 core/primitives-core/src/profile/snapshots/near_primitives_core__profile__profile_v2__test__profile_data_debug.snap create mode 100644 core/primitives-core/src/snapshots/near_primitives_core__profile__profile_v1__test__profile_data_debug.snap create mode 100644 core/primitives-core/src/snapshots/near_primitives_core__profile__test__profile_data_debug.snap create mode 100644 core/primitives/src/snapshots/near_primitives__views__tests__exec_metadata_v1_view.snap create mode 100644 core/primitives/src/snapshots/near_primitives__views__tests__exec_metadata_v2_view.snap create mode 100644 core/primitives/src/snapshots/near_primitives__views__tests__exec_metadata_v3_view.snap create mode 100644 core/primitives/src/snapshots/near_primitives__views__tests__runtime_config_view.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index d2d9154825e..19ba761f9b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,8 @@ `opentelemetry-otlp`: [#7563](https://github.com/near/nearcore/pull/7563). * Tracing of requests across processes: [#8004](https://github.com/near/nearcore/pull/8004). +* Gas profiles as displayed in the `EXPERIMENTAL_tx_status` are now more + detailed and give the gas cost per parameter. ## 1.30.0 diff --git a/Cargo.lock b/Cargo.lock index 73edd3059fd..6b6a7d1aa2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3414,6 +3414,7 @@ dependencies = [ "bs58", "derive_more", "enum-map", + "insta", "near-account-id", "num-rational", "serde", diff --git a/core/primitives-core/Cargo.toml b/core/primitives-core/Cargo.toml index 2c3bd756710..617391b1602 100644 --- a/core/primitives-core/Cargo.toml +++ b/core/primitives-core/Cargo.toml @@ -30,7 +30,9 @@ near-account-id = { path = "../account-id", features = ["arbitrary"] } [dev-dependencies] serde_json.workspace = true +insta.workspace = true [features] default = [] protocol_feature_ed25519_verify = [] +nightly = ["protocol_feature_ed25519_verify"] diff --git a/core/primitives-core/src/config.rs b/core/primitives-core/src/config.rs index 17d794bf380..7e230769592 100644 --- a/core/primitives-core/src/config.rs +++ b/core/primitives-core/src/config.rs @@ -359,6 +359,9 @@ impl ExtCostsConfig { } /// Strongly-typed representation of the fees for counting. +/// +/// Do not change the enum discriminants here, they are used for borsh +/// (de-)serialization. #[derive( Copy, Clone, @@ -374,69 +377,69 @@ impl ExtCostsConfig { )] #[allow(non_camel_case_types)] pub enum ExtCosts { - base, - contract_loading_base, - contract_loading_bytes, - read_memory_base, - read_memory_byte, - write_memory_base, - write_memory_byte, - read_register_base, - read_register_byte, - write_register_base, - write_register_byte, - utf8_decoding_base, - utf8_decoding_byte, - utf16_decoding_base, - utf16_decoding_byte, - sha256_base, - sha256_byte, - keccak256_base, - keccak256_byte, - keccak512_base, - keccak512_byte, - ripemd160_base, - ripemd160_block, + base = 0, + contract_loading_base = 1, + contract_loading_bytes = 2, + read_memory_base = 3, + read_memory_byte = 4, + write_memory_base = 5, + write_memory_byte = 6, + read_register_base = 7, + read_register_byte = 8, + write_register_base = 9, + write_register_byte = 10, + utf8_decoding_base = 11, + utf8_decoding_byte = 12, + utf16_decoding_base = 13, + utf16_decoding_byte = 14, + sha256_base = 15, + sha256_byte = 16, + keccak256_base = 17, + keccak256_byte = 18, + keccak512_base = 19, + keccak512_byte = 20, + ripemd160_base = 21, + ripemd160_block = 22, + ecrecover_base = 23, + log_base = 24, + log_byte = 25, + storage_write_base = 26, + storage_write_key_byte = 27, + storage_write_value_byte = 28, + storage_write_evicted_byte = 29, + storage_read_base = 30, + storage_read_key_byte = 31, + storage_read_value_byte = 32, + storage_remove_base = 33, + storage_remove_key_byte = 34, + storage_remove_ret_value_byte = 35, + storage_has_key_base = 36, + storage_has_key_byte = 37, + storage_iter_create_prefix_base = 38, + storage_iter_create_prefix_byte = 39, + storage_iter_create_range_base = 40, + storage_iter_create_from_byte = 41, + storage_iter_create_to_byte = 42, + storage_iter_next_base = 43, + storage_iter_next_key_byte = 44, + storage_iter_next_value_byte = 45, + touching_trie_node = 46, + read_cached_trie_node = 47, + promise_and_base = 48, + promise_and_per_promise = 49, + promise_return = 50, + validator_stake_base = 51, + validator_total_stake_base = 52, + alt_bn128_g1_multiexp_base = 53, + alt_bn128_g1_multiexp_element = 54, + alt_bn128_pairing_check_base = 55, + alt_bn128_pairing_check_element = 56, + alt_bn128_g1_sum_base = 57, + alt_bn128_g1_sum_element = 58, #[cfg(feature = "protocol_feature_ed25519_verify")] - ed25519_verify_base, + ed25519_verify_base = 59, #[cfg(feature = "protocol_feature_ed25519_verify")] - ed25519_verify_byte, - ecrecover_base, - log_base, - log_byte, - storage_write_base, - storage_write_key_byte, - storage_write_value_byte, - storage_write_evicted_byte, - storage_read_base, - storage_read_key_byte, - storage_read_value_byte, - storage_remove_base, - storage_remove_key_byte, - storage_remove_ret_value_byte, - storage_has_key_base, - storage_has_key_byte, - storage_iter_create_prefix_base, - storage_iter_create_prefix_byte, - storage_iter_create_range_base, - storage_iter_create_from_byte, - storage_iter_create_to_byte, - storage_iter_next_base, - storage_iter_next_key_byte, - storage_iter_next_value_byte, - touching_trie_node, - read_cached_trie_node, - promise_and_base, - promise_and_per_promise, - promise_return, - validator_stake_base, - validator_total_stake_base, - alt_bn128_g1_multiexp_base, - alt_bn128_g1_multiexp_element, - alt_bn128_pairing_check_base, - alt_bn128_pairing_check_element, - alt_bn128_g1_sum_base, - alt_bn128_g1_sum_element, + ed25519_verify_byte = 60, } // Type of an action, used in fees logic. @@ -455,21 +458,21 @@ pub enum ExtCosts { )] #[allow(non_camel_case_types)] pub enum ActionCosts { - create_account, - delete_account, - deploy_contract_base, - deploy_contract_byte, - function_call_base, - function_call_byte, - transfer, - stake, - add_full_access_key, - add_function_call_key_base, - add_function_call_key_byte, - delete_key, - new_action_receipt, - new_data_receipt_base, - new_data_receipt_byte, + create_account = 0, + delete_account = 1, + deploy_contract_base = 2, + deploy_contract_byte = 3, + function_call_base = 4, + function_call_byte = 5, + transfer = 6, + stake = 7, + add_full_access_key = 8, + add_function_call_key_base = 9, + add_function_call_key_byte = 10, + delete_key = 11, + new_action_receipt = 12, + new_data_receipt_base = 13, + new_data_receipt_byte = 14, } impl ExtCosts { diff --git a/core/primitives-core/src/profile.rs b/core/primitives-core/src/profile.rs index 2cf232ad80c..cd0bb3d1a48 100644 --- a/core/primitives-core/src/profile.rs +++ b/core/primitives-core/src/profile.rs @@ -1,88 +1,76 @@ +pub use profile_v2::ProfileDataV2; + use crate::config::{ActionCosts, ExtCosts}; +use crate::types::Gas; use borsh::{BorshDeserialize, BorshSerialize}; +use enum_map::{enum_map, Enum, EnumMap}; use std::fmt; -use std::ops::{Index, IndexMut}; use strum::IntoEnumIterator; -/// Serialization format to store profiles in the database. -/// -/// This is not part of the protocol but archival nodes still rely on this not -/// changing to answer old tx-status requests with a gas profile. -#[derive(Clone, PartialEq, Eq)] -pub struct DataArray(Box<[u64; Self::LEN]>); - -impl DataArray { - pub const LEN: usize = if cfg!(feature = "protocol_feature_ed25519_verify") { 72 } else { 70 }; -} - -impl Index for DataArray { - type Output = u64; - - fn index(&self, index: usize) -> &Self::Output { - &self.0[index] - } -} - -impl IndexMut for DataArray { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - &mut self.0[index] - } -} - -impl BorshDeserialize for DataArray { - fn deserialize(buf: &mut &[u8]) -> Result { - let data_vec: Vec = BorshDeserialize::deserialize(buf)?; - let mut data_array = [0; Self::LEN]; - let len = Self::LEN.min(data_vec.len()); - data_array[0..len].copy_from_slice(&data_vec[0..len]); - Ok(Self(Box::new(data_array))) - } -} - -impl BorshSerialize for DataArray { - fn serialize(&self, writer: &mut W) -> Result<(), std::io::Error> { - let v: Vec<_> = self.0.as_ref().to_vec(); - BorshSerialize::serialize(&v, writer) - } -} +mod profile_v2; /// Profile of gas consumption. -/// When add new cost, the new cost should also be append to profile_index -#[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] -pub struct ProfileData { - data: DataArray, +#[derive(Clone, PartialEq, Eq)] +pub struct ProfileDataV3 { + /// Gas spent on sending or executing actions. + actions_profile: EnumMap, + /// Non-action gas spent outside the WASM VM while executing a contract. + wasm_ext_profile: EnumMap, + /// Gas spent on execution inside the WASM VM. + wasm_gas: Gas, } -impl Default for ProfileData { - fn default() -> ProfileData { - ProfileData::new() +impl Default for ProfileDataV3 { + fn default() -> ProfileDataV3 { + ProfileDataV3::new() } } -impl ProfileData { +impl ProfileDataV3 { #[inline] pub fn new() -> Self { - let costs = DataArray(Box::new([0; DataArray::LEN])); - ProfileData { data: costs } + Self { + actions_profile: enum_map! { _ => 0 }, + wasm_ext_profile: enum_map! { _ => 0 }, + wasm_gas: 0, + } + } + + /// Test instance with unique numbers in each field. + pub fn test() -> Self { + let mut profile_data = ProfileDataV3::default(); + for (i, cost) in ExtCosts::iter().enumerate() { + profile_data.add_ext_cost(cost, i as Gas); + } + for (i, cost) in ActionCosts::iter().enumerate() { + profile_data.add_action_cost(cost, i as Gas + 1000); + } + profile_data } #[inline] - pub fn merge(&mut self, other: &ProfileData) { - for i in 0..DataArray::LEN { - self.data[i] = self.data[i].saturating_add(other.data[i]); + pub fn merge(&mut self, other: &ProfileDataV3) { + for ((_, gas), (_, other_gas)) in + self.actions_profile.iter_mut().zip(other.actions_profile.iter()) + { + *gas = gas.saturating_add(*other_gas); + } + for ((_, gas), (_, other_gas)) in + self.wasm_ext_profile.iter_mut().zip(other.wasm_ext_profile.iter()) + { + *gas = gas.saturating_add(*other_gas); } + self.wasm_gas = self.wasm_gas.saturating_add(other.wasm_gas); } #[inline] pub fn add_action_cost(&mut self, action: ActionCosts, value: u64) { - self[Cost::ActionCost { action_cost_kind: action }] = - self[Cost::ActionCost { action_cost_kind: action }].saturating_add(value); + self.actions_profile[action] = self.actions_profile[action].saturating_add(value); } #[inline] pub fn add_ext_cost(&mut self, ext: ExtCosts, value: u64) { - self[Cost::ExtCost { ext_cost_kind: ext }] = - self[Cost::ExtCost { ext_cost_kind: ext }].saturating_add(value); + self.wasm_ext_profile[ext] = self.wasm_ext_profile[ext].saturating_add(value); } /// WasmInstruction is the only cost we don't explicitly account for. @@ -94,37 +82,100 @@ impl ProfileData { /// with the help on the VM side, so we don't want to have profiling logic /// there both for simplicity and efficiency reasons. pub fn compute_wasm_instruction_cost(&mut self, total_gas_burnt: u64) { - self[Cost::WasmInstruction] = + self.wasm_gas = total_gas_burnt.saturating_sub(self.action_gas()).saturating_sub(self.host_gas()); } - fn get_action_cost(&self, action: ActionCosts) -> u64 { - self[Cost::ActionCost { action_cost_kind: action }] + pub fn get_action_cost(&self, action: ActionCosts) -> u64 { + self.actions_profile[action] } pub fn get_ext_cost(&self, ext: ExtCosts) -> u64 { - self[Cost::ExtCost { ext_cost_kind: ext }] + self.wasm_ext_profile[ext] + } + + pub fn get_wasm_cost(&self) -> u64 { + self.wasm_gas } fn host_gas(&self) -> u64 { - ExtCosts::iter().map(|a| self.get_ext_cost(a)).fold(0, u64::saturating_add) + self.wasm_ext_profile.as_slice().iter().copied().fold(0, u64::saturating_add) } pub fn action_gas(&self) -> u64 { - // TODO(#8033): Temporary hack to work around multiple action costs - // mapping to the same profile entry. Can be removed as soon as inner - // tracking of profile is tracked by parameter or when the profile is - // more detail, which ever happens first. - let mut indices: Vec<_> = ActionCosts::iter() - .map(|action_cost_kind| Cost::ActionCost { action_cost_kind }.profile_index()) - .collect(); - indices.sort_unstable(); - indices.dedup(); - indices.into_iter().map(|i| self.data[i]).fold(0, u64::saturating_add) + self.actions_profile.as_slice().iter().copied().fold(0, u64::saturating_add) } } -impl fmt::Debug for ProfileData { +impl BorshDeserialize for ProfileDataV3 { + fn deserialize(buf: &mut &[u8]) -> Result { + let actions_array: Vec = BorshDeserialize::deserialize(buf)?; + let ext_array: Vec = BorshDeserialize::deserialize(buf)?; + let wasm_gas: u64 = BorshDeserialize::deserialize(buf)?; + + // Mapping raw arrays to enum maps. + // The enum map could be smaller or larger than the raw array. + // Extra values in the array that are unknown to the current binary will + // be ignored. Missing values are filled with 0. + let actions_profile = enum_map! { + cost => actions_array.get(borsh_action_index(cost)).copied().unwrap_or(0) + }; + let wasm_ext_profile = enum_map! { + cost => ext_array.get(borsh_ext_index(cost)).copied().unwrap_or(0) + }; + + Ok(Self { actions_profile, wasm_ext_profile, wasm_gas }) + } +} + +impl BorshSerialize for ProfileDataV3 { + fn serialize(&self, writer: &mut W) -> Result<(), std::io::Error> { + let mut actions_costs: Vec = vec![0u64; ActionCosts::LENGTH]; + for (cost, gas) in self.actions_profile.iter() { + actions_costs[borsh_action_index(cost)] = *gas; + } + BorshSerialize::serialize(&actions_costs, writer)?; + + let mut ext_costs: Vec = vec![0u64; ExtCosts::LENGTH]; + for (cost, gas) in self.wasm_ext_profile.iter() { + ext_costs[borsh_ext_index(cost)] = *gas; + } + BorshSerialize::serialize(&ext_costs, writer)?; + + let wasm_cost: u64 = self.wasm_gas; + BorshSerialize::serialize(&wasm_cost, writer) + } +} + +/// Fixed index of an action cost for borsh (de)serialization. +/// +/// We use borsh to store profiles on the DB and borsh is quite fragile with +/// changes. This mapping is to decouple the Rust enum from the borsh +/// representation. The enum can be changed freely but here in the mapping we +/// can only append more values at the end. +/// +/// TODO: Consider changing this to a different format (e.g. protobuf) because +/// canonical representation is not required here. +const fn borsh_action_index(action: ActionCosts) -> usize { + // actual indices are defined on the enum variants + action as usize +} + +/// Fixed index of an ext cost for borsh (de)serialization. +/// +/// We use borsh to store profiles on the DB and borsh is quite fragile with +/// changes. This mapping is to decouple the Rust enum from the borsh +/// representation. The enum can be changed freely but here in the mapping we +/// can only append more values at the end. +/// +/// TODO: Consider changing this to a different format (e.g. protobuf) because +/// canonical representation is not required here. +const fn borsh_ext_index(ext: ExtCosts) -> usize { + // actual indices are defined on the enum variants + ext as usize +} + +impl fmt::Debug for ProfileDataV3 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use num_rational::Ratio; let host_gas = self.host_gas(); @@ -157,146 +208,30 @@ impl fmt::Debug for ProfileData { } } -#[derive(Clone, Copy, Debug)] -pub enum Cost { - ActionCost { action_cost_kind: ActionCosts }, - ExtCost { ext_cost_kind: ExtCosts }, - WasmInstruction, -} - -impl Cost { - pub fn iter() -> impl Iterator { - let actions = - ActionCosts::iter().map(|action_cost_kind| Cost::ActionCost { action_cost_kind }); - let ext = ExtCosts::iter().map(|ext_cost_kind| Cost::ExtCost { ext_cost_kind }); - let op = std::iter::once(Cost::WasmInstruction); - actions.chain(ext).chain(op) - } - - /// Entry in DataArray inside the gas profile. - /// - /// Entries can be shared by multiple costs. - /// - /// TODO: Remove after completing all of #8033. - pub fn profile_index(self) -> usize { - // make sure to increase `DataArray::LEN` when adding a new index - match self { - Cost::ActionCost { action_cost_kind: ActionCosts::create_account } => 0, - Cost::ActionCost { action_cost_kind: ActionCosts::delete_account } => 1, - Cost::ActionCost { action_cost_kind: ActionCosts::deploy_contract_base } => 2, - Cost::ActionCost { action_cost_kind: ActionCosts::deploy_contract_byte } => 2, - Cost::ActionCost { action_cost_kind: ActionCosts::function_call_base } => 3, - Cost::ActionCost { action_cost_kind: ActionCosts::function_call_byte } => 3, - Cost::ActionCost { action_cost_kind: ActionCosts::transfer } => 4, - Cost::ActionCost { action_cost_kind: ActionCosts::stake } => 5, - Cost::ActionCost { action_cost_kind: ActionCosts::add_full_access_key } => 6, - Cost::ActionCost { action_cost_kind: ActionCosts::add_function_call_key_base } => 6, - Cost::ActionCost { action_cost_kind: ActionCosts::add_function_call_key_byte } => 6, - Cost::ActionCost { action_cost_kind: ActionCosts::delete_key } => 7, - Cost::ActionCost { action_cost_kind: ActionCosts::new_data_receipt_byte } => 8, - Cost::ActionCost { action_cost_kind: ActionCosts::new_action_receipt } => 9, - Cost::ActionCost { action_cost_kind: ActionCosts::new_data_receipt_base } => 9, - Cost::ExtCost { ext_cost_kind: ExtCosts::base } => 10, - Cost::ExtCost { ext_cost_kind: ExtCosts::contract_loading_base } => 11, - Cost::ExtCost { ext_cost_kind: ExtCosts::contract_loading_bytes } => 12, - Cost::ExtCost { ext_cost_kind: ExtCosts::read_memory_base } => 13, - Cost::ExtCost { ext_cost_kind: ExtCosts::read_memory_byte } => 14, - Cost::ExtCost { ext_cost_kind: ExtCosts::write_memory_base } => 15, - Cost::ExtCost { ext_cost_kind: ExtCosts::write_memory_byte } => 16, - Cost::ExtCost { ext_cost_kind: ExtCosts::read_register_base } => 17, - Cost::ExtCost { ext_cost_kind: ExtCosts::read_register_byte } => 18, - Cost::ExtCost { ext_cost_kind: ExtCosts::write_register_base } => 19, - Cost::ExtCost { ext_cost_kind: ExtCosts::write_register_byte } => 20, - Cost::ExtCost { ext_cost_kind: ExtCosts::utf8_decoding_base } => 21, - Cost::ExtCost { ext_cost_kind: ExtCosts::utf8_decoding_byte } => 22, - Cost::ExtCost { ext_cost_kind: ExtCosts::utf16_decoding_base } => 23, - Cost::ExtCost { ext_cost_kind: ExtCosts::utf16_decoding_byte } => 24, - Cost::ExtCost { ext_cost_kind: ExtCosts::sha256_base } => 25, - Cost::ExtCost { ext_cost_kind: ExtCosts::sha256_byte } => 26, - Cost::ExtCost { ext_cost_kind: ExtCosts::keccak256_base } => 27, - Cost::ExtCost { ext_cost_kind: ExtCosts::keccak256_byte } => 28, - Cost::ExtCost { ext_cost_kind: ExtCosts::keccak512_base } => 29, - Cost::ExtCost { ext_cost_kind: ExtCosts::keccak512_byte } => 30, - Cost::ExtCost { ext_cost_kind: ExtCosts::ripemd160_base } => 31, - Cost::ExtCost { ext_cost_kind: ExtCosts::ripemd160_block } => 32, - Cost::ExtCost { ext_cost_kind: ExtCosts::ecrecover_base } => 33, - Cost::ExtCost { ext_cost_kind: ExtCosts::log_base } => 34, - Cost::ExtCost { ext_cost_kind: ExtCosts::log_byte } => 35, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_write_base } => 36, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_write_key_byte } => 37, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_write_value_byte } => 38, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_write_evicted_byte } => 39, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_read_base } => 40, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_read_key_byte } => 41, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_read_value_byte } => 42, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_remove_base } => 43, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_remove_key_byte } => 44, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_remove_ret_value_byte } => 45, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_has_key_base } => 46, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_has_key_byte } => 47, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_iter_create_prefix_base } => 48, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_iter_create_prefix_byte } => 49, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_iter_create_range_base } => 50, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_iter_create_from_byte } => 51, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_iter_create_to_byte } => 52, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_iter_next_base } => 53, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_iter_next_key_byte } => 54, - Cost::ExtCost { ext_cost_kind: ExtCosts::storage_iter_next_value_byte } => 55, - Cost::ExtCost { ext_cost_kind: ExtCosts::touching_trie_node } => 56, - Cost::ExtCost { ext_cost_kind: ExtCosts::promise_and_base } => 57, - Cost::ExtCost { ext_cost_kind: ExtCosts::promise_and_per_promise } => 58, - Cost::ExtCost { ext_cost_kind: ExtCosts::promise_return } => 59, - Cost::ExtCost { ext_cost_kind: ExtCosts::validator_stake_base } => 60, - Cost::ExtCost { ext_cost_kind: ExtCosts::validator_total_stake_base } => 61, - Cost::WasmInstruction => 62, - Cost::ExtCost { ext_cost_kind: ExtCosts::read_cached_trie_node } => 63, - Cost::ExtCost { ext_cost_kind: ExtCosts::alt_bn128_g1_multiexp_base } => 64, - Cost::ExtCost { ext_cost_kind: ExtCosts::alt_bn128_g1_multiexp_element } => 65, - Cost::ExtCost { ext_cost_kind: ExtCosts::alt_bn128_pairing_check_base } => 66, - Cost::ExtCost { ext_cost_kind: ExtCosts::alt_bn128_pairing_check_element } => 67, - Cost::ExtCost { ext_cost_kind: ExtCosts::alt_bn128_g1_sum_base } => 68, - Cost::ExtCost { ext_cost_kind: ExtCosts::alt_bn128_g1_sum_element } => 69, - #[cfg(feature = "protocol_feature_ed25519_verify")] - Cost::ExtCost { ext_cost_kind: ExtCosts::ed25519_verify_base } => 70, - #[cfg(feature = "protocol_feature_ed25519_verify")] - Cost::ExtCost { ext_cost_kind: ExtCosts::ed25519_verify_byte } => 71, - } - } -} - -impl Index for ProfileData { - type Output = u64; - - fn index(&self, index: Cost) -> &Self::Output { - &self.data[index.profile_index()] - } -} - -impl IndexMut for ProfileData { - fn index_mut(&mut self, index: Cost) -> &mut Self::Output { - &mut self.data[index.profile_index()] - } -} - +/// Tests for ProfileDataV3 #[cfg(test)] mod test { use super::*; #[test] + #[cfg(not(feature = "nightly"))] fn test_profile_data_debug() { - let profile_data = ProfileData::new(); - println!("{:#?}", &profile_data); + let profile_data = ProfileDataV3::test(); + // we don't care about exact formatting, but the numbers should not change unexpectedly + let pretty_debug_str = format!("{profile_data:#?}"); + insta::assert_snapshot!(pretty_debug_str); } #[test] fn test_profile_data_debug_no_data() { - let profile_data = ProfileData::new(); + let profile_data = ProfileDataV3::default(); + // we don't care about exact formatting, but at least it should not panic println!("{:#?}", &profile_data); } #[test] fn test_no_panic_on_overflow() { - let mut profile_data = ProfileData::new(); + let mut profile_data = ProfileDataV3::default(); profile_data.add_action_cost(ActionCosts::add_full_access_key, u64::MAX); profile_data.add_action_cost(ActionCosts::add_full_access_key, u64::MAX); @@ -306,11 +241,11 @@ mod test { #[test] fn test_merge() { - let mut profile_data = ProfileData::new(); + let mut profile_data = ProfileDataV3::default(); profile_data.add_action_cost(ActionCosts::add_full_access_key, 111); profile_data.add_ext_cost(ExtCosts::storage_read_base, 11); - let mut profile_data2 = ProfileData::new(); + let mut profile_data2 = ProfileDataV3::default(); profile_data2.add_action_cost(ActionCosts::add_full_access_key, 222); profile_data2.add_ext_cost(ExtCosts::storage_read_base, 22); @@ -320,10 +255,73 @@ mod test { } #[test] - fn test_profile_len() { - let mut indices: Vec<_> = Cost::iter().map(|i| i.profile_index()).collect(); - indices.sort(); - indices.dedup(); - assert_eq!(indices.len(), DataArray::LEN); + fn test_borsh_ser_deser() { + let mut profile_data = ProfileDataV3::default(); + for (i, cost) in ExtCosts::iter().enumerate() { + profile_data.add_ext_cost(cost, i as Gas); + } + for (i, cost) in ActionCosts::iter().enumerate() { + profile_data.add_action_cost(cost, i as Gas + 1000); + } + let buf = profile_data.try_to_vec().expect("failed serializing a normal profile"); + + let restored: ProfileDataV3 = BorshDeserialize::deserialize(&mut buf.as_slice()) + .expect("failed deserializing a normal profile"); + + assert_eq!(profile_data, restored); + } + + #[test] + fn test_borsh_incomplete_profile() { + let action_profile = vec![50u64, 60]; + let ext_profile = vec![100u64, 200]; + let wasm_cost = 99u64; + let input = manually_encode_profile_v2(action_profile, ext_profile, wasm_cost); + + let profile: ProfileDataV3 = BorshDeserialize::deserialize(&mut input.as_slice()) + .expect("should be able to parse a profile with less entries"); + + assert_eq!(50, profile.get_action_cost(ActionCosts::create_account)); + assert_eq!(60, profile.get_action_cost(ActionCosts::delete_account)); + assert_eq!(0, profile.get_action_cost(ActionCosts::deploy_contract_base)); + + assert_eq!(100, profile.get_ext_cost(ExtCosts::base)); + assert_eq!(200, profile.get_ext_cost(ExtCosts::contract_loading_base)); + assert_eq!(0, profile.get_ext_cost(ExtCosts::contract_loading_bytes)); + } + + #[test] + fn test_borsh_larger_profile_than_current() { + let action_profile = vec![1234u64; ActionCosts::LENGTH + 5]; + let ext_profile = vec![5678u64; ExtCosts::LENGTH + 10]; + let wasm_cost = 90u64; + let input = manually_encode_profile_v2(action_profile, ext_profile, wasm_cost); + + let profile: ProfileDataV3 = BorshDeserialize::deserialize(&mut input.as_slice()).expect( + "should be able to parse a profile with more entries than the current version has", + ); + + for action in ActionCosts::iter() { + assert_eq!(1234, profile.get_action_cost(action), "{action:?}"); + } + + for ext in ExtCosts::iter() { + assert_eq!(5678, profile.get_ext_cost(ext), "{ext:?}"); + } + + assert_eq!(90, profile.wasm_gas); + } + + #[track_caller] + fn manually_encode_profile_v2( + action_profile: Vec, + ext_profile: Vec, + wasm_cost: u64, + ) -> Vec { + let mut input = vec![]; + BorshSerialize::serialize(&action_profile, &mut input).unwrap(); + BorshSerialize::serialize(&ext_profile, &mut input).unwrap(); + BorshSerialize::serialize(&wasm_cost, &mut input).unwrap(); + input } } diff --git a/core/primitives-core/src/profile/profile_v2.rs b/core/primitives-core/src/profile/profile_v2.rs new file mode 100644 index 00000000000..e0086cf55c0 --- /dev/null +++ b/core/primitives-core/src/profile/profile_v2.rs @@ -0,0 +1,265 @@ +use crate::config::{ActionCosts, ExtCosts}; +use crate::types::Gas; +use borsh::{BorshDeserialize, BorshSerialize}; +use std::fmt; +use std::ops::Index; +use strum::IntoEnumIterator; + +/// Deprecated serialization format to store profiles in the database. +/// +/// There is no ProfileDataV1 because meta data V1 did no have profiles. +/// Counting thus starts with 2 to match the meta data version numbers. +/// +/// This is not part of the protocol but archival nodes still rely on this not +/// changing to answer old tx-status requests with a gas profile. +/// +/// It used to store an array that manually mapped `enum Cost` to gas +/// numbers. Now `ProfileDataV2` and `Cost` are deprecated. But to lookup +/// old gas profiles from the DB, we need to keep the code around. +#[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +pub struct ProfileDataV2 { + data: DataArray, +} + +#[derive(Clone, PartialEq, Eq)] +struct DataArray(Box<[u64; Self::LEN]>); + +impl DataArray { + const LEN: usize = if cfg!(feature = "protocol_feature_ed25519_verify") { 72 } else { 70 }; +} + +impl ProfileDataV2 { + pub fn get_ext_cost(&self, ext: ExtCosts) -> u64 { + self[ext] + } + + pub fn get_wasm_cost(&self) -> u64 { + // ProfileV2Cost::WasmInstruction => 62, + self.data[62] + } + + fn host_gas(&self) -> u64 { + ExtCosts::iter().map(|a| self.get_ext_cost(a)).fold(0, u64::saturating_add) + } + + /// List action cost in the old way, which conflated several action parameters into one. + /// + /// This is used to display old gas profiles on the RPC API and in debug output. + pub fn legacy_action_costs(&self) -> Vec<(&'static str, Gas)> { + vec![ + ("CREATE_ACCOUNT", self.data[0]), + ("DELETE_ACCOUNT", self.data[1]), + ("DEPLOY_CONTRACT", self.data[2]), // contains per byte and base cost + ("FUNCTION_CALL", self.data[3]), // contains per byte and base cost + ("TRANSFER", self.data[4]), + ("STAKE", self.data[5]), + ("ADD_KEY", self.data[6]), // contains base + per byte cost for function call keys and full access keys + ("DELETE_KEY", self.data[7]), + ("NEW_DATA_RECEIPT_BYTE", self.data[8]), // contains the per-byte cost for sending back a data dependency + ("NEW_RECEIPT", self.data[9]), // contains base cost for data receipts and action receipts + ] + } + + pub fn action_gas(&self) -> u64 { + self.legacy_action_costs().iter().map(|(_name, cost)| *cost).fold(0, u64::saturating_add) + } + + /// Test instance with unique numbers in each field. + pub fn test() -> Self { + let mut profile_data = ProfileDataV2::default(); + let num_legacy_actions = 10; + for i in 0..num_legacy_actions { + profile_data.data.0[i] = i as Gas + 1000; + } + for i in num_legacy_actions..DataArray::LEN { + profile_data.data.0[i] = (i - num_legacy_actions) as Gas; + } + profile_data + } +} + +impl fmt::Debug for ProfileDataV2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use num_rational::Ratio; + let host_gas = self.host_gas(); + let action_gas = self.action_gas(); + + writeln!(f, "------------------------------")?; + writeln!(f, "Action gas: {}", action_gas)?; + writeln!(f, "------ Host functions --------")?; + for cost in ExtCosts::iter() { + let d = self.get_ext_cost(cost); + if d != 0 { + writeln!( + f, + "{} -> {} [{}% host]", + cost, + d, + Ratio::new(d * 100, core::cmp::max(host_gas, 1)).to_integer(), + )?; + } + } + writeln!(f, "------ Actions --------")?; + for (cost, gas) in self.legacy_action_costs() { + if gas != 0 { + writeln!(f, "{} -> {}", cost.to_ascii_lowercase(), gas)?; + } + } + writeln!(f, "------------------------------")?; + Ok(()) + } +} + +impl Index for DataArray { + type Output = u64; + + fn index(&self, index: usize) -> &Self::Output { + &self.0[index] + } +} + +impl Index for ProfileDataV2 { + type Output = u64; + + fn index(&self, cost: ActionCosts) -> &Self::Output { + let index = match cost { + ActionCosts::create_account => 0, + ActionCosts::delete_account => 1, + ActionCosts::deploy_contract_base => 2, + ActionCosts::deploy_contract_byte => 2, + ActionCosts::function_call_base => 3, + ActionCosts::function_call_byte => 3, + ActionCosts::transfer => 4, + ActionCosts::stake => 5, + ActionCosts::add_full_access_key => 6, + ActionCosts::add_function_call_key_base => 6, + ActionCosts::add_function_call_key_byte => 6, + ActionCosts::delete_key => 7, + ActionCosts::new_data_receipt_byte => 8, + ActionCosts::new_action_receipt => 9, + ActionCosts::new_data_receipt_base => 9, + // new costs added after profile v1 was deprecated don't have this entry + #[allow(unreachable_patterns)] + _ => return &0, + }; + &self.data[index] + } +} + +impl Index for ProfileDataV2 { + type Output = u64; + + fn index(&self, cost: ExtCosts) -> &Self::Output { + let index = match cost { + ExtCosts::base => 10, + ExtCosts::contract_loading_base => 11, + ExtCosts::contract_loading_bytes => 12, + ExtCosts::read_memory_base => 13, + ExtCosts::read_memory_byte => 14, + ExtCosts::write_memory_base => 15, + ExtCosts::write_memory_byte => 16, + ExtCosts::read_register_base => 17, + ExtCosts::read_register_byte => 18, + ExtCosts::write_register_base => 19, + ExtCosts::write_register_byte => 20, + ExtCosts::utf8_decoding_base => 21, + ExtCosts::utf8_decoding_byte => 22, + ExtCosts::utf16_decoding_base => 23, + ExtCosts::utf16_decoding_byte => 24, + ExtCosts::sha256_base => 25, + ExtCosts::sha256_byte => 26, + ExtCosts::keccak256_base => 27, + ExtCosts::keccak256_byte => 28, + ExtCosts::keccak512_base => 29, + ExtCosts::keccak512_byte => 30, + ExtCosts::ripemd160_base => 31, + ExtCosts::ripemd160_block => 32, + ExtCosts::ecrecover_base => 33, + ExtCosts::log_base => 34, + ExtCosts::log_byte => 35, + ExtCosts::storage_write_base => 36, + ExtCosts::storage_write_key_byte => 37, + ExtCosts::storage_write_value_byte => 38, + ExtCosts::storage_write_evicted_byte => 39, + ExtCosts::storage_read_base => 40, + ExtCosts::storage_read_key_byte => 41, + ExtCosts::storage_read_value_byte => 42, + ExtCosts::storage_remove_base => 43, + ExtCosts::storage_remove_key_byte => 44, + ExtCosts::storage_remove_ret_value_byte => 45, + ExtCosts::storage_has_key_base => 46, + ExtCosts::storage_has_key_byte => 47, + ExtCosts::storage_iter_create_prefix_base => 48, + ExtCosts::storage_iter_create_prefix_byte => 49, + ExtCosts::storage_iter_create_range_base => 50, + ExtCosts::storage_iter_create_from_byte => 51, + ExtCosts::storage_iter_create_to_byte => 52, + ExtCosts::storage_iter_next_base => 53, + ExtCosts::storage_iter_next_key_byte => 54, + ExtCosts::storage_iter_next_value_byte => 55, + ExtCosts::touching_trie_node => 56, + ExtCosts::promise_and_base => 57, + ExtCosts::promise_and_per_promise => 58, + ExtCosts::promise_return => 59, + ExtCosts::validator_stake_base => 60, + ExtCosts::validator_total_stake_base => 61, + ExtCosts::read_cached_trie_node => 63, + ExtCosts::alt_bn128_g1_multiexp_base => 64, + ExtCosts::alt_bn128_g1_multiexp_element => 65, + ExtCosts::alt_bn128_pairing_check_base => 66, + ExtCosts::alt_bn128_pairing_check_element => 67, + ExtCosts::alt_bn128_g1_sum_base => 68, + ExtCosts::alt_bn128_g1_sum_element => 69, + // new costs added after profile v1 was deprecated don't have this entry + #[allow(unreachable_patterns)] + _ => return &0, + }; + &self.data[index] + } +} + +impl BorshDeserialize for DataArray { + fn deserialize(buf: &mut &[u8]) -> Result { + let data_vec: Vec = BorshDeserialize::deserialize(buf)?; + let mut data_array = [0; Self::LEN]; + let len = Self::LEN.min(data_vec.len()); + data_array[0..len].copy_from_slice(&data_vec[0..len]); + Ok(Self(Box::new(data_array))) + } +} + +impl BorshSerialize for DataArray { + fn serialize(&self, writer: &mut W) -> Result<(), std::io::Error> { + let v: Vec<_> = self.0.as_ref().to_vec(); + BorshSerialize::serialize(&v, writer) + } +} + +impl Default for ProfileDataV2 { + fn default() -> Self { + let costs = DataArray(Box::new([0; DataArray::LEN])); + ProfileDataV2 { data: costs } + } +} + +/// Tests for ProfileDataV2 +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(not(feature = "nightly"))] + fn test_profile_data_debug() { + let profile_data = ProfileDataV2::test(); + // we don't care about exact formatting, but the numbers should not change unexpectedly + let pretty_debug_str = format!("{profile_data:#?}"); + insta::assert_snapshot!(pretty_debug_str); + } + + #[test] + fn test_profile_data_debug_no_data() { + let profile_data = ProfileDataV2::default(); + // we don't care about exact formatting, but at least it should not panic + println!("{:#?}", &profile_data); + } +} diff --git a/core/primitives-core/src/profile/snapshots/near_primitives_core__profile__profile_v2__test__profile_data_debug.snap b/core/primitives-core/src/profile/snapshots/near_primitives_core__profile__profile_v2__test__profile_data_debug.snap new file mode 100644 index 00000000000..00442a71dd8 --- /dev/null +++ b/core/primitives-core/src/profile/snapshots/near_primitives_core__profile__profile_v2__test__profile_data_debug.snap @@ -0,0 +1,78 @@ +--- +source: core/primitives-core/src/profile/profile_v2.rs +expression: pretty_debug_str +--- +------------------------------ +Action gas: 10045 +------ Host functions -------- +contract_loading_base -> 1 [0% host] +contract_loading_bytes -> 2 [0% host] +read_memory_base -> 3 [0% host] +read_memory_byte -> 4 [0% host] +write_memory_base -> 5 [0% host] +write_memory_byte -> 6 [0% host] +read_register_base -> 7 [0% host] +read_register_byte -> 8 [0% host] +write_register_base -> 9 [0% host] +write_register_byte -> 10 [0% host] +utf8_decoding_base -> 11 [0% host] +utf8_decoding_byte -> 12 [0% host] +utf16_decoding_base -> 13 [0% host] +utf16_decoding_byte -> 14 [0% host] +sha256_base -> 15 [0% host] +sha256_byte -> 16 [0% host] +keccak256_base -> 17 [0% host] +keccak256_byte -> 18 [1% host] +keccak512_base -> 19 [1% host] +keccak512_byte -> 20 [1% host] +ripemd160_base -> 21 [1% host] +ripemd160_block -> 22 [1% host] +ecrecover_base -> 23 [1% host] +log_base -> 24 [1% host] +log_byte -> 25 [1% host] +storage_write_base -> 26 [1% host] +storage_write_key_byte -> 27 [1% host] +storage_write_value_byte -> 28 [1% host] +storage_write_evicted_byte -> 29 [1% host] +storage_read_base -> 30 [1% host] +storage_read_key_byte -> 31 [1% host] +storage_read_value_byte -> 32 [1% host] +storage_remove_base -> 33 [1% host] +storage_remove_key_byte -> 34 [1% host] +storage_remove_ret_value_byte -> 35 [2% host] +storage_has_key_base -> 36 [2% host] +storage_has_key_byte -> 37 [2% host] +storage_iter_create_prefix_base -> 38 [2% host] +storage_iter_create_prefix_byte -> 39 [2% host] +storage_iter_create_range_base -> 40 [2% host] +storage_iter_create_from_byte -> 41 [2% host] +storage_iter_create_to_byte -> 42 [2% host] +storage_iter_next_base -> 43 [2% host] +storage_iter_next_key_byte -> 44 [2% host] +storage_iter_next_value_byte -> 45 [2% host] +touching_trie_node -> 46 [2% host] +read_cached_trie_node -> 53 [3% host] +promise_and_base -> 47 [2% host] +promise_and_per_promise -> 48 [2% host] +promise_return -> 49 [2% host] +validator_stake_base -> 50 [2% host] +validator_total_stake_base -> 51 [2% host] +alt_bn128_g1_multiexp_base -> 54 [3% host] +alt_bn128_g1_multiexp_element -> 55 [3% host] +alt_bn128_pairing_check_base -> 56 [3% host] +alt_bn128_pairing_check_element -> 57 [3% host] +alt_bn128_g1_sum_base -> 58 [3% host] +alt_bn128_g1_sum_element -> 59 [3% host] +------ Actions -------- +create_account -> 1000 +delete_account -> 1001 +deploy_contract -> 1002 +function_call -> 1003 +transfer -> 1004 +stake -> 1005 +add_key -> 1006 +delete_key -> 1007 +new_data_receipt_byte -> 1008 +new_receipt -> 1009 +------------------------------ + diff --git a/core/primitives-core/src/snapshots/near_primitives_core__profile__profile_v1__test__profile_data_debug.snap b/core/primitives-core/src/snapshots/near_primitives_core__profile__profile_v1__test__profile_data_debug.snap new file mode 100644 index 00000000000..115c4212678 --- /dev/null +++ b/core/primitives-core/src/snapshots/near_primitives_core__profile__profile_v1__test__profile_data_debug.snap @@ -0,0 +1,78 @@ +--- +source: core/primitives-core/src/profile.rs +expression: pretty_debug_str +--- +------------------------------ +Action gas: 10045 +------ Host functions -------- +contract_loading_base -> 1 [0% host] +contract_loading_bytes -> 2 [0% host] +read_memory_base -> 3 [0% host] +read_memory_byte -> 4 [0% host] +write_memory_base -> 5 [0% host] +write_memory_byte -> 6 [0% host] +read_register_base -> 7 [0% host] +read_register_byte -> 8 [0% host] +write_register_base -> 9 [0% host] +write_register_byte -> 10 [0% host] +utf8_decoding_base -> 11 [0% host] +utf8_decoding_byte -> 12 [0% host] +utf16_decoding_base -> 13 [0% host] +utf16_decoding_byte -> 14 [0% host] +sha256_base -> 15 [0% host] +sha256_byte -> 16 [0% host] +keccak256_base -> 17 [0% host] +keccak256_byte -> 18 [1% host] +keccak512_base -> 19 [1% host] +keccak512_byte -> 20 [1% host] +ripemd160_base -> 21 [1% host] +ripemd160_block -> 22 [1% host] +ecrecover_base -> 23 [1% host] +log_base -> 24 [1% host] +log_byte -> 25 [1% host] +storage_write_base -> 26 [1% host] +storage_write_key_byte -> 27 [1% host] +storage_write_value_byte -> 28 [1% host] +storage_write_evicted_byte -> 29 [1% host] +storage_read_base -> 30 [1% host] +storage_read_key_byte -> 31 [1% host] +storage_read_value_byte -> 32 [1% host] +storage_remove_base -> 33 [1% host] +storage_remove_key_byte -> 34 [1% host] +storage_remove_ret_value_byte -> 35 [2% host] +storage_has_key_base -> 36 [2% host] +storage_has_key_byte -> 37 [2% host] +storage_iter_create_prefix_base -> 38 [2% host] +storage_iter_create_prefix_byte -> 39 [2% host] +storage_iter_create_range_base -> 40 [2% host] +storage_iter_create_from_byte -> 41 [2% host] +storage_iter_create_to_byte -> 42 [2% host] +storage_iter_next_base -> 43 [2% host] +storage_iter_next_key_byte -> 44 [2% host] +storage_iter_next_value_byte -> 45 [2% host] +touching_trie_node -> 46 [2% host] +read_cached_trie_node -> 53 [3% host] +promise_and_base -> 47 [2% host] +promise_and_per_promise -> 48 [2% host] +promise_return -> 49 [2% host] +validator_stake_base -> 50 [2% host] +validator_total_stake_base -> 51 [2% host] +alt_bn128_g1_multiexp_base -> 54 [3% host] +alt_bn128_g1_multiexp_element -> 55 [3% host] +alt_bn128_pairing_check_base -> 56 [3% host] +alt_bn128_pairing_check_element -> 57 [3% host] +alt_bn128_g1_sum_base -> 58 [3% host] +alt_bn128_g1_sum_element -> 59 [3% host] +------ Actions -------- +create_account -> 1000 +delete_account -> 1001 +deploy_contract -> 1002 +function_call -> 1003 +transfer -> 1004 +stake -> 1005 +add_key -> 1006 +delete_key -> 1007 +new_data_receipt_byte -> 1008 +new_receipt -> 1009 +------------------------------ + diff --git a/core/primitives-core/src/snapshots/near_primitives_core__profile__test__profile_data_debug.snap b/core/primitives-core/src/snapshots/near_primitives_core__profile__test__profile_data_debug.snap new file mode 100644 index 00000000000..ce0a8fd6bc5 --- /dev/null +++ b/core/primitives-core/src/snapshots/near_primitives_core__profile__test__profile_data_debug.snap @@ -0,0 +1,83 @@ +--- +source: core/primitives-core/src/profile.rs +expression: pretty_debug_str +--- +------------------------------ +Action gas: 15105 +------ Host functions -------- +contract_loading_base -> 1 [0% host] +contract_loading_bytes -> 2 [0% host] +read_memory_base -> 3 [0% host] +read_memory_byte -> 4 [0% host] +write_memory_base -> 5 [0% host] +write_memory_byte -> 6 [0% host] +read_register_base -> 7 [0% host] +read_register_byte -> 8 [0% host] +write_register_base -> 9 [0% host] +write_register_byte -> 10 [0% host] +utf8_decoding_base -> 11 [0% host] +utf8_decoding_byte -> 12 [0% host] +utf16_decoding_base -> 13 [0% host] +utf16_decoding_byte -> 14 [0% host] +sha256_base -> 15 [0% host] +sha256_byte -> 16 [0% host] +keccak256_base -> 17 [0% host] +keccak256_byte -> 18 [1% host] +keccak512_base -> 19 [1% host] +keccak512_byte -> 20 [1% host] +ripemd160_base -> 21 [1% host] +ripemd160_block -> 22 [1% host] +ecrecover_base -> 23 [1% host] +log_base -> 24 [1% host] +log_byte -> 25 [1% host] +storage_write_base -> 26 [1% host] +storage_write_key_byte -> 27 [1% host] +storage_write_value_byte -> 28 [1% host] +storage_write_evicted_byte -> 29 [1% host] +storage_read_base -> 30 [1% host] +storage_read_key_byte -> 31 [1% host] +storage_read_value_byte -> 32 [1% host] +storage_remove_base -> 33 [1% host] +storage_remove_key_byte -> 34 [1% host] +storage_remove_ret_value_byte -> 35 [2% host] +storage_has_key_base -> 36 [2% host] +storage_has_key_byte -> 37 [2% host] +storage_iter_create_prefix_base -> 38 [2% host] +storage_iter_create_prefix_byte -> 39 [2% host] +storage_iter_create_range_base -> 40 [2% host] +storage_iter_create_from_byte -> 41 [2% host] +storage_iter_create_to_byte -> 42 [2% host] +storage_iter_next_base -> 43 [2% host] +storage_iter_next_key_byte -> 44 [2% host] +storage_iter_next_value_byte -> 45 [2% host] +touching_trie_node -> 46 [2% host] +read_cached_trie_node -> 47 [2% host] +promise_and_base -> 48 [2% host] +promise_and_per_promise -> 49 [2% host] +promise_return -> 50 [2% host] +validator_stake_base -> 51 [2% host] +validator_total_stake_base -> 52 [3% host] +alt_bn128_g1_multiexp_base -> 53 [3% host] +alt_bn128_g1_multiexp_element -> 54 [3% host] +alt_bn128_pairing_check_base -> 55 [3% host] +alt_bn128_pairing_check_element -> 56 [3% host] +alt_bn128_g1_sum_base -> 57 [3% host] +alt_bn128_g1_sum_element -> 58 [3% host] +------ Actions -------- +create_account -> 1000 +delete_account -> 1001 +deploy_contract_base -> 1002 +deploy_contract_byte -> 1003 +function_call_base -> 1004 +function_call_byte -> 1005 +transfer -> 1006 +stake -> 1007 +add_full_access_key -> 1008 +add_function_call_key_base -> 1009 +add_function_call_key_byte -> 1010 +delete_key -> 1011 +new_action_receipt -> 1012 +new_data_receipt_base -> 1013 +new_data_receipt_byte -> 1014 +------------------------------ + diff --git a/core/primitives/src/snapshots/near_primitives__views__tests__exec_metadata_v1_view.snap b/core/primitives/src/snapshots/near_primitives__views__tests__exec_metadata_v1_view.snap new file mode 100644 index 00000000000..38bc34efcdf --- /dev/null +++ b/core/primitives/src/snapshots/near_primitives__views__tests__exec_metadata_v1_view.snap @@ -0,0 +1,8 @@ +--- +source: core/primitives/src/views.rs +expression: view +--- +{ + "version": 1, + "gas_profile": null +} diff --git a/core/primitives/src/snapshots/near_primitives__views__tests__exec_metadata_v2_view.snap b/core/primitives/src/snapshots/near_primitives__views__tests__exec_metadata_v2_view.snap new file mode 100644 index 00000000000..e88d8d8ce0a --- /dev/null +++ b/core/primitives/src/snapshots/near_primitives__views__tests__exec_metadata_v2_view.snap @@ -0,0 +1,359 @@ +--- +source: core/primitives/src/views.rs +expression: view +--- +{ + "version": 2, + "gas_profile": [ + { + "cost_category": "ACTION_COST", + "cost": "ADD_KEY", + "gas_used": "1006" + }, + { + "cost_category": "ACTION_COST", + "cost": "CREATE_ACCOUNT", + "gas_used": "1000" + }, + { + "cost_category": "ACTION_COST", + "cost": "DELETE_ACCOUNT", + "gas_used": "1001" + }, + { + "cost_category": "ACTION_COST", + "cost": "DELETE_KEY", + "gas_used": "1007" + }, + { + "cost_category": "ACTION_COST", + "cost": "DEPLOY_CONTRACT", + "gas_used": "1002" + }, + { + "cost_category": "ACTION_COST", + "cost": "FUNCTION_CALL", + "gas_used": "1003" + }, + { + "cost_category": "ACTION_COST", + "cost": "NEW_DATA_RECEIPT_BYTE", + "gas_used": "1008" + }, + { + "cost_category": "ACTION_COST", + "cost": "NEW_RECEIPT", + "gas_used": "1009" + }, + { + "cost_category": "ACTION_COST", + "cost": "STAKE", + "gas_used": "1005" + }, + { + "cost_category": "ACTION_COST", + "cost": "TRANSFER", + "gas_used": "1004" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "ALT_BN128_G1_MULTIEXP_BASE", + "gas_used": "54" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "ALT_BN128_G1_MULTIEXP_ELEMENT", + "gas_used": "55" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "ALT_BN128_G1_SUM_BASE", + "gas_used": "58" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "ALT_BN128_G1_SUM_ELEMENT", + "gas_used": "59" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "ALT_BN128_PAIRING_CHECK_BASE", + "gas_used": "56" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "ALT_BN128_PAIRING_CHECK_ELEMENT", + "gas_used": "57" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "BASE", + "gas_used": "0" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "CONTRACT_LOADING_BASE", + "gas_used": "1" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "CONTRACT_LOADING_BYTES", + "gas_used": "2" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "ECRECOVER_BASE", + "gas_used": "23" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "KECCAK256_BASE", + "gas_used": "17" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "KECCAK256_BYTE", + "gas_used": "18" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "KECCAK512_BASE", + "gas_used": "19" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "KECCAK512_BYTE", + "gas_used": "20" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "LOG_BASE", + "gas_used": "24" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "LOG_BYTE", + "gas_used": "25" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "PROMISE_AND_BASE", + "gas_used": "47" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "PROMISE_AND_PER_PROMISE", + "gas_used": "48" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "PROMISE_RETURN", + "gas_used": "49" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "READ_CACHED_TRIE_NODE", + "gas_used": "53" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "READ_MEMORY_BASE", + "gas_used": "3" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "READ_MEMORY_BYTE", + "gas_used": "4" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "READ_REGISTER_BASE", + "gas_used": "7" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "READ_REGISTER_BYTE", + "gas_used": "8" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "RIPEMD160_BASE", + "gas_used": "21" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "RIPEMD160_BLOCK", + "gas_used": "22" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "SHA256_BASE", + "gas_used": "15" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "SHA256_BYTE", + "gas_used": "16" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_HAS_KEY_BASE", + "gas_used": "36" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_HAS_KEY_BYTE", + "gas_used": "37" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_ITER_CREATE_FROM_BYTE", + "gas_used": "41" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_ITER_CREATE_PREFIX_BASE", + "gas_used": "38" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_ITER_CREATE_PREFIX_BYTE", + "gas_used": "39" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_ITER_CREATE_RANGE_BASE", + "gas_used": "40" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_ITER_CREATE_TO_BYTE", + "gas_used": "42" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_ITER_NEXT_BASE", + "gas_used": "43" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_ITER_NEXT_KEY_BYTE", + "gas_used": "44" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_ITER_NEXT_VALUE_BYTE", + "gas_used": "45" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_READ_BASE", + "gas_used": "30" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_READ_KEY_BYTE", + "gas_used": "31" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_READ_VALUE_BYTE", + "gas_used": "32" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_REMOVE_BASE", + "gas_used": "33" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_REMOVE_KEY_BYTE", + "gas_used": "34" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_REMOVE_RET_VALUE_BYTE", + "gas_used": "35" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_WRITE_BASE", + "gas_used": "26" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_WRITE_EVICTED_BYTE", + "gas_used": "29" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_WRITE_KEY_BYTE", + "gas_used": "27" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_WRITE_VALUE_BYTE", + "gas_used": "28" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "TOUCHING_TRIE_NODE", + "gas_used": "46" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "UTF16_DECODING_BASE", + "gas_used": "13" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "UTF16_DECODING_BYTE", + "gas_used": "14" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "UTF8_DECODING_BASE", + "gas_used": "11" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "UTF8_DECODING_BYTE", + "gas_used": "12" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "VALIDATOR_STAKE_BASE", + "gas_used": "50" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "VALIDATOR_TOTAL_STAKE_BASE", + "gas_used": "51" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "WASM_INSTRUCTION", + "gas_used": "52" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "WRITE_MEMORY_BASE", + "gas_used": "5" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "WRITE_MEMORY_BYTE", + "gas_used": "6" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "WRITE_REGISTER_BASE", + "gas_used": "9" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "WRITE_REGISTER_BYTE", + "gas_used": "10" + } + ] +} diff --git a/core/primitives/src/snapshots/near_primitives__views__tests__exec_metadata_v3_view.snap b/core/primitives/src/snapshots/near_primitives__views__tests__exec_metadata_v3_view.snap new file mode 100644 index 00000000000..a9d3e193f14 --- /dev/null +++ b/core/primitives/src/snapshots/near_primitives__views__tests__exec_metadata_v3_view.snap @@ -0,0 +1,374 @@ +--- +source: core/primitives/src/views.rs +expression: view +--- +{ + "version": 3, + "gas_profile": [ + { + "cost_category": "ACTION_COST", + "cost": "ADD_FULL_ACCESS_KEY", + "gas_used": "1008" + }, + { + "cost_category": "ACTION_COST", + "cost": "ADD_FUNCTION_CALL_KEY_BASE", + "gas_used": "1009" + }, + { + "cost_category": "ACTION_COST", + "cost": "ADD_FUNCTION_CALL_KEY_BYTE", + "gas_used": "1010" + }, + { + "cost_category": "ACTION_COST", + "cost": "CREATE_ACCOUNT", + "gas_used": "1000" + }, + { + "cost_category": "ACTION_COST", + "cost": "DELETE_ACCOUNT", + "gas_used": "1001" + }, + { + "cost_category": "ACTION_COST", + "cost": "DELETE_KEY", + "gas_used": "1011" + }, + { + "cost_category": "ACTION_COST", + "cost": "DEPLOY_CONTRACT_BASE", + "gas_used": "1002" + }, + { + "cost_category": "ACTION_COST", + "cost": "DEPLOY_CONTRACT_BYTE", + "gas_used": "1003" + }, + { + "cost_category": "ACTION_COST", + "cost": "FUNCTION_CALL_BASE", + "gas_used": "1004" + }, + { + "cost_category": "ACTION_COST", + "cost": "FUNCTION_CALL_BYTE", + "gas_used": "1005" + }, + { + "cost_category": "ACTION_COST", + "cost": "NEW_ACTION_RECEIPT", + "gas_used": "1012" + }, + { + "cost_category": "ACTION_COST", + "cost": "NEW_DATA_RECEIPT_BASE", + "gas_used": "1013" + }, + { + "cost_category": "ACTION_COST", + "cost": "NEW_DATA_RECEIPT_BYTE", + "gas_used": "1014" + }, + { + "cost_category": "ACTION_COST", + "cost": "STAKE", + "gas_used": "1007" + }, + { + "cost_category": "ACTION_COST", + "cost": "TRANSFER", + "gas_used": "1006" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "ALT_BN128_G1_MULTIEXP_BASE", + "gas_used": "53" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "ALT_BN128_G1_MULTIEXP_ELEMENT", + "gas_used": "54" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "ALT_BN128_G1_SUM_BASE", + "gas_used": "57" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "ALT_BN128_G1_SUM_ELEMENT", + "gas_used": "58" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "ALT_BN128_PAIRING_CHECK_BASE", + "gas_used": "55" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "ALT_BN128_PAIRING_CHECK_ELEMENT", + "gas_used": "56" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "CONTRACT_LOADING_BASE", + "gas_used": "1" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "CONTRACT_LOADING_BYTES", + "gas_used": "2" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "ECRECOVER_BASE", + "gas_used": "23" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "KECCAK256_BASE", + "gas_used": "17" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "KECCAK256_BYTE", + "gas_used": "18" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "KECCAK512_BASE", + "gas_used": "19" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "KECCAK512_BYTE", + "gas_used": "20" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "LOG_BASE", + "gas_used": "24" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "LOG_BYTE", + "gas_used": "25" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "PROMISE_AND_BASE", + "gas_used": "48" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "PROMISE_AND_PER_PROMISE", + "gas_used": "49" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "PROMISE_RETURN", + "gas_used": "50" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "READ_CACHED_TRIE_NODE", + "gas_used": "47" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "READ_MEMORY_BASE", + "gas_used": "3" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "READ_MEMORY_BYTE", + "gas_used": "4" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "READ_REGISTER_BASE", + "gas_used": "7" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "READ_REGISTER_BYTE", + "gas_used": "8" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "RIPEMD160_BASE", + "gas_used": "21" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "RIPEMD160_BLOCK", + "gas_used": "22" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "SHA256_BASE", + "gas_used": "15" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "SHA256_BYTE", + "gas_used": "16" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_HAS_KEY_BASE", + "gas_used": "36" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_HAS_KEY_BYTE", + "gas_used": "37" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_ITER_CREATE_FROM_BYTE", + "gas_used": "41" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_ITER_CREATE_PREFIX_BASE", + "gas_used": "38" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_ITER_CREATE_PREFIX_BYTE", + "gas_used": "39" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_ITER_CREATE_RANGE_BASE", + "gas_used": "40" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_ITER_CREATE_TO_BYTE", + "gas_used": "42" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_ITER_NEXT_BASE", + "gas_used": "43" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_ITER_NEXT_KEY_BYTE", + "gas_used": "44" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_ITER_NEXT_VALUE_BYTE", + "gas_used": "45" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_READ_BASE", + "gas_used": "30" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_READ_KEY_BYTE", + "gas_used": "31" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_READ_VALUE_BYTE", + "gas_used": "32" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_REMOVE_BASE", + "gas_used": "33" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_REMOVE_KEY_BYTE", + "gas_used": "34" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_REMOVE_RET_VALUE_BYTE", + "gas_used": "35" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_WRITE_BASE", + "gas_used": "26" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_WRITE_EVICTED_BYTE", + "gas_used": "29" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_WRITE_KEY_BYTE", + "gas_used": "27" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "STORAGE_WRITE_VALUE_BYTE", + "gas_used": "28" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "TOUCHING_TRIE_NODE", + "gas_used": "46" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "UTF16_DECODING_BASE", + "gas_used": "13" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "UTF16_DECODING_BYTE", + "gas_used": "14" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "UTF8_DECODING_BASE", + "gas_used": "11" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "UTF8_DECODING_BYTE", + "gas_used": "12" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "VALIDATOR_STAKE_BASE", + "gas_used": "51" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "VALIDATOR_TOTAL_STAKE_BASE", + "gas_used": "52" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "WRITE_MEMORY_BASE", + "gas_used": "5" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "WRITE_MEMORY_BYTE", + "gas_used": "6" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "WRITE_REGISTER_BASE", + "gas_used": "9" + }, + { + "cost_category": "WASM_HOST_COST", + "cost": "WRITE_REGISTER_BYTE", + "gas_used": "10" + } + ] +} 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 new file mode 100644 index 00000000000..ba2ca5ad713 --- /dev/null +++ b/core/primitives/src/snapshots/near_primitives__views__tests__runtime_config_view.snap @@ -0,0 +1,201 @@ +--- +source: core/primitives/src/views.rs +expression: "&view" +--- +{ + "storage_amount_per_byte": "90900000000000000000", + "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 + } + }, + "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, + "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, + "limit_config": { + "max_gas_burnt": 200000000000000, + "max_stack_height": 16384, + "stack_limiter_version": 1, + "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, + "max_functions_number_per_contract": 10000, + "wasmer2_stack_limit": 204800, + "max_locals_per_contract": 1048576, + "account_id_validity_rules_version": 1 + } + }, + "account_creation_config": { + "min_allowed_top_level_account_length": 0, + "registrar_account_id": "registrar" + } +} diff --git a/core/primitives/src/transaction.rs b/core/primitives/src/transaction.rs index 46f82880ef4..40f32a32d4d 100644 --- a/core/primitives/src/transaction.rs +++ b/core/primitives/src/transaction.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use near_crypto::{PublicKey, Signature}; use near_o11y::pretty; -use near_primitives_core::profile::ProfileData; +use near_primitives_core::profile::{ProfileDataV2, ProfileDataV3}; use crate::account::AccessKey; use crate::errors::TxExecutionError; @@ -366,11 +366,12 @@ pub struct ExecutionOutcome { #[derive(BorshSerialize, BorshDeserialize, PartialEq, Clone, Eq, Debug)] pub enum ExecutionMetadata { - // V1: Empty Metadata + /// V1: Empty Metadata V1, - - // V2: With ProfileData - V2(ProfileData), + /// V2: With ProfileData by legacy `Cost` enum + V2(ProfileDataV2), + // V3: With ProfileData by gas parameters + V3(ProfileDataV3), } impl Default for ExecutionMetadata { diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index a1b4652e491..ccac1d4f5c7 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -17,6 +17,7 @@ use serde::{Deserialize, Serialize}; use near_crypto::{PublicKey, Signature}; use near_o11y::pretty; +use strum::IntoEnumIterator; use crate::account::{AccessKey, AccessKeyPermission, Account, FunctionCallPermission}; use crate::block::{Block, BlockHeader, Tip}; @@ -30,7 +31,6 @@ use crate::errors::TxExecutionError; use crate::hash::{hash, CryptoHash}; use crate::merkle::{combine_hash, MerklePath}; use crate::network::PeerId; -use crate::profile::Cost; use crate::receipt::{ActionReceipt, DataReceipt, DataReceiver, Receipt, ReceiptEnum}; use crate::runtime::config::RuntimeConfig; use crate::serialize::{base64_format, dec_format, option_base64_format}; @@ -1266,73 +1266,102 @@ impl Default for ExecutionMetadataView { impl From for ExecutionMetadataView { fn from(metadata: ExecutionMetadata) -> Self { - let gas_profile = match metadata { + let version = match metadata { + ExecutionMetadata::V1 => 1, + ExecutionMetadata::V2(_) => 2, + ExecutionMetadata::V3(_) => 3, + }; + let mut gas_profile = match metadata { ExecutionMetadata::V1 => None, ExecutionMetadata::V2(profile_data) => { - let mut costs: Vec<_> = - Cost::iter() - .filter(|&cost| profile_data[cost] > 0) - .map(|cost| CostGasUsed { - cost_category: match cost { - Cost::ActionCost { .. } => "ACTION_COST", - Cost::ExtCost { .. } => "WASM_HOST_COST", - Cost::WasmInstruction => "WASM_HOST_COST", - } - .to_string(), - cost: match cost { - // preserve old behavior that conflated some action - // costs for profile (duplicates are removed afterwards) - Cost::ActionCost { - action_cost_kind: - ActionCosts::deploy_contract_base - | ActionCosts::deploy_contract_byte, - } => "DEPLOY_CONTRACT".to_owned(), - Cost::ActionCost { - action_cost_kind: - ActionCosts::function_call_base - | ActionCosts::function_call_byte, - } => "FUNCTION_CALL".to_owned(), - Cost::ActionCost { - action_cost_kind: - ActionCosts::add_full_access_key - | ActionCosts::add_function_call_key_base - | ActionCosts::add_function_call_key_byte, - } => "ADD_KEY".to_owned(), - Cost::ActionCost { - action_cost_kind: - ActionCosts::new_action_receipt - | ActionCosts::new_data_receipt_base, - } => "NEW_RECEIPT".to_owned(), - // other costs have always been mapped one-to-one - Cost::ActionCost { action_cost_kind: action_cost } => { - format!("{:?}", action_cost).to_ascii_uppercase() - } - Cost::ExtCost { ext_cost_kind: ext_cost } => { - format!("{:?}", ext_cost).to_ascii_uppercase() - } - Cost::WasmInstruction => "WASM_INSTRUCTION".to_string(), - }, - gas_used: profile_data[cost], - }) - .collect(); + // Add actions, wasm op, and ext costs in groups. + + // actions should use the old format, since `ActionCosts` + // includes more detailed entries than were present in the old + // profile + let mut costs: Vec = profile_data + .legacy_action_costs() + .into_iter() + .filter(|&(_, gas)| gas > 0) + .map(|(name, gas)| CostGasUsed::action(name.to_string(), gas)) + .collect(); + + // wasm op is a single cost, for historical reasons it is inaccurately displayed as "wasm host" + costs.push(CostGasUsed::wasm_host( + "WASM_INSTRUCTION".to_string(), + profile_data.get_wasm_cost(), + )); + + // ext costs are 1-to-1, except for those added later which we will display as 0 + for ext_cost in ExtCosts::iter() { + costs.push(CostGasUsed::wasm_host( + format!("{:?}", ext_cost).to_ascii_uppercase(), + profile_data.get_ext_cost(ext_cost), + )); + } - // The order doesn't really matter, but the default one is just - // historical, which is especially unintuitive, so let's sort - // lexicographically. - // - // Can't `sort_by_key` here because lifetime inference in - // closures is limited. - costs.sort_by(|lhs, rhs| { - lhs.cost_category.cmp(&rhs.cost_category).then(lhs.cost.cmp(&rhs.cost)) - }); + Some(costs) + } + ExecutionMetadata::V3(profile) => { + // Add actions, wasm op, and ext costs in groups. + // actions costs are 1-to-1 + let mut costs: Vec = ActionCosts::iter() + .flat_map(|cost| { + let gas_used = profile.get_action_cost(cost); + (gas_used > 0).then(|| { + CostGasUsed::action( + format!("{:?}", cost).to_ascii_uppercase(), + gas_used, + ) + }) + }) + .collect(); + + // wasm op is a single cost, for historical reasons it is inaccurately displayed as "wasm host" + let wasm_gas_used = profile.get_wasm_cost(); + if wasm_gas_used > 0 { + costs.push(CostGasUsed::wasm_host( + "WASM_INSTRUCTION".to_string(), + wasm_gas_used, + )); + } - // need to remove duplicate entries due to cost conflation - costs.dedup(); + // ext costs are 1-to-1 + for ext_cost in ExtCosts::iter() { + let gas_used = profile.get_ext_cost(ext_cost); + if gas_used > 0 { + costs.push(CostGasUsed::wasm_host( + format!("{:?}", ext_cost).to_ascii_uppercase(), + gas_used, + )); + } + } Some(costs) } }; - ExecutionMetadataView { version: 1, gas_profile } + if let Some(ref mut costs) = gas_profile { + // The order doesn't really matter, but the default one is just + // historical, which is especially unintuitive, so let's sort + // lexicographically. + // + // Can't `sort_by_key` here because lifetime inference in + // closures is limited. + costs.sort_by(|lhs, rhs| { + lhs.cost_category.cmp(&rhs.cost_category).then_with(|| lhs.cost.cmp(&rhs.cost)) + }); + } + ExecutionMetadataView { version, gas_profile } + } +} + +impl CostGasUsed { + pub fn action(cost: String, gas_used: Gas) -> Self { + Self { cost_category: "ACTION_COST".to_string(), cost, gas_used } + } + + pub fn wasm_host(cost: String, gas_used: Gas) -> Self { + Self { cost_category: "WASM_HOST_COST".to_string(), cost, gas_used } } } @@ -2166,7 +2195,7 @@ impl From for RuntimeConfigView { } } -// reverse direction: rosetta adapter uses this, also we use to test that all fields are present in view (TODO) +// reverse direction: rosetta adapter uses this, also we use to test that all fields are present in view impl From for RuntimeConfig { fn from(config: RuntimeConfigView) -> Self { Self { @@ -2569,3 +2598,62 @@ impl From for near_primitives_core::config::ExtCostsConfig { Self { costs } } } + +#[cfg(test)] +mod tests { + #[cfg(not(feature = "nightly"))] + use super::ExecutionMetadataView; + use super::RuntimeConfigView; + use crate::runtime::config::RuntimeConfig; + #[cfg(not(feature = "nightly"))] + use crate::transaction::ExecutionMetadata; + #[cfg(not(feature = "nightly"))] + use near_primitives_core::profile::{ProfileDataV2, ProfileDataV3}; + + /// The JSON representation used in RPC responses must not remove or rename + /// fields, only adding fields is allowed or we risk breaking clients. + #[test] + #[cfg(not(feature = "nightly"))] + fn test_runtime_config_view() { + let config = RuntimeConfig::test(); + let view = RuntimeConfigView::from(config); + insta::assert_json_snapshot!(&view); + } + + /// A `RuntimeConfigView` must contain all info to reconstruct a `RuntimeConfig`. + #[test] + fn test_runtime_config_view_is_complete() { + let config = RuntimeConfig::test(); + let view = RuntimeConfigView::from(config.clone()); + let reconstructed_config = RuntimeConfig::from(view); + + assert_eq!(config, reconstructed_config); + } + + /// `ExecutionMetadataView` with profile V1 displayed on the RPC should not change. + #[test] + #[cfg(not(feature = "nightly"))] + fn test_exec_metadata_v1_view() { + let metadata = ExecutionMetadata::V1; + let view = ExecutionMetadataView::from(metadata); + insta::assert_json_snapshot!(view); + } + + /// `ExecutionMetadataView` with profile V2 displayed on the RPC should not change. + #[test] + #[cfg(not(feature = "nightly"))] + fn test_exec_metadata_v2_view() { + let metadata = ExecutionMetadata::V2(ProfileDataV2::test()); + let view = ExecutionMetadataView::from(metadata); + insta::assert_json_snapshot!(view); + } + + /// `ExecutionMetadataView` with profile V3 displayed on the RPC should not change. + #[test] + #[cfg(not(feature = "nightly"))] + fn test_exec_metadata_v3_view() { + let metadata = ExecutionMetadata::V3(ProfileDataV3::test()); + let view = ExecutionMetadataView::from(metadata); + insta::assert_json_snapshot!(view); + } +} 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 19a8998c75b..b3ddd536019 100644 --- a/integration-tests/src/tests/client/features/chunk_nodes_cache.rs +++ b/integration-tests/src/tests/client/features/chunk_nodes_cache.rs @@ -133,7 +133,8 @@ fn compare_node_counts() { let metadata = receipt_execution_outcome.outcome_with_id.outcome.metadata; match metadata { ExecutionMetadata::V1 => panic!("ExecutionMetadata cannot be empty"), - ExecutionMetadata::V2(profile_data) => TrieNodesCount { + ExecutionMetadata::V2(_profile_data) => panic!("expected newest ExecutionMetadata"), + ExecutionMetadata::V3(profile_data) => TrieNodesCount { db_reads: { let cost = profile_data.get_ext_cost(ExtCosts::touching_trie_node); assert_eq!(cost % touching_trie_node_cost, 0); diff --git a/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile.snap b/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile.snap index 516c8ba19a0..23dc32f5334 100644 --- a/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile.snap +++ b/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile.snap @@ -6,8 +6,18 @@ expression: receipts_gas_profile [ CostGasUsed { cost_category: "ACTION_COST", - cost: "ADD_KEY", - gas_used: 203992376655, + cost: "ADD_FULL_ACCESS_KEY", + gas_used: 101765125000, + }, + CostGasUsed { + cost_category: "ACTION_COST", + cost: "ADD_FUNCTION_CALL_KEY_BASE", + gas_used: 102217625000, + }, + CostGasUsed { + cost_category: "ACTION_COST", + cost: "ADD_FUNCTION_CALL_KEY_BYTE", + gas_used: 9626655, }, CostGasUsed { cost_category: "ACTION_COST", @@ -26,17 +36,27 @@ expression: receipts_gas_profile }, CostGasUsed { cost_category: "ACTION_COST", - cost: "DEPLOY_CONTRACT", - gas_used: 184997391966, + cost: "DEPLOY_CONTRACT_BASE", + gas_used: 184765750000, + }, + CostGasUsed { + cost_category: "ACTION_COST", + cost: "DEPLOY_CONTRACT_BYTE", + gas_used: 231641966, + }, + CostGasUsed { + cost_category: "ACTION_COST", + cost: "FUNCTION_CALL_BASE", + gas_used: 20878753500000, }, CostGasUsed { cost_category: "ACTION_COST", - cost: "FUNCTION_CALL", - gas_used: 20878961441862, + cost: "FUNCTION_CALL_BYTE", + gas_used: 207941862, }, CostGasUsed { cost_category: "ACTION_COST", - cost: "NEW_RECEIPT", + cost: "NEW_ACTION_RECEIPT", gas_used: 1480548358496, }, CostGasUsed { diff --git a/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile_nightly.snap b/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile_nightly.snap index 516c8ba19a0..23dc32f5334 100644 --- a/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile_nightly.snap +++ b/integration-tests/src/tests/runtime/snapshots/integration_tests__tests__runtime__sanity_checks__receipts_gas_profile_nightly.snap @@ -6,8 +6,18 @@ expression: receipts_gas_profile [ CostGasUsed { cost_category: "ACTION_COST", - cost: "ADD_KEY", - gas_used: 203992376655, + cost: "ADD_FULL_ACCESS_KEY", + gas_used: 101765125000, + }, + CostGasUsed { + cost_category: "ACTION_COST", + cost: "ADD_FUNCTION_CALL_KEY_BASE", + gas_used: 102217625000, + }, + CostGasUsed { + cost_category: "ACTION_COST", + cost: "ADD_FUNCTION_CALL_KEY_BYTE", + gas_used: 9626655, }, CostGasUsed { cost_category: "ACTION_COST", @@ -26,17 +36,27 @@ expression: receipts_gas_profile }, CostGasUsed { cost_category: "ACTION_COST", - cost: "DEPLOY_CONTRACT", - gas_used: 184997391966, + cost: "DEPLOY_CONTRACT_BASE", + gas_used: 184765750000, + }, + CostGasUsed { + cost_category: "ACTION_COST", + cost: "DEPLOY_CONTRACT_BYTE", + gas_used: 231641966, + }, + CostGasUsed { + cost_category: "ACTION_COST", + cost: "FUNCTION_CALL_BASE", + gas_used: 20878753500000, }, CostGasUsed { cost_category: "ACTION_COST", - cost: "FUNCTION_CALL", - gas_used: 20878961441862, + cost: "FUNCTION_CALL_BYTE", + gas_used: 207941862, }, CostGasUsed { cost_category: "ACTION_COST", - cost: "NEW_RECEIPT", + cost: "NEW_ACTION_RECEIPT", gas_used: 1480548358496, }, CostGasUsed { diff --git a/runtime/near-vm-logic/src/gas_counter.rs b/runtime/near-vm-logic/src/gas_counter.rs index ed54520863d..c1c08468554 100644 --- a/runtime/near-vm-logic/src/gas_counter.rs +++ b/runtime/near-vm-logic/src/gas_counter.rs @@ -4,7 +4,7 @@ use near_primitives_core::config::ExtCosts::read_cached_trie_node; use near_primitives_core::config::ExtCosts::touching_trie_node; use near_primitives_core::{ config::{ActionCosts, ExtCosts, ExtCostsConfig}, - profile::ProfileData, + profile::ProfileDataV3, types::Gas, }; use std::collections::HashMap; @@ -58,7 +58,7 @@ pub struct GasCounter { /// FIXME(nagisa): why do we store a copy both here and in the VMLogic??? ext_costs_config: ExtCostsConfig, /// Where to store profile data, if needed. - profile: ProfileData, + profile: ProfileDataV3, } impl GasCounter { @@ -274,7 +274,7 @@ impl GasCounter { self.prepaid_gas - self.used_gas() } - pub fn profile_data(&self) -> ProfileData { + pub fn profile_data(&self) -> ProfileDataV3 { self.profile.clone() } } diff --git a/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs index c9bbb587e7a..2fe169a702b 100644 --- a/runtime/near-vm-logic/src/logic.rs +++ b/runtime/near-vm-logic/src/logic.rs @@ -9,11 +9,11 @@ use byteorder::ByteOrder; use near_crypto::Secp256K1Signature; use near_primitives::checked_feature; use near_primitives::config::ViewConfig; +use near_primitives::profile::ProfileDataV3; use near_primitives::runtime::fees::RuntimeFeesConfig; use near_primitives::version::is_implicit_account_creation_enabled; use near_primitives_core::config::ExtCosts::*; use near_primitives_core::config::{ActionCosts, ExtCosts, VMConfig}; -use near_primitives_core::profile::ProfileData; use near_primitives_core::runtime::fees::{transfer_exec_fee, transfer_send_fee}; use near_primitives_core::types::{ AccountId, Balance, EpochHeight, Gas, ProtocolVersion, StorageUsage, @@ -2863,7 +2863,7 @@ pub struct VMOutcome { pub used_gas: Gas, pub logs: Vec, /// Data collected from making a contract call - pub profile: ProfileData, + pub profile: ProfileDataV3, pub action_receipts: Vec<(AccountId, ReceiptMetadata)>, pub aborted: Option, } @@ -2895,7 +2895,7 @@ impl VMOutcome { burnt_gas: 0, used_gas: 0, logs: Vec::new(), - profile: ProfileData::default(), + profile: ProfileDataV3::default(), action_receipts: Vec::new(), aborted: Some(error), } diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index c7edaf6ac59..65d98141074 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -11,7 +11,7 @@ pub use near_crypto; use near_crypto::PublicKey; pub use near_primitives; use near_primitives::contract::ContractCode; -use near_primitives::profile::ProfileData; +use near_primitives::profile::ProfileDataV3; pub use near_primitives::runtime::apply_state::ApplyState; use near_primitives::runtime::fees::RuntimeFeesConfig; use near_primitives::runtime::get_insufficient_storage_stake; @@ -134,7 +134,7 @@ pub struct ActionResult { pub logs: Vec, pub new_receipts: Vec, pub validator_proposals: Vec, - pub profile: ProfileData, + pub profile: ProfileDataV3, } impl ActionResult { @@ -732,7 +732,7 @@ impl Runtime { gas_burnt: result.gas_burnt, tokens_burnt, executor_id: account_id.clone(), - metadata: ExecutionMetadata::V2(result.profile), + metadata: ExecutionMetadata::V3(result.profile), }, }) } From d78787b37eebfdd56fd31d648bccaea8bfa30f50 Mon Sep 17 00:00:00 2001 From: nikurt <86772482+nikurt@users.noreply.github.com> Date: Wed, 11 Jan 2023 16:41:56 +0100 Subject: [PATCH 167/188] Remove field `Client::me` (#8278) because it is redundant and can be derived from a `pub` field `Client::validator_signer`. Pass `validator_id` explicitly to `persist_and_distribute_encoded_chunk()` to make it obvious that this function requires the node to be a validator. --- chain/client/src/client.rs | 26 +++++++------------ .../src/tests/client/challenges.rs | 15 +++++++++-- .../access_key_nonce_for_implicit_accounts.rs | 8 +++++- .../src/tests/client/process_blocks.rs | 7 ++++- 4 files changed, 35 insertions(+), 21 deletions(-) diff --git a/chain/client/src/client.rs b/chain/client/src/client.rs index 618fb20de3d..fcf91dfae28 100644 --- a/chain/client/src/client.rs +++ b/chain/client/src/client.rs @@ -100,7 +100,6 @@ pub struct Client { pub doomslug: Doomslug, pub runtime_adapter: Arc, pub shards_mgr: ShardsManager, - me: Option, pub sharded_tx_pool: ShardedTransactionPool, prev_block_to_chunk_headers_ready_for_inclusion: LruCache< CryptoHash, @@ -263,7 +262,6 @@ impl Client { doomslug, runtime_adapter, shards_mgr, - me, sharded_tx_pool, prev_block_to_chunk_headers_ready_for_inclusion: LruCache::new( CHUNK_HEADERS_FOR_INCLUSION_CACHE_SIZE, @@ -1453,14 +1451,12 @@ 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_signer.validator_id().clone(), - &block, - ); + self.remove_transactions_for_block(validator_id.clone(), &block); } BlockStatus::Fork => { // If it's a fork, no need to reconcile transactions or produce chunks @@ -1501,20 +1497,14 @@ impl Client { 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_signer.validator_id().clone(), - &block, - ); + 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_signer.validator_id().clone(), - &block, - ); + self.remove_transactions_for_block(validator_id.clone(), &block); } } } @@ -1535,7 +1525,7 @@ impl Client { .get_chunk_producer(&epoch_id, block.header().height() + 1, shard_id) .unwrap(); - if chunk_proposer == *validator_signer.validator_id() { + if &chunk_proposer == &validator_id { let _span = tracing::debug_span!( target: "client", "on_block_accepted_produce_chunk", @@ -1558,6 +1548,7 @@ impl Client { encoded_chunk, merkle_paths, receipts, + validator_id.clone(), ) .expect("Failed to process produced chunk"); } @@ -1578,17 +1569,18 @@ impl Client { encoded_chunk: EncodedShardChunk, merkle_paths: Vec, receipts: Vec, + validator_id: AccountId, ) -> Result<(), Error> { let (shard_chunk, partial_chunk) = decode_encoded_chunk( &encoded_chunk, merkle_paths.clone(), - self.me.as_ref(), + Some(&validator_id), self.runtime_adapter.as_ref(), )?; persist_chunk(partial_chunk.clone(), Some(shard_chunk), self.chain.mut_store())?; self.on_chunk_header_ready_for_inclusion( encoded_chunk.cloned_header(), - self.me.clone().unwrap(), + validator_id.clone(), ); self.shards_mgr.distribute_encoded_chunk( partial_chunk, diff --git a/integration-tests/src/tests/client/challenges.rs b/integration-tests/src/tests/client/challenges.rs index 2b745fbc6ca..e51b53afb0e 100644 --- a/integration-tests/src/tests/client/challenges.rs +++ b/integration-tests/src/tests/client/challenges.rs @@ -25,6 +25,7 @@ use near_primitives::sharding::{EncodedShardChunk, ReedSolomonWrapper}; use near_primitives::transaction::SignedTransaction; use near_primitives::types::chunk_extra::ChunkExtra; use near_primitives::types::{AccountId, EpochId}; +use near_primitives::validator_signer::ValidatorSigner; use near_primitives::version::PROTOCOL_VERSION; use near_store::test_utils::create_test_store; use near_store::Trie; @@ -380,7 +381,12 @@ fn test_verify_chunk_invalid_state_challenge() { // Receive invalid chunk to the validator. client - .persist_and_distribute_encoded_chunk(invalid_chunk.clone(), merkle_paths, vec![]) + .persist_and_distribute_encoded_chunk( + invalid_chunk.clone(), + merkle_paths, + vec![], + validator_signer.validator_id().clone(), + ) .unwrap(); match &mut invalid_chunk { @@ -498,7 +504,12 @@ fn test_receive_invalid_chunk_as_chunk_producer() { let (chunk, merkle_paths, receipts, block) = create_invalid_proofs_chunk(&mut env.clients[0]); let client = &mut env.clients[0]; assert!(client - .persist_and_distribute_encoded_chunk(chunk.clone(), merkle_paths.clone(), receipts.clone()) + .persist_and_distribute_encoded_chunk( + chunk.clone(), + merkle_paths.clone(), + receipts.clone(), + client.validator_signer.as_ref().unwrap().validator_id().clone() + ) .is_err()); let result = client.process_block_test(block.clone().into(), Provenance::NONE); // We have declined block with invalid chunk. diff --git a/integration-tests/src/tests/client/features/access_key_nonce_for_implicit_accounts.rs b/integration-tests/src/tests/client/features/access_key_nonce_for_implicit_accounts.rs index c02c2d7369d..f9070223eec 100644 --- a/integration-tests/src/tests/client/features/access_key_nonce_for_implicit_accounts.rs +++ b/integration-tests/src/tests/client/features/access_key_nonce_for_implicit_accounts.rs @@ -236,8 +236,14 @@ fn test_chunk_transaction_validity() { } let (encoded_shard_chunk, merkle_path, receipts, block) = create_chunk_with_transactions(&mut env.clients[0], vec![tx]); + let validator_id = env.clients[0].validator_signer.as_ref().unwrap().validator_id().clone(); env.clients[0] - .persist_and_distribute_encoded_chunk(encoded_shard_chunk, merkle_path, receipts) + .persist_and_distribute_encoded_chunk( + encoded_shard_chunk, + merkle_path, + receipts, + validator_id, + ) .unwrap(); let res = env.clients[0].process_block_test(block.into(), Provenance::NONE); assert_matches!(res.unwrap_err(), Error::InvalidTransactions); diff --git a/integration-tests/src/tests/client/process_blocks.rs b/integration-tests/src/tests/client/process_blocks.rs index 5aa59bd3b12..4b62ce03fbb 100644 --- a/integration-tests/src/tests/client/process_blocks.rs +++ b/integration-tests/src/tests/client/process_blocks.rs @@ -1377,6 +1377,7 @@ fn test_bad_chunk_mask() { encoded_chunk.clone(), merkle_paths.clone(), receipts.clone(), + client.validator_signer.as_ref().unwrap().validator_id().clone(), ) .unwrap(); } @@ -2305,8 +2306,9 @@ fn test_validate_chunk_extra() { let mut chain_store = ChainStore::new(env.clients[0].chain.store().store().clone(), genesis_height, true); let chunk_header = encoded_chunk.cloned_header(); + let validator_id = env.clients[0].validator_signer.as_ref().unwrap().validator_id().clone(); env.clients[0] - .persist_and_distribute_encoded_chunk(encoded_chunk, merkle_paths, receipts) + .persist_and_distribute_encoded_chunk(encoded_chunk, merkle_paths, receipts, validator_id) .unwrap(); env.clients[0].chain.blocks_with_missing_chunks.accept_chunk(&chunk_header.chunk_hash()); env.clients[0].process_blocks_with_missing_chunks(Arc::new(|_| {})); @@ -2772,11 +2774,14 @@ fn test_epoch_protocol_version_change() { create_chunk_on_height(&mut env.clients[index], i); for j in 0..2 { + let validator_id = + env.clients[j].validator_signer.as_ref().unwrap().validator_id().clone(); env.clients[j] .persist_and_distribute_encoded_chunk( encoded_chunk.clone(), merkle_paths.clone(), receipts.clone(), + validator_id, ) .unwrap(); } From 83a54be1429f8d09bcf17ed07d160e3656dae052 Mon Sep 17 00:00:00 2001 From: wacban Date: Wed, 11 Jan 2023 17:12:11 +0000 Subject: [PATCH 168/188] fix: fix cold store build (#8333) --- neard/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neard/Cargo.toml b/neard/Cargo.toml index f29b9e7e41d..bc22bf428e7 100644 --- a/neard/Cargo.toml +++ b/neard/Cargo.toml @@ -62,7 +62,7 @@ rosetta_rpc = ["nearcore/rosetta_rpc"] json_rpc = ["nearcore/json_rpc"] protocol_feature_fix_staking_threshold = ["nearcore/protocol_feature_fix_staking_threshold"] protocol_feature_flat_state = ["nearcore/protocol_feature_flat_state"] -cold_store = ["nearcore/cold_store", "near-store/cold_store", "near-cold-store-tool/cold_store"] +cold_store = ["nearcore/cold_store", "near-store/cold_store", "near-state-viewer/cold_store", "near-cold-store-tool/cold_store"] nightly = [ "nightly_protocol", From 400e12e5ace4160434a5060e655b75e677b21e9f Mon Sep 17 00:00:00 2001 From: Marcelo Diop-Gonzalez Date: Wed, 11 Jan 2023 13:34:08 -0500 Subject: [PATCH 169/188] fix: exit with nonzero code on init cmd error (#8334) https://github.com/near/nearcore/pull/5967 refactored things so that the init command wouldn't panic on normal/expected errors, but it inadvertenly changed things so that we always get exit code 0 even when the command fails, which is quite sad. So just return an `anyhow::Error` from `InitCmd::run()` --- neard/src/cli.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/neard/src/cli.rs b/neard/src/cli.rs index acc9101d212..e38753ed117 100644 --- a/neard/src/cli.rs +++ b/neard/src/cli.rs @@ -83,7 +83,7 @@ impl NeardCmd { }; match neard_cmd.subcmd { - NeardSubCommand::Init(cmd) => cmd.run(&home_dir), + NeardSubCommand::Init(cmd) => cmd.run(&home_dir)?, NeardSubCommand::Localnet(cmd) => cmd.run(&home_dir), NeardSubCommand::Run(cmd) => cmd.run( &home_dir, @@ -312,17 +312,16 @@ fn check_release_build(chain: &str) { } impl InitCmd { - pub(super) fn run(self, home_dir: &Path) { + pub(super) fn run(self, home_dir: &Path) -> anyhow::Result<()> { // TODO: Check if `home` exists. If exists check what networks we already have there. if (self.download_genesis || self.download_genesis_url.is_some()) && self.genesis.is_some() { - error!("Please give either --genesis or --download-genesis, not both."); - return; + anyhow::bail!("Please give either --genesis or --download-genesis, not both."); } self.chain_id.as_ref().map(|chain| check_release_build(chain)); - if let Err(e) = nearcore::init_configs( + nearcore::init_configs( home_dir, self.chain_id.as_deref(), self.account_id.and_then(|account_id| account_id.parse().ok()), @@ -337,9 +336,8 @@ impl InitCmd { self.download_config_url.as_deref(), self.boot_nodes.as_deref(), self.max_gas_burnt_view, - ) { - error!("Failed to initialize configs: {:#}", e); - } + ) + .context("Failed to initialize configs") } } From 4b6b8583bbd96b407921a8ea6a7dcd2190b102bc Mon Sep 17 00:00:00 2001 From: "R. N. West" <98110034+rnwst@users.noreply.github.com> Date: Wed, 11 Jan 2023 19:10:16 +0000 Subject: [PATCH 170/188] Remove superfluous layer tag from logo.svg (#8325) Should've done this on my previous commit, but only just noticed the layer tag. The ` + From 1f73d569b910eee666ea5a699d6a69774fc754f2 Mon Sep 17 00:00:00 2001 From: Simonas Kazlauskas Date: Thu, 12 Jan 2023 13:27:28 +0200 Subject: [PATCH 171/188] toolchain: update to 1.66.1 (#8336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes a MiTM vulnerability that has been identified in cargo when obtaining dependencies over ssh (note that the crates’ index is not affected – it is fetched via https). While this doesn’t appear to affect us, as we don’t have specified any ssh dependencies, it is still a good hygiene to be up-to date nevertheless. --- Cargo.toml | 2 +- rust-toolchain.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 69bad9d1353..31836f0424e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -241,4 +241,4 @@ opt-level = 3 [workspace.package] edition = "2021" authors = ["Near Inc "] -rust-version = "1.66.0" +rust-version = "1.66.1" diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 46ce70e1cfa..460d0404735 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.66.0" +channel = "1.66.1" components = [ "rustfmt" ] targets = [ "wasm32-unknown-unknown" ] From 05cb5b7d9dce332dc827d4f4c37375704dd2bcfc Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Thu, 12 Jan 2023 13:24:07 +0100 Subject: [PATCH 172/188] doc: gas profiles (#8331) * doc: gas profiles * Fix spelling and style --- docs/architecture/gas/gas_profile.md | 156 +++++++++++++++++++++++++-- 1 file changed, 149 insertions(+), 7 deletions(-) diff --git a/docs/architecture/gas/gas_profile.md b/docs/architecture/gas/gas_profile.md index b44d1585eb3..1b6f2bcc98c 100644 --- a/docs/architecture/gas/gas_profile.md +++ b/docs/architecture/gas/gas_profile.md @@ -1,9 +1,150 @@ # Gas Profile -The transaction runtime charges gas in various places around the code. The -charges end up as summaries inside an `ActionResult`. More specifically, the -`gas_burnt` and `gas_used` counters track the total gas required and the -`profile` field keeps track of what the gas was spent on. +What if you want to understand the exact gas spending of a smart contract call? +It would be very complicated to predict exactly how much gas executing a piece +of WASM code will require, including all host function calls and actions. An +easier approach is to just run the code on testnet and see how much gas it +burns. Gas profiles allow one to dig deeper and understand the breakdown of the +gas costs per parameter. + +**Gas profiles are not very reliable**, in that they are often incomplete and the +details of how they are computed can change without a protocol version bump. + +## Example Transaction Gas Profile + +You can query the gas profile of a transaction with +[NEAR CLI](https://docs.near.org/tools/near-cli). + +```bash +NEAR_ENV=mainnet near tx-status 8vYxsqYp5Kkfe8j9LsTqZRsEupNkAs1WvgcGcUE4MUUw --accountId app.nearcrowd.near + +Transaction app.nearcrowd.near:8vYxsqYp5Kkfe8j9LsTqZRsEupNkAs1WvgcGcUE4MUUw +{ + receipts_outcome: [ + { + block_hash: '2UVQKpxH6PhEqiKr6zMggqux4hwMrqqjpsbKrJG3vFXW', + id: '14bwmJF21PXY9YWGYN1jpjF3BRuyCKzgVWfhXhZBKH4u', + outcome: { + executor_id: 'app.nearcrowd.near', + gas_burnt: 5302170867180, + logs: [], + metadata: { + gas_profile: [ + { + cost: 'BASE', + cost_category: 'WASM_HOST_COST', + gas_used: '15091782327' + }, + { + cost: 'CONTRACT_LOADING_BASE', + cost_category: 'WASM_HOST_COST', + gas_used: '35445963' + }, + { + cost: 'CONTRACT_LOADING_BYTES', + cost_category: 'WASM_HOST_COST', + gas_used: '117474381750' + }, + { + cost: 'READ_CACHED_TRIE_NODE', + cost_category: 'WASM_HOST_COST', + gas_used: '615600000000' + }, + # ... + # skipping entries for presentation brevity + # ... + { + cost: 'WRITE_REGISTER_BASE', + cost_category: 'WASM_HOST_COST', + gas_used: '48713882262' + }, + { + cost: 'WRITE_REGISTER_BYTE', + cost_category: 'WASM_HOST_COST', + gas_used: '4797573768' + } + ], + version: 2 + }, + receipt_ids: [ '46Qsorkr6hy36ZzWmjPkjbgG28ko1iwz1NT25gvia51G' ], + status: { SuccessValue: 'ZmFsc2U=' }, + tokens_burnt: '530217086718000000000' + }, + proof: [ ... ] + }, + { ... } + ], + status: { SuccessValue: 'ZmFsc2U=' }, + transaction: { ... }, + transaction_outcome: { + block_hash: '7MgTTVi3aMG9LiGV8ezrNvoorUwQ7TwkJ4Wkbk3Fq5Uq', + id: '8vYxsqYp5Kkfe8j9LsTqZRsEupNkAs1WvgcGcUE4MUUw', + outcome: { + executor_id: 'evgeniya.near', + gas_burnt: 2428068571644, + ... + tokens_burnt: '242806857164400000000' + }, + } +} +``` + +The gas profile is in `receipts_outcome.outcome.metadata.gas_profile`. It shows +gas costs per parameter and with associated categories such as `WASM_HOST_COST` +or `ACTION_COST`. In the example, all costs are of the former category, which is +gas expended on smart contract execution. The latter is for gas spent on +actions. + +To be complete, the output above should also have a gas profile entry for the +function call action. But currently this is not included since gas profiles only +work properly on function call receipts. Improving this is planned, see +[nearcore#8261](https://github.com/near/nearcore/issues/8261). + +The `tx-status` query returns one gas profile for each receipt. The output above +contains a single gas profile because the transaction only spawned one receipt. +If there was a chain of cross contract calls, there would be multiple profiles. + +Besides receipts, also note the `transaction_outcome` in the output. It contains +the gas cost for converting the transaction into a receipt. To calculate the +full gas cost, add up the transaction cost with all receipt costs. + +The transaction outcome currently does not have a gas profile, it only shows the +total gas spent converting the transaction. Arguably, it is not necessary to +provide the gas profile since the costs only depend on the list of actions. With +sufficient understanding of the protocol, one could reverse-engineer the exact +breakdown simply by looking at the action list. But adding the profile would +still make sense to make it easier to understand. + +## Gas Profile Versions + +Depending on the version in `receipts_outcome.outcome.metadata.version`, you +should expect a different format of the gas profile. Version 1 has no profile +data at all. Version 2 has a detailed profile but some parameters are conflated, +so you cannot extract the exact gas spending in some cases. Version 3 will have +the cost exactly per parameter. + +Which version of the profile an RPC node returns depends on the version it had +when it first processed the transaction. The profiles are stored in the database +with one version and never updated. Therefore, older transactions will usually +only have old profiles. However, one could replay the chain from genesis with a +new nearcore client and generate the newest profile for all transactions in this +way. + +Note: Due to bugs, some nodes will claim they send version 1 but actually +send version 2. (Did I mention that profiles are unreliable?) + +## How Gas Profiles are Created + +The transaction runtime charges gas in various places around the code. +`ActionResult` keeps a summary of all costs for an action. The `gas_burnt` and +`gas_used` fields track the total gas burned and reserved for spawned receipts. +These two fields are crucial for the protocol to function correctly, as they are +used to determine when execution runs out of gas. + +Additionally, `ActionResult` also has a `profile` field which keeps a detailed +breakdown of the gas spending per parameter. Profiles are not stored on chain +but RPC nodes and archival nodes keep them in their databases. This is mostly a +debug tool and has no direct impact on the correct functioning of the protocol. ## Charging Gas Generally speaking, gas is charged right before the computation that it pays for @@ -20,6 +161,7 @@ Therefore, a fast gas counter exists that can be updated from within the VM. At the end of a function call execution, the gas counter is read by the host and merged into the `ActionResult`. - - - + From 63dc982cd3f0f9e8eeaf03a10a8c344b1b31f764 Mon Sep 17 00:00:00 2001 From: Aleksandr Logunov Date: Thu, 12 Jan 2023 18:40:34 +0400 Subject: [PATCH 173/188] feat: flat storage metrics (#8275) --- Cargo.lock | 1 + chain/chain/Cargo.toml | 1 + chain/chain/src/flat_storage_creator.rs | 55 +++++++++++-- core/store/src/flat_state.rs | 102 ++++++++++++++++++++---- core/store/src/lib.rs | 1 + core/store/src/metrics.rs | 94 ++++++++++++++++++++++ 6 files changed, 235 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b6a7d1aa2e..93288979685 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2844,6 +2844,7 @@ dependencies = [ "enum-map", "insta", "itertools", + "itoa 1.0.2", "lru", "near-cache", "near-chain-configs", diff --git a/chain/chain/Cargo.toml b/chain/chain/Cargo.toml index a454ed405ab..05c3316aa52 100644 --- a/chain/chain/Cargo.toml +++ b/chain/chain/Cargo.toml @@ -14,6 +14,7 @@ chrono.workspace = true crossbeam-channel.workspace = true enum-map.workspace = true itertools.workspace = true +itoa.workspace = true lru.workspace = true num-rational.workspace = true once_cell.workspace = true diff --git a/chain/chain/src/flat_storage_creator.rs b/chain/chain/src/flat_storage_creator.rs index 897f3e7a06f..75cf873a8ff 100644 --- a/chain/chain/src/flat_storage_creator.rs +++ b/chain/chain/src/flat_storage_creator.rs @@ -14,6 +14,7 @@ use crate::{ChainStore, ChainStoreAccess, RuntimeAdapter}; use assert_matches::assert_matches; use crossbeam_channel::{unbounded, Receiver, Sender}; use near_chain_primitives::Error; +use near_o11y::metrics::{IntCounter, IntGauge}; use near_primitives::shard_layout::ShardUId; use near_primitives::state::ValueRef; use near_primitives::state_part::PartId; @@ -28,7 +29,7 @@ use near_store::migrations::BatchedStoreUpdate; use near_store::DBCol; #[cfg(feature = "protocol_feature_flat_state")] use near_store::FlatStateDelta; -use near_store::Store; +use near_store::{Store, FLAT_STORAGE_HEAD_HEIGHT}; use near_store::{Trie, TrieDBStorage, TrieTraversalItem}; use std::sync::atomic::AtomicU64; use std::sync::Arc; @@ -36,6 +37,22 @@ use tracing::debug; #[cfg(feature = "protocol_feature_flat_state")] use tracing::info; +/// Metrics reporting about flat storage creation progress on each status update. +struct FlatStorageCreationMetrics { + #[allow(unused)] + status: IntGauge, + #[allow(unused)] + flat_head_height: IntGauge, + #[allow(unused)] + remaining_state_parts: IntGauge, + #[allow(unused)] + fetched_state_parts: IntCounter, + #[allow(unused)] + fetched_state_items: IntCounter, + #[allow(unused)] + threads_used: IntGauge, +} + /// If we launched a node with enabled flat storage but it doesn't have flat storage data on disk, we have to create it. /// This struct is responsible for this process for the given shard. /// See doc comment on [`FlatStorageStateStatus`] for the details of the process. @@ -58,7 +75,7 @@ pub struct FlatStorageShardCreator { #[allow(unused)] fetched_parts_receiver: Receiver, #[allow(unused)] - visited_trie_items: u64, + metrics: FlatStorageCreationMetrics, } impl FlatStorageShardCreator { @@ -72,6 +89,10 @@ impl FlatStorageShardCreator { runtime_adapter: Arc, ) -> Self { let (fetched_parts_sender, fetched_parts_receiver) = unbounded(); + // `itoa` is much faster for printing shard_id to a string than trivial alternatives. + let mut buffer = itoa::Buffer::new(); + let shard_id_label = buffer.format(shard_id); + Self { shard_id, start_height, @@ -79,7 +100,22 @@ impl FlatStorageShardCreator { remaining_state_parts: None, fetched_parts_sender, fetched_parts_receiver, - visited_trie_items: 0, + metrics: FlatStorageCreationMetrics { + status: near_store::flat_state_metrics::FLAT_STORAGE_CREATION_STATUS + .with_label_values(&[shard_id_label]), + flat_head_height: FLAT_STORAGE_HEAD_HEIGHT.with_label_values(&[shard_id_label]), + remaining_state_parts: + near_store::flat_state_metrics::FLAT_STORAGE_CREATION_REMAINING_STATE_PARTS + .with_label_values(&[shard_id_label]), + fetched_state_parts: + near_store::flat_state_metrics::FLAT_STORAGE_CREATION_FETCHED_STATE_PARTS + .with_label_values(&[shard_id_label]), + fetched_state_items: + near_store::flat_state_metrics::FLAT_STORAGE_CREATION_FETCHED_STATE_ITEMS + .with_label_values(&[shard_id_label]), + threads_used: near_store::flat_state_metrics::FLAT_STORAGE_CREATION_THREADS_USED + .with_label_values(&[shard_id_label]), + }, } } @@ -157,6 +193,7 @@ impl FlatStorageShardCreator { ) -> Result<(), Error> { let current_status = store_helper::get_flat_storage_state_status(chain_store.store(), self.shard_id); + self.metrics.status.set((¤t_status).into()); let shard_id = self.shard_id; match ¤t_status { FlatStorageStateStatus::SavingDeltas => { @@ -209,6 +246,7 @@ impl FlatStorageShardCreator { info!(target: "store", %shard_id, %final_height, ?status, "Switching status to fetching state"); let mut store_update = chain_store.store().store_update(); + self.metrics.flat_head_height.set(final_head.height as i64); store_helper::set_flat_head(&mut store_update, shard_id, &block_hash); store_helper::set_fetching_state_status(&mut store_update, shard_id, status); store_update.commit()?; @@ -222,6 +260,7 @@ impl FlatStorageShardCreator { let num_parts_in_step = fetching_state_status.num_parts_in_step; let num_parts = fetching_state_status.num_parts; let next_start_part_id = num_parts.min(start_part_id + num_parts_in_step); + self.metrics.remaining_state_parts.set((num_parts - start_part_id) as i64); match self.remaining_state_parts.clone() { None => { @@ -244,7 +283,9 @@ impl FlatStorageShardCreator { let inner_state_root = state_root.clone(); let inner_progress = progress.clone(); let inner_sender = self.fetched_parts_sender.clone(); + let inner_threads_used = self.metrics.threads_used.clone(); thread_pool.spawn(move || { + inner_threads_used.inc(); Self::fetch_state_part( inner_store, shard_uid, @@ -253,6 +294,7 @@ impl FlatStorageShardCreator { inner_progress, inner_sender, ); + inner_threads_used.dec(); }) } @@ -262,9 +304,10 @@ impl FlatStorageShardCreator { Some(state_parts) if state_parts > 0 => { // If not all state parts were fetched, try receiving new results. let mut updated_state_parts = state_parts; - while let Ok(n) = self.fetched_parts_receiver.try_recv() { + while let Ok(num_items) = self.fetched_parts_receiver.try_recv() { updated_state_parts -= 1; - self.visited_trie_items += n; + self.metrics.fetched_state_items.inc_by(num_items); + self.metrics.fetched_state_parts.inc(); } self.remaining_state_parts = Some(updated_state_parts); Ok(()) @@ -291,6 +334,7 @@ impl FlatStorageShardCreator { } else { // If all parts were fetched, we can start catchup. info!(target: "chain", %shard_id, %block_hash, "Finished fetching state"); + self.metrics.remaining_state_parts.set(0); store_helper::remove_fetching_state_status(&mut store_update, shard_id); store_helper::start_catchup(&mut store_update, shard_id); } @@ -329,6 +373,7 @@ impl FlatStorageShardCreator { let old_height = chain_store.get_block_height(&old_flat_head).unwrap(); let height = chain_store.get_block_height(&flat_head).unwrap(); debug!(target: "chain", %shard_id, %old_flat_head, %old_height, %flat_head, %height, "Catching up flat head"); + self.metrics.flat_head_height.set(height as i64); let mut store_update = self.runtime_adapter.store().store_update(); store_helper::set_flat_head(&mut store_update, shard_id, &flat_head); merged_delta.apply_to_flat_state(&mut store_update); diff --git a/core/store/src/flat_state.rs b/core/store/src/flat_state.rs index 5399edcff17..2db94ca758b 100644 --- a/core/store/src/flat_state.rs +++ b/core/store/src/flat_state.rs @@ -343,7 +343,7 @@ mod imp { use borsh::{BorshDeserialize, BorshSerialize}; -use crate::{CryptoHash, Store, StoreUpdate}; +use crate::{metrics, CryptoHash, Store, StoreUpdate}; pub use imp::{FlatState, FlatStateFactory}; use near_primitives::state::ValueRef; use near_primitives::types::{BlockHeight, RawStateChangesWithTrieKey, ShardId}; @@ -367,6 +367,11 @@ impl From<[(Vec, Option); N]> for FlatStateDelta { } impl FlatStateDelta { + /// Assumed number of bytes used to store an entry in the cache. + /// + /// Based on 36 bytes for `ValueRef` + guessed overhead of 24 bytes for `Vec` and `HashMap`. + pub(crate) const PER_ENTRY_OVERHEAD: u64 = 60; + /// Returns `Some(Option)` from delta for the given key. If key is not present, returns None. pub fn get(&self, key: &[u8]) -> Option> { self.0.get(key).cloned() @@ -381,6 +386,10 @@ impl FlatStateDelta { self.0.len() } + fn total_size(&self) -> u64 { + self.0.keys().map(|key| key.len() as u64 + Self::PER_ENTRY_OVERHEAD).sum() + } + /// Merge two deltas. Values from `other` should override values from `self`. pub fn merge(&mut self, other: &Self) { self.0.extend(other.0.iter().map(|(k, v)| (k.clone(), v.clone()))) @@ -424,6 +433,7 @@ impl FlatStateDelta { pub fn apply_to_flat_state(self, _store_update: &mut StoreUpdate) {} } +use near_o11y::metrics::IntGauge; use near_primitives::errors::StorageError; #[cfg(feature = "protocol_feature_flat_state")] use near_primitives::shard_layout::account_id_to_shard_id; @@ -486,6 +496,18 @@ struct FlatStorageStateInner { /// All these deltas here are stored on disk too. #[allow(unused)] deltas: HashMap>, + #[allow(unused)] + metrics: FlatStorageMetrics, +} + +struct FlatStorageMetrics { + flat_head_height: IntGauge, + cached_blocks: IntGauge, + cached_deltas: IntGauge, + cached_deltas_num_items: IntGauge, + cached_deltas_size: IntGauge, + #[allow(unused)] + distance_to_head: IntGauge, } /// Number of traversed parts during a single step of fetching state. @@ -536,6 +558,20 @@ pub enum FlatStorageStateStatus { DontCreate, } +impl Into for &FlatStorageStateStatus { + /// Converts status to integer to export to prometheus later. + /// Cast inside enum does not work because it is not fieldless. + fn into(self) -> i64 { + match self { + FlatStorageStateStatus::Ready => 0, + FlatStorageStateStatus::SavingDeltas => 1, + FlatStorageStateStatus::FetchingState(_) => 2, + FlatStorageStateStatus::CatchingUp => 3, + FlatStorageStateStatus::DontCreate => 4, + } + } +} + #[cfg(feature = "protocol_feature_flat_state")] pub mod store_helper { use crate::flat_state::{ @@ -786,6 +822,7 @@ impl FlatStorageStateInner { block_hash = block_info.prev_hash; } + self.metrics.distance_to_head.set(deltas.len() as i64); Ok(deltas) } @@ -816,6 +853,24 @@ impl FlatStorageState { }, )]); let mut deltas = HashMap::new(); + + // `itoa` is much faster for printing shard_id to a string than trivial alternatives. + let mut buffer = itoa::Buffer::new(); + let shard_id_label = buffer.format(shard_id); + let metrics = FlatStorageMetrics { + flat_head_height: metrics::FLAT_STORAGE_HEAD_HEIGHT + .with_label_values(&[shard_id_label]), + cached_blocks: metrics::FLAT_STORAGE_CACHED_BLOCKS.with_label_values(&[shard_id_label]), + cached_deltas: metrics::FLAT_STORAGE_CACHED_DELTAS.with_label_values(&[shard_id_label]), + cached_deltas_num_items: metrics::FLAT_STORAGE_CACHED_DELTAS_NUM_ITEMS + .with_label_values(&[shard_id_label]), + cached_deltas_size: metrics::FLAT_STORAGE_CACHED_DELTAS_SIZE + .with_label_values(&[shard_id_label]), + distance_to_head: metrics::FLAT_STORAGE_DISTANCE_TO_HEAD + .with_label_values(&[shard_id_label]), + }; + metrics.flat_head_height.set(flat_head_height as i64); + for height in flat_head_height + 1..=latest_block_height { for hash in chain_access.get_block_hashes_at_height(height) { let block_info = chain_access.get_block_info(&hash); @@ -828,17 +883,16 @@ impl FlatStorageState { block_info.height ); blocks.insert(hash, block_info); - deltas.insert( - hash, - store_helper::get_delta(&store, shard_id, hash) - .expect(BORSH_ERR) - .unwrap_or_else(|| { - panic!( - "Cannot find block delta for block {:?} shard {}", - hash, shard_id - ) - }), - ); + metrics.cached_blocks.inc(); + let delta = store_helper::get_delta(&store, shard_id, hash) + .expect(BORSH_ERR) + .unwrap_or_else(|| { + panic!("Cannot find block delta for block {:?} shard {}", hash, shard_id) + }); + metrics.cached_deltas.inc(); + metrics.cached_deltas_num_items.add(delta.len() as i64); + metrics.cached_deltas_size.add(delta.total_size() as i64); + deltas.insert(hash, delta); } } @@ -848,6 +902,7 @@ impl FlatStorageState { flat_head, blocks, deltas, + metrics, }))) } @@ -895,6 +950,7 @@ impl FlatStorageState { // 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 making removals lazy. let flat_head_height = guard.blocks.get(&guard.flat_head).unwrap().height; + guard.metrics.flat_head_height.set(flat_head_height as i64); let hashes_to_remove: Vec<_> = guard .blocks .iter() @@ -905,9 +961,23 @@ impl FlatStorageState { for hash in hashes_to_remove { // Note that we have to remove delta for new head but we still need to keep block info, e.g. for knowing // height of the head. - guard.deltas.remove(&hash); + // TODO (#7327): should we throw an error if delta/block is not present as we expect? + match guard.deltas.remove(&hash) { + Some(delta) => { + guard.metrics.cached_deltas.dec(); + guard.metrics.cached_deltas_num_items.sub(delta.len() as i64); + guard.metrics.cached_deltas_size.sub(delta.total_size() as i64); + } + None => {} + } + if &hash != new_head { - guard.blocks.remove(&hash); + match guard.blocks.remove(&hash) { + Some(_) => { + guard.metrics.cached_blocks.dec(); + } + None => {} + } } store_helper::remove_delta(&mut store_update, guard.shard_id, hash); } @@ -944,8 +1014,12 @@ impl FlatStorageState { } let mut store_update = StoreUpdate::new(guard.store.storage.clone()); store_helper::set_delta(&mut store_update, guard.shard_id, block_hash.clone(), &delta)?; + guard.metrics.cached_deltas.inc(); + guard.metrics.cached_deltas_num_items.add(delta.len() as i64); + guard.metrics.cached_deltas_size.add(delta.total_size() as i64); guard.deltas.insert(*block_hash, Arc::new(delta)); guard.blocks.insert(*block_hash, block); + guard.metrics.cached_blocks.inc(); Ok(store_update) } diff --git a/core/store/src/lib.rs b/core/store/src/lib.rs index 7928dc6b211..70e304c78e1 100644 --- a/core/store/src/lib.rs +++ b/core/store/src/lib.rs @@ -55,6 +55,7 @@ pub mod test_utils; mod trie; pub use crate::config::{Mode, StoreConfig}; +pub use crate::metrics::{flat_state_metrics, FLAT_STORAGE_HEAD_HEIGHT}; pub use crate::opener::{StoreMigrator, StoreOpener, StoreOpenerError}; /// Specifies temperature of a storage. diff --git a/core/store/src/metrics.rs b/core/store/src/metrics.rs index 213c5e6e93d..db7f249d7ca 100644 --- a/core/store/src/metrics.rs +++ b/core/store/src/metrics.rs @@ -222,3 +222,97 @@ pub static COLD_MIGRATION_READS: Lazy = Lazy::new(|| { ) .unwrap() }); + +pub static FLAT_STORAGE_HEAD_HEIGHT: Lazy = Lazy::new(|| { + try_create_int_gauge_vec( + "flat_storage_head_height", + "Height of flat storage head", + &["shard_id"], + ) + .unwrap() +}); +pub static FLAT_STORAGE_CACHED_BLOCKS: Lazy = Lazy::new(|| { + try_create_int_gauge_vec( + "flat_storage_cached_blocks", + "Number of cached blocks in flat storage", + &["shard_id"], + ) + .unwrap() +}); +pub static FLAT_STORAGE_CACHED_DELTAS: Lazy = Lazy::new(|| { + try_create_int_gauge_vec( + "flat_storage_cached_deltas", + "Number of cached deltas in flat storage", + &["shard_id"], + ) + .unwrap() +}); +pub static FLAT_STORAGE_CACHED_DELTAS_NUM_ITEMS: Lazy = Lazy::new(|| { + try_create_int_gauge_vec( + "flat_storage_cached_deltas_num_items", + "Number of items in all cached deltas in flat storage", + &["shard_id"], + ) + .unwrap() +}); +pub static FLAT_STORAGE_CACHED_DELTAS_SIZE: Lazy = Lazy::new(|| { + try_create_int_gauge_vec( + "flat_storage_cached_deltas_size", + "Total size of cached deltas in flat storage", + &["shard_id"], + ) + .unwrap() +}); +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", + &["shard_id"], + ) + .unwrap() +}); + +pub mod flat_state_metrics { + use super::*; + + pub static FLAT_STORAGE_CREATION_STATUS: Lazy = Lazy::new(|| { + try_create_int_gauge_vec( + "flat_storage_creation_status", + "Integer representing status of flat storage creation", + &["shard_id"], + ) + .unwrap() + }); + pub static FLAT_STORAGE_CREATION_REMAINING_STATE_PARTS: Lazy = Lazy::new(|| { + try_create_int_gauge_vec( + "flat_storage_creation_remaining_state_parts", + "Number of remaining state parts to fetch to fill flat storage in bytes", + &["shard_id"], + ) + .unwrap() + }); + pub static FLAT_STORAGE_CREATION_FETCHED_STATE_PARTS: Lazy = Lazy::new(|| { + try_create_int_counter_vec( + "flat_storage_creation_fetched_state_parts", + "Number of fetched state parts to fill flat storage in bytes", + &["shard_id"], + ) + .unwrap() + }); + pub static FLAT_STORAGE_CREATION_FETCHED_STATE_ITEMS: Lazy = Lazy::new(|| { + try_create_int_counter_vec( + "flat_storage_creation_fetched_state_items", + "Number of fetched items to fill flat storage", + &["shard_id"], + ) + .unwrap() + }); + pub static FLAT_STORAGE_CREATION_THREADS_USED: Lazy = Lazy::new(|| { + try_create_int_gauge_vec( + "flat_storage_creation_threads_used", + "Number of currently used threads to fetch state", + &["shard_id"], + ) + .unwrap() + }); +} From 3f9aa1e2bc2a0fa6b69f3e8ddf545699144b540e Mon Sep 17 00:00:00 2001 From: Simonas Kazlauskas Date: Thu, 12 Jan 2023 17:36:21 +0200 Subject: [PATCH 174/188] scripts: maintain clippy lints in a bash array (#8335) This is going to be a lil' bit more readable and scannable, especially as the list of lints we specify grows, which it is quite guaranteed to. --- scripts/run_clippy.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/run_clippy.sh b/scripts/run_clippy.sh index d2b80f5c38e..35459d50df2 100755 --- a/scripts/run_clippy.sh +++ b/scripts/run_clippy.sh @@ -1,7 +1,10 @@ #!/usr/bin/env bash # clippy adoption is in progress, see https://github.com/near/nearcore/issues/8145 -cargo clippy -- \ - -A clippy::all \ - -D clippy::correctness \ - -D clippy::suspicious +LINTS=( + -A clippy::all + -D clippy::correctness + -D clippy::suspicious +) + +cargo clippy -- "${LINTS[@]}" From cb46c0456ce0d0799c084a1546017354224f20e6 Mon Sep 17 00:00:00 2001 From: Aleksandr Logunov Date: Fri, 13 Jan 2023 17:12:02 +0400 Subject: [PATCH 175/188] docs: comments for check_triggers (#8342) An attempt to make comments related to logic in `check_triggers` easier to understand. Feel free to suggest changes if I got something wrong here. --- chain/client/src/client_actor.rs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/chain/client/src/client_actor.rs b/chain/client/src/client_actor.rs index fc8f5b4fefc..f5073457917 100644 --- a/chain/client/src/client_actor.rs +++ b/chain/client/src/client_actor.rs @@ -270,6 +270,13 @@ impl Actor for ClientActor { } impl ClientActor { + /// Wrapper for processing actix message which must be called after receiving it. + /// + /// Due to a bug in Actix library, while there are messages in mailbox, Actix + /// will prioritize processing messages until mailbox is empty. In such case execution + /// of any other task scheduled with `run_later` will be delayed. At the same time, + /// we have several important functions which have to be called regularly, so we put + /// these calls into `check_triggers` and call it here as a quick hack. fn wrap( &mut self, msg: WithSpanContext, @@ -1107,11 +1114,19 @@ impl ClientActor { }); } + /// Check if the scheduled time of any "triggers" has passed, and if so, call the trigger. + /// Triggers are important functions of client, like running single step of state sync or + /// checking if we can produce a block. + /// + /// It is called before processing Actix message and also in schedule_triggers. + /// This is to ensure all triggers enjoy higher priority than any actix message. + /// Otherwise due to a bug in Actix library Actix prioritizes processing messages + /// while there are messages in mailbox. Because of that we handle scheduling + /// triggers with custom `run_timer` function instead of `run_later` in Actix. + /// + /// Returns the delay before the next time `check_triggers` should be called, which is + /// min(time until the closest trigger, 1 second). fn check_triggers(&mut self, ctx: &mut Context) -> Duration { - // There is a bug in Actix library. While there are messages in mailbox, Actix - // will prioritize processing messages until mailbox is empty. Execution of any other task - // scheduled with run_later will be delayed. - // Check block height to trigger expected shutdown if let Ok(head) = self.client.chain.head() { let block_height_to_shutdown = @@ -1525,9 +1540,11 @@ impl ClientActor { ); } + /// Runs given callback if the time now is at least `next_attempt`. + /// Returns time for next run which should be made based on given `delay` between runs. fn run_timer( &mut self, - duration: Duration, + delay: Duration, next_attempt: DateTime, ctx: &mut Context, f: F, @@ -1546,7 +1563,7 @@ impl ClientActor { f(self, ctx); timer.observe_duration(); - now.checked_add_signed(chrono::Duration::from_std(duration).unwrap()).unwrap() + now.checked_add_signed(chrono::Duration::from_std(delay).unwrap()).unwrap() } fn sync_wait_period(&self) -> Duration { From 58840e4e2dec970745351d65062546e8ad93c70f Mon Sep 17 00:00:00 2001 From: Aleksandr Logunov Date: Fri, 13 Jan 2023 17:35:15 +0400 Subject: [PATCH 176/188] refactor: move background flat storage creaton to Client (#8262) As discussed in https://github.com/near/nearcore/pull/8193#discussion_r1052707068, flat storage creation makes more sense inside `Client` and `check_triggers`. `update_status` is a job which should be triggered periodically, and it doesn't have to be connected with finishing of block processing. To support that, we introduce config option `flat_storage_creation_period` which defines frequency with which creation status update will be triggered. Node owners could change it to higher values if this work executed in main thread is time consuming for some reasion. Also we fix `TestEnv::restart` a bit, because now we can call `cares_about_shard` in newly created client, and it fails, as described here: https://github.com/near/nearcore/issues/8269. P.S. It makes https://github.com/near/nearcore/issues/8254 not necessary because `Client` already has information about validator signer, what is even more convenient. ## Testing * test `test_flat_storage_creation` needed minor changes and still passes; * https://nayduck.near.org/#/run/2811: nayduck test `python3 pytest/tests/sanity/repro_2916.py` passes now - without a change, a node crashed on restart trying to create FS for non-tracked shard. --- chain/chain/src/chain.rs | 32 ++----- chain/chain/src/flat_storage_creator.rs | 83 ++++++++++--------- chain/chain/src/lib.rs | 2 +- chain/chain/src/test_utils/kv_runtime.rs | 4 + chain/chain/src/types.rs | 1 + chain/client/src/client.rs | 31 ++++++- chain/client/src/client_actor.rs | 22 +++++ chain/client/src/test_utils.rs | 19 +++-- core/chain-configs/src/client_config.rs | 3 + core/store/src/config.rs | 12 +++ .../src/tests/client/flat_storage.rs | 7 +- nearcore/src/config.rs | 1 + 12 files changed, 139 insertions(+), 78 deletions(-) diff --git a/chain/chain/src/chain.rs b/chain/chain/src/chain.rs index 6dc7eaa9572..49c308ff249 100644 --- a/chain/chain/src/chain.rs +++ b/chain/chain/src/chain.rs @@ -59,7 +59,6 @@ use crate::block_processing_utils::{ }; use crate::blocks_delay_tracker::BlocksDelayTracker; use crate::crypto_hash_timer::CryptoHashTimer; -use crate::flat_storage_creator::FlatStorageCreator; use crate::lightclient::get_epoch_block_producers_view; use crate::migrations::check_if_block_is_first_with_chunk_of_version; use crate::missing_chunks::{BlockLike, MissingChunksPool}; @@ -451,8 +450,6 @@ pub struct Chain { apply_chunks_receiver: Receiver, /// Time when head was updated most recently. last_time_head_updated: Instant, - /// Used when it is needed to create flat storage in background for some shards. - flat_storage_creator: Option, invalid_blocks: LruCache, @@ -534,7 +531,6 @@ impl Chain { apply_chunks_sender: sc, apply_chunks_receiver: rc, last_time_head_updated: Clock::instant(), - flat_storage_creator: None, invalid_blocks: LruCache::new(INVALID_CHUNKS_POOL_SIZE), pending_state_patch: Default::default(), requested_state_parts: StateRequestTracker::new(), @@ -653,13 +649,6 @@ impl Chain { }; store_update.commit()?; - // Create flat storage or initiate migration to flat storage. - let flat_storage_creator = FlatStorageCreator::new( - runtime_adapter.clone(), - &store, - chain_config.background_migration_threads, - ); - info!(target: "chain", "Init: header head @ #{} {}; block head @ #{} {}", header_head.height, header_head.last_block_hash, block_head.height, block_head.last_block_hash); @@ -692,7 +681,6 @@ impl Chain { apply_chunks_sender: sc, apply_chunks_receiver: rc, last_time_head_updated: Clock::instant(), - flat_storage_creator, pending_state_patch: Default::default(), requested_state_parts: StateRequestTracker::new(), }) @@ -2145,21 +2133,11 @@ impl Chain { } }); } else { - // If background flat storage creation was initiated, update its creation status, which means executing - // some work related to current creation step and possibly moving status forward until flat storage is - // finally created. - // Note that it doesn't work with state sync / catchup logic. - match &mut self.flat_storage_creator { - Some(flat_storage_creator) => { - flat_storage_creator.update_status(shard_id, &self.store)?; - } - None => { - // TODO (#8250): enable this assertion. Currently it doesn't work because runtime may be implemented - // with KeyValueRuntime which doesn't support flat storage. - // #[cfg(feature = "protocol_feature_flat_state")] - // debug_assert!(false, "Flat storage state for shard {shard_id} does not exist and its creation was not initiated"); - } - } + // 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. + // #[cfg(feature = "protocol_feature_flat_state")] + // debug_assert!(false, "Flat storage state for shard {shard_id} does not exist and its creation was not initiated"); } Ok(()) } diff --git a/chain/chain/src/flat_storage_creator.rs b/chain/chain/src/flat_storage_creator.rs index 75cf873a8ff..0969c8937e0 100644 --- a/chain/chain/src/flat_storage_creator.rs +++ b/chain/chain/src/flat_storage_creator.rs @@ -18,7 +18,7 @@ use near_o11y::metrics::{IntCounter, IntGauge}; use near_primitives::shard_layout::ShardUId; use near_primitives::state::ValueRef; use near_primitives::state_part::PartId; -use near_primitives::types::{BlockHeight, ShardId, StateRoot}; +use near_primitives::types::{AccountId, BlockHeight, ShardId, StateRoot}; use near_store::flat_state::FlatStorageStateStatus; #[cfg(feature = "protocol_feature_flat_state")] use near_store::flat_state::{store_helper, FetchingStateStatus}; @@ -31,6 +31,7 @@ use near_store::DBCol; use near_store::FlatStateDelta; use near_store::{Store, FLAT_STORAGE_HEAD_HEIGHT}; use near_store::{Trie, TrieDBStorage, TrieTraversalItem}; +use std::collections::HashMap; use std::sync::atomic::AtomicU64; use std::sync::Arc; use tracing::debug; @@ -185,12 +186,13 @@ impl FlatStorageShardCreator { /// Checks current flat storage creation status, execute work related to it and possibly switch to next status. /// Creates flat storage when all intermediate steps are finished. + /// Returns boolean indicating if flat storage was created. #[cfg(feature = "protocol_feature_flat_state")] pub(crate) fn update_status( &mut self, chain_store: &ChainStore, thread_pool: &rayon::ThreadPool, - ) -> Result<(), Error> { + ) -> Result { let current_status = store_helper::get_flat_storage_state_status(chain_store.store(), self.shard_id); self.metrics.status.set((¤t_status).into()); @@ -251,7 +253,6 @@ impl FlatStorageShardCreator { store_helper::set_fetching_state_status(&mut store_update, shard_id, status); store_update.commit()?; } - Ok(()) } FlatStorageStateStatus::FetchingState(fetching_state_status) => { let store = self.runtime_adapter.store().clone(); @@ -299,7 +300,6 @@ impl FlatStorageShardCreator { } self.remaining_state_parts = Some(next_start_part_id - start_part_id); - Ok(()) } Some(state_parts) if state_parts > 0 => { // If not all state parts were fetched, try receiving new results. @@ -310,7 +310,6 @@ impl FlatStorageShardCreator { self.metrics.fetched_state_parts.inc(); } self.remaining_state_parts = Some(updated_state_parts); - Ok(()) } Some(_) => { // Mark that we don't wait for new state parts. @@ -339,8 +338,6 @@ impl FlatStorageShardCreator { store_helper::start_catchup(&mut store_update, shard_id); } store_update.commit()?; - - Ok(()) } } } @@ -393,26 +390,28 @@ impl FlatStorageShardCreator { store_update.commit()?; } } - - Ok(()) } - FlatStorageStateStatus::Ready => Ok(()), + FlatStorageStateStatus::Ready => {} FlatStorageStateStatus::DontCreate => { panic!("We initiated flat storage creation for shard {shard_id} but according to flat storage state status in db it cannot be created"); } - } + }; + Ok(current_status == FlatStorageStateStatus::Ready) } } /// Creates flat storages for all shards. pub struct FlatStorageCreator { - pub shard_creators: Vec, + pub shard_creators: HashMap, /// Used to spawn threads for traversing state parts. pub pool: rayon::ThreadPool, } impl FlatStorageCreator { + /// For each of tracked shards, either creates flat storage if it is already stored on DB, + /// or starts migration to flat storage which updates DB in background and creates flat storage afterwards. pub fn new( + me: Option<&AccountId>, runtime_adapter: Arc, chain_store: &ChainStore, num_threads: usize, @@ -420,25 +419,30 @@ impl FlatStorageCreator { let chain_head = chain_store.head().unwrap(); let num_shards = runtime_adapter.num_shards(&chain_head.epoch_id).unwrap(); let start_height = chain_head.height; - let mut shard_creators: Vec = vec![]; + let mut shard_creators: HashMap = HashMap::new(); let mut creation_needed = false; for shard_id in 0..num_shards { - let status = runtime_adapter.try_create_flat_storage_state_for_shard( - shard_id, - chain_store.head().unwrap().height, - chain_store, - ); - match status { - FlatStorageStateStatus::Ready | FlatStorageStateStatus::DontCreate => {} - _ => { - creation_needed = true; + if runtime_adapter.cares_about_shard(me, &chain_head.prev_block_hash, shard_id, true) { + let status = runtime_adapter.try_create_flat_storage_state_for_shard( + shard_id, + chain_store.head().unwrap().height, + chain_store, + ); + match status { + FlatStorageStateStatus::Ready | FlatStorageStateStatus::DontCreate => {} + _ => { + creation_needed = true; + shard_creators.insert( + shard_id, + FlatStorageShardCreator::new( + shard_id, + start_height, + runtime_adapter.clone(), + ), + ); + } } } - shard_creators.push(FlatStorageShardCreator::new( - shard_id, - start_height, - runtime_adapter.clone(), - )); } if creation_needed { @@ -451,20 +455,25 @@ impl FlatStorageCreator { } } + /// Updates statuses of underlying flat storage creation processes. Returns boolean + /// indicating if all flat storages are created. pub fn update_status( &mut self, - shard_id: ShardId, - #[allow(unused_variables)] chain_store: &ChainStore, - ) -> Result<(), Error> { - if shard_id as usize >= self.shard_creators.len() { - // We can request update for not supported shard if resharding happens. We don't support it yet, so we just - // return Ok. - return Ok(()); - } + #[allow(unused)] chain_store: &ChainStore, + ) -> Result { + // TODO (#7327): If resharding happens, we may want to throw an error here. + // TODO (#7327): If flat storage is created, the creator probably should be removed. #[cfg(feature = "protocol_feature_flat_state")] - self.shard_creators[shard_id as usize].update_status(chain_store, &self.pool)?; + { + let mut all_created = true; + for shard_creator in self.shard_creators.values_mut() { + all_created &= shard_creator.update_status(chain_store, &self.pool)?; + } + Ok(all_created) + } - Ok(()) + #[cfg(not(feature = "protocol_feature_flat_state"))] + Ok(true) } } diff --git a/chain/chain/src/lib.rs b/chain/chain/src/lib.rs index 1c5d238bef8..bb68d0a3aba 100644 --- a/chain/chain/src/lib.rs +++ b/chain/chain/src/lib.rs @@ -14,7 +14,7 @@ pub mod chain; pub mod chunks_store; pub mod crypto_hash_timer; mod doomslug; -mod flat_storage_creator; +pub mod flat_storage_creator; mod lightclient; mod metrics; pub mod migrations; diff --git a/chain/chain/src/test_utils/kv_runtime.rs b/chain/chain/src/test_utils/kv_runtime.rs index e24eeb63033..d6761686fcd 100644 --- a/chain/chain/src/test_utils/kv_runtime.rs +++ b/chain/chain/src/test_utils/kv_runtime.rs @@ -241,6 +241,10 @@ impl KeyValueRuntime { Ok(None) } + /// Get epoch and index of validator set by the hash of previous block. + /// Note that it also fills in-memory chain info and there is some + /// assumption that it is called for all previous blocks. + /// TODO (#8269): should we call it recursively for previous blocks if info is not found? fn get_epoch_and_valset( &self, prev_hash: CryptoHash, diff --git a/chain/chain/src/types.rs b/chain/chain/src/types.rs index 1d804e4a203..1c9e94168ea 100644 --- a/chain/chain/src/types.rs +++ b/chain/chain/src/types.rs @@ -236,6 +236,7 @@ pub struct ChainGenesis { pub protocol_version: ProtocolVersion, } +#[derive(Clone)] pub struct ChainConfig { /// Whether to save `TrieChanges` on disk or not. pub save_trie_changes: bool, diff --git a/chain/client/src/client.rs b/chain/client/src/client.rs index fcf91dfae28..fe77842cc8c 100644 --- a/chain/client/src/client.rs +++ b/chain/client/src/client.rs @@ -19,6 +19,7 @@ use near_chain::chain::{ ApplyStatePartsRequest, BlockCatchUpRequest, BlockMissingChunks, BlocksCatchUpState, OrphanMissingChunks, StateSplitRequest, TX_ROUTING_HEIGHT_HORIZON, }; +use near_chain::flat_storage_creator::FlatStorageCreator; use near_chain::test_utils::format_hash; use near_chain::types::{ChainConfig, LatestKnown}; use near_chain::{ @@ -144,6 +145,8 @@ pub struct Client { /// Cached precomputed set of TIER1 accounts. /// See send_network_chain_info(). tier1_accounts_cache: Option<(EpochId, Arc)>, + /// Used when it is needed to create flat storage in background for some shards. + flat_storage_creator: Option, } // Debug information about the upcoming block. @@ -186,16 +189,24 @@ impl Client { } else { DoomslugThresholdMode::NoApprovals }; + let chain_config = ChainConfig { + save_trie_changes: !config.archive, + background_migration_threads: config.client_background_migration_threads, + }; let chain = Chain::new( runtime_adapter.clone(), &chain_genesis, doomslug_threshold_mode, - ChainConfig { - save_trie_changes: config.save_trie_changes, - background_migration_threads: config.client_background_migration_threads, - }, + chain_config.clone(), )?; let me = validator_signer.as_ref().map(|x| x.validator_id().clone()); + // Create flat storage or initiate migration to flat storage. + let flat_storage_creator = FlatStorageCreator::new( + me.as_ref(), + runtime_adapter.clone(), + chain.store(), + chain_config.background_migration_threads, + ); let shards_mgr = ShardsManager::new( me.clone(), runtime_adapter.clone(), @@ -284,6 +295,7 @@ impl Client { block_production_info: BlockProductionTracker::new(), chunk_production_info: lru::LruCache::new(PRODUCTION_TIMES_CACHE_SIZE), tier1_accounts_cache: None, + flat_storage_creator, }) } @@ -2196,6 +2208,17 @@ impl Client { // } Ok(()) } + + /// Check updates from background flat storage creation processes and possibly update + /// creation statuses. Returns boolean indicating if all flat storages are created or + /// creation is not needed. + pub fn run_flat_storage_creation_step(&mut self) -> Result { + let result = match &mut self.flat_storage_creator { + Some(flat_storage_creator) => flat_storage_creator.update_status(self.chain.store())?, + None => true, + }; + Ok(result) + } } /* implements functions used to communicate with network */ diff --git a/chain/client/src/client_actor.rs b/chain/client/src/client_actor.rs index f5073457917..cacf4d42350 100644 --- a/chain/client/src/client_actor.rs +++ b/chain/client/src/client_actor.rs @@ -251,6 +251,8 @@ impl Actor for ClientActor { type Context = Context; fn started(&mut self, ctx: &mut Self::Context) { + self.start_flat_storage_creation(ctx); + // Start syncing job. self.start_sync(ctx); @@ -1465,6 +1467,26 @@ impl ClientActor { !self.adv.disable_header_sync() && needs_syncing } + fn start_flat_storage_creation(&mut self, ctx: &mut Context) { + match self.client.run_flat_storage_creation_step() { + Ok(false) => {} + Ok(true) => { + return; + } + Err(err) => { + error!(target: "client", "Error occurred during flat storage creation step: {:?}", err); + } + } + + near_performance_metrics::actix::run_later( + ctx, + self.client.config.flat_storage_creation_period, + move |act, ctx| { + act.start_flat_storage_creation(ctx); + }, + ); + } + /// Starts syncing and then switches to either syncing or regular mode. fn start_sync(&mut self, ctx: &mut Context) { // Wait for connections reach at least minimum peers unless skipping sync. diff --git a/chain/client/src/test_utils.rs b/chain/client/src/test_utils.rs index 3282fe85ff4..eddb0f0b8dc 100644 --- a/chain/client/src/test_utils.rs +++ b/chain/client/src/test_utils.rs @@ -1635,26 +1635,29 @@ impl TestEnv { self.query_account(account_id).amount } - /// Restarts client at given index. Note that the client is restarted with - /// the default runtime adapter (i.e. [`KeyValueRuntime`]). That is, if - /// this `TestEnv` was created with custom runtime adapters that - /// customisation will be lost. + /// Restarts client at given index. Note that the new client reuses runtime + /// adapter of old client. + /// TODO (#8269): create new `KeyValueRuntime` for new client. Currently it + /// doesn't work because `KeyValueRuntime` misses info about new epochs in + /// memory caches. + /// Though, it seems that it is not necessary for current use cases. pub fn restart(&mut self, idx: usize) { - let store = self.clients[idx].chain.store().store().clone(); let account_id = self.get_client_id(idx).clone(); let rng_seed = match self.seeds.get(&account_id) { Some(seed) => *seed, None => TEST_SEED, }; let vs = ValidatorSchedule::new().block_producers_per_epoch(vec![self.validators.clone()]); - self.clients[idx] = setup_client( - store, - vs, + let num_validator_seats = vs.all_block_producers().count() as NumSeats; + let runtime_adapter = self.clients[idx].runtime_adapter.clone(); + self.clients[idx] = setup_client_with_runtime( + num_validator_seats, Some(self.get_client_id(idx).clone()), false, self.network_adapters[idx].clone(), self.client_adapters[idx].clone(), self.chain_genesis.clone(), + runtime_adapter, rng_seed, self.archive, self.save_trie_changes, diff --git a/core/chain-configs/src/client_config.rs b/core/chain-configs/src/client_config.rs index e4f198ba304..ad25750d3a5 100644 --- a/core/chain-configs/src/client_config.rs +++ b/core/chain-configs/src/client_config.rs @@ -162,6 +162,8 @@ pub struct ClientConfig { pub enable_statistics_export: bool, /// Number of threads to execute background migration work in client. pub client_background_migration_threads: usize, + /// Duration to perform background flat storage creation step. + pub flat_storage_creation_period: Duration, } impl ClientConfig { @@ -230,6 +232,7 @@ impl ClientConfig { max_gas_burnt_view: None, enable_statistics_export: true, client_background_migration_threads: 1, + flat_storage_creation_period: Duration::from_secs(1), } } } diff --git a/core/store/src/config.rs b/core/store/src/config.rs index e2385fc1c20..185b1cd407d 100644 --- a/core/store/src/config.rs +++ b/core/store/src/config.rs @@ -1,4 +1,5 @@ use near_primitives::shard_layout::ShardUId; +use std::time::Duration; use std::{collections::HashMap, iter::FromIterator}; use crate::trie::DEFAULT_SHARD_CACHE_TOTAL_SIZE_LIMIT; @@ -87,6 +88,11 @@ pub struct StoreConfig { /// Needed to create flat storage which need to happen in parallel /// with block processing. pub background_migration_threads: usize, + + /// Duration to perform background flat storage creation step. Defines how + /// frequently we check creation status and execute work related to it in + /// main thread (scheduling and collecting state parts, catching up blocks, etc.). + pub flat_storage_creation_period: Duration, } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -219,6 +225,12 @@ impl Default for StoreConfig { // We checked that this number of threads doesn't impact // regular block processing significantly. background_migration_threads: 8, + + // It shouldn't be very low, because on single flat storage creation step + // we do several disk reads from `FlatStateMisc` and `FlatStateDeltas`. + // One second should be enough to save deltas on start and catch up + // flat storage head quickly. State read work is much more expensive. + flat_storage_creation_period: Duration::from_secs(1), } } } diff --git a/integration-tests/src/tests/client/flat_storage.rs b/integration-tests/src/tests/client/flat_storage.rs index a60acc74841..d1e5d176eb4 100644 --- a/integration-tests/src/tests/client/flat_storage.rs +++ b/integration-tests/src/tests/client/flat_storage.rs @@ -106,9 +106,12 @@ fn test_flat_storage_creation() { assert_matches!(store_helper::get_delta(&store, 0, block_hash), Ok(Some(_))); } - // When final head height becomes greater than height on which node started, we must start fetching the state. + // Produce new block and run flat storage creation step. // We started the node from height 3, and now final head should move to height 4. + // Because final head height became greater than height on which node started, + // we must start fetching the state. env.produce_block(0, 6); + assert!(!env.clients[0].run_flat_storage_creation_step().unwrap()); let final_block_hash = env.clients[0].chain.get_block_hash_by_height(4).unwrap(); assert_eq!(store_helper::get_flat_head(&store, 0), Some(final_block_hash)); assert_eq!( @@ -129,6 +132,7 @@ fn test_flat_storage_creation() { let mut was_catching_up = false; while next_height < start_height + BLOCKS_TIMEOUT { env.produce_block(0, next_height); + env.clients[0].run_flat_storage_creation_step().unwrap(); next_height += 1; match store_helper::get_flat_storage_state_status(&store, 0) { FlatStorageStateStatus::FetchingState(..) => { @@ -159,5 +163,6 @@ fn test_flat_storage_creation() { } // Finally, check that flat storage state was created. + assert!(env.clients[0].run_flat_storage_creation_step().unwrap()); assert!(env.clients[0].runtime_adapter.get_flat_storage_state_for_shard(0).is_some()); } diff --git a/nearcore/src/config.rs b/nearcore/src/config.rs index 75a4ddfc5a2..f1a6db1f4f5 100644 --- a/nearcore/src/config.rs +++ b/nearcore/src/config.rs @@ -614,6 +614,7 @@ impl NearConfig { max_gas_burnt_view: config.max_gas_burnt_view, enable_statistics_export: config.store.enable_statistics_export, client_background_migration_threads: config.store.background_migration_threads, + flat_storage_creation_period: config.store.flat_storage_creation_period, }, network_config: NetworkConfig::new( config.network, From ee3fb3f560beca3b356dff7f1bb8a38308487abf Mon Sep 17 00:00:00 2001 From: Marcelo Diop-Gonzalez Date: Fri, 13 Jan 2023 09:32:27 -0500 Subject: [PATCH 177/188] fix(store-validator): fix outcome_indexed_by_block_hash() logic (#8301) https://github.com/near/nearcore/pull/7799 updated this function after changes to the way tx/receipt outcomes are stored on disk, but now it stops looking for the outcome after the first up-to-date chunk (since the check is inside the chunks loop). this leads to failed nayduck tests when --features test_features is in the build: https://nayduck.near.org/#/test/405653 --- chain/chain/src/store_validator/validate.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/chain/chain/src/store_validator/validate.rs b/chain/chain/src/store_validator/validate.rs index 7b15a332e96..c352e3c6bc1 100644 --- a/chain/chain/src/store_validator/validate.rs +++ b/chain/chain/src/store_validator/validate.rs @@ -692,7 +692,6 @@ pub(crate) fn outcome_indexed_by_block_hash( "Can't get Block {} from DB", block_hash ); - let mut outcome_ids = vec![]; for chunk_header in block.chunks().iter() { if chunk_header.height_included() == block.header().height() { let shard_uid = sv @@ -706,21 +705,20 @@ pub(crate) fn outcome_indexed_by_block_hash( DBCol::ChunkExtra, &get_block_shard_uid(block.hash(), &shard_uid), ) { - outcome_ids.extend(unwrap_or_err_db!( + let outcome_ids = unwrap_or_err_db!( sv.store.get_ser::>( DBCol::OutcomeIds, &get_block_shard_id(block.hash(), chunk_header.shard_id()) ), "Can't get Outcome ids by Block Hash" - )); + ); + if outcome_ids.contains(outcome_id) { + return Ok(()); + } } } - if !outcome_ids.contains(outcome_id) { - println!("outcome ids: {:?}, block: {:?}", outcome_ids, block); - err!("Outcome id {:?} is not found in DBCol::OutcomeIds", outcome_id); - } } - Ok(()) + err!("Outcome id {:?} is not found in DBCol::OutcomeIds", outcome_id) } pub(crate) fn state_sync_info_valid( From 048985a051b9f54cb02c88d4ce23b2f21f510c5a Mon Sep 17 00:00:00 2001 From: Ekleog-NEAR <96595974+Ekleog-NEAR@users.noreply.github.com> Date: Fri, 13 Jan 2023 15:45:38 +0100 Subject: [PATCH 178/188] Fix contract deployment detection command to avoid greedy match (#8345) * Fix contract deployment detection command to avoid greedy match * Make it explicit the command should be run on an archival node db Co-authored-by: near-bulldozer[bot] <73298989+near-bulldozer[bot]@users.noreply.github.com> --- docs/misc/database.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/misc/database.md b/docs/misc/database.md index 4ae9b1087c5..495d521eb03 100644 --- a/docs/misc/database.md +++ b/docs/misc/database.md @@ -25,11 +25,12 @@ We store the database in RocksDB. This document is an attempt to give hints abou - Contract deployments happen with `Column = 0x01` - `AdditionalInfo` is the account id for which the contract is being deployed - The key value contains the contract code alongside other pieces of data. It is possible to extract the contract code by removing everything until the wasm magic number, 0061736D01000000 -- As such, it is possible to dump all the contracts that were ever deployed on-chain using this command: +- As such, it is possible to dump all the contracts that were ever deployed on-chain using this command on an archival node: ``` - ldb --db=. scan --column_family=col35 --hex | \ + ldb --db=~/.near/data scan --column_family=col35 --hex | \ grep -E '^0x.{64}01' | \ - sed 's/^.*0061736D01000000/0061736D01000000/' | \ + sed 's/0061736D01000000/x/' | \ + sed 's/^.*x/0061736D01000000/' | \ grep -v ' : ' ``` (Note that the last grep is required because not every such value appears to contain contract code) From 056c62183e31e64cd6cacfc923a357775bc2b5c9 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Sat, 14 Jan 2023 21:32:04 +0100 Subject: [PATCH 179/188] runtime: use an allocated slice to represent mocked memory (#8298) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rather than allowing users of MockedMemory to read the entire address space, initialise it with an owned memory slice which represent contract’s memory. This complicates usage slightly since the memory must now be populated with data that contract code is supposed to read but it has two advantages. It makes MokcedMemory safe but more importantly it makes it possible to test contract function behaviour when addresses out of range are provided. For now, this commit introduces no such new tests. --- runtime/near-vm-logic/src/lib.rs | 1 + runtime/near-vm-logic/src/logic.rs | 10 + .../near-vm-logic/src/mocks/mock_memory.rs | 46 +- runtime/near-vm-logic/src/test_utils.rs | 71 +++ runtime/near-vm-logic/src/tests/alt_bn128.rs | 21 +- runtime/near-vm-logic/src/tests/context.rs | 11 +- .../near-vm-logic/src/tests/ed25519_verify.rs | 6 +- runtime/near-vm-logic/src/tests/fixtures.rs | 7 + .../near-vm-logic/src/tests/gas_counter.rs | 97 ++-- runtime/near-vm-logic/src/tests/helpers.rs | 102 ++-- runtime/near-vm-logic/src/tests/miscs.rs | 538 ++++++++---------- runtime/near-vm-logic/src/tests/mod.rs | 4 +- runtime/near-vm-logic/src/tests/promises.rs | 110 ++-- runtime/near-vm-logic/src/tests/registers.rs | 9 +- .../src/tests/storage_read_write.rs | 21 +- .../near-vm-logic/src/tests/storage_usage.rs | 28 +- .../src/tests/vm_logic_builder.rs | 97 +++- runtime/near-vm-logic/src/vmstate.rs | 12 +- runtime/near-vm-runner/src/memory.rs | 2 +- runtime/near-vm-runner/src/tests.rs | 74 +-- runtime/near-vm-runner/src/wasmer2_runner.rs | 2 +- 21 files changed, 647 insertions(+), 622 deletions(-) create mode 100644 runtime/near-vm-logic/src/test_utils.rs diff --git a/runtime/near-vm-logic/src/lib.rs b/runtime/near-vm-logic/src/lib.rs index 44927ad7244..65dc70dc1ff 100644 --- a/runtime/near-vm-logic/src/lib.rs +++ b/runtime/near-vm-logic/src/lib.rs @@ -7,6 +7,7 @@ pub mod gas_counter; mod logic; pub mod mocks; pub(crate) mod receipt_manager; +pub mod test_utils; #[cfg(test)] mod tests; pub mod types; diff --git a/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs index 2fe169a702b..ad1c42c3c6b 100644 --- a/runtime/near-vm-logic/src/logic.rs +++ b/runtime/near-vm-logic/src/logic.rs @@ -197,6 +197,16 @@ impl<'a> VMLogic<'a> { &self.config } + #[cfg(test)] + pub(crate) fn memory(&mut self) -> &mut crate::vmstate::Memory<'a> { + &mut self.memory + } + + #[cfg(test)] + pub(crate) fn registers(&mut self) -> &mut crate::vmstate::Registers { + &mut self.registers + } + // ################# // # Registers API # // ################# diff --git a/runtime/near-vm-logic/src/mocks/mock_memory.rs b/runtime/near-vm-logic/src/mocks/mock_memory.rs index f00a13736da..81bed25a719 100644 --- a/runtime/near-vm-logic/src/mocks/mock_memory.rs +++ b/runtime/near-vm-logic/src/mocks/mock_memory.rs @@ -2,28 +2,50 @@ use crate::{MemSlice, MemoryLike}; use std::borrow::Cow; -#[derive(Default)] -pub struct MockedMemory {} +pub struct MockedMemory(Box<[u8]>); + +impl MockedMemory { + pub const MEMORY_SIZE: u64 = 64 * 1024; +} + +impl Default for MockedMemory { + fn default() -> Self { + Self(vec![0; Self::MEMORY_SIZE as usize].into()) + } +} + +fn make_range(ptr: u64, len: usize) -> Result, ()> { + let start = usize::try_from(ptr).map_err(|_| ())?; + let end = start.checked_add(len).ok_or(())?; + Ok(start..end) +} impl MemoryLike for MockedMemory { - fn fits_memory(&self, _slice: MemSlice) -> Result<(), ()> { - Ok(()) + fn fits_memory(&self, slice: MemSlice) -> Result<(), ()> { + match self.0.get(slice.range::()?) { + Some(_) => Ok(()), + None => Err(()), + } } fn view_memory(&self, slice: MemSlice) -> Result, ()> { - let view = unsafe { std::slice::from_raw_parts(slice.ptr as *const u8, slice.len()?) }; - Ok(Cow::Borrowed(view)) + self.0.get(slice.range::()?).map(Cow::Borrowed).ok_or(()) } - fn read_memory(&self, offset: u64, buffer: &mut [u8]) -> Result<(), ()> { - let src = unsafe { std::slice::from_raw_parts(offset as *const u8, buffer.len()) }; - buffer.copy_from_slice(src); + fn read_memory(&self, ptr: u64, buffer: &mut [u8]) -> Result<(), ()> { + let slice = self.0.get(make_range(ptr, buffer.len())?).ok_or(())?; + buffer.copy_from_slice(slice); Ok(()) } - fn write_memory(&mut self, offset: u64, buffer: &[u8]) -> Result<(), ()> { - let dest = unsafe { std::slice::from_raw_parts_mut(offset as *mut u8, buffer.len()) }; - dest.copy_from_slice(buffer); + fn write_memory(&mut self, ptr: u64, buffer: &[u8]) -> Result<(), ()> { + let slice = self.0.get_mut(make_range(ptr, buffer.len())?).ok_or(())?; + slice.copy_from_slice(buffer); Ok(()) } } + +#[test] +fn test_memory_like() { + crate::test_utils::test_memory_like(|| Box::new(MockedMemory::default())); +} diff --git a/runtime/near-vm-logic/src/test_utils.rs b/runtime/near-vm-logic/src/test_utils.rs new file mode 100644 index 00000000000..be9e6e44a0b --- /dev/null +++ b/runtime/near-vm-logic/src/test_utils.rs @@ -0,0 +1,71 @@ +use crate::{MemSlice, MemoryLike}; + +/// Tests for implementation of MemoryLike interface. +/// +/// The `factory` returns a [`MemoryLike`] implementation to be tested. The +/// memory must be configured with 64 KiB (i.e. single WASM page) of memory +/// available. +/// +/// Panics if any of the tests fails. +pub fn test_memory_like(factory: impl FnOnce() -> Box) { + const PAGE: u64 = 0x10000; + + struct TestContext { + mem: Box, + buf: [u8; PAGE as usize + 1], + } + + impl TestContext { + fn test_read(&mut self, ptr: u64, len: u64, value: u8) { + self.buf.fill(!value); + self.mem.fits_memory(MemSlice { ptr, len }).unwrap(); + self.mem.read_memory(ptr, &mut self.buf[..(len as usize)]).unwrap(); + assert!(self.buf[..(len as usize)].iter().all(|&v| v == value)); + } + + fn test_write(&mut self, ptr: u64, len: u64, value: u8) { + self.buf.fill(value); + self.mem.fits_memory(MemSlice { ptr, len }).unwrap(); + self.mem.write_memory(ptr, &self.buf[..(len as usize)]).unwrap(); + } + + fn test_oob(&mut self, ptr: u64, len: u64) { + self.buf.fill(42); + self.mem.fits_memory(MemSlice { ptr, len }).unwrap_err(); + self.mem.read_memory(ptr, &mut self.buf[..(len as usize)]).unwrap_err(); + assert!(self.buf[..(len as usize)].iter().all(|&v| v == 42)); + self.mem.write_memory(ptr, &self.buf[..(len as usize)]).unwrap_err(); + } + } + + let mut ctx = TestContext { mem: factory(), buf: [0; PAGE as usize + 1] }; + + // Test memory is initialised to zero. + ctx.test_read(0, PAGE, 0); + ctx.test_read(PAGE, 0, 0); + ctx.test_read(0, PAGE / 2, 0); + ctx.test_read(PAGE / 2, PAGE / 2, 0); + + // Test writing works. + ctx.test_write(0, PAGE / 2, 42); + ctx.test_read(0, PAGE / 2, 42); + ctx.test_read(PAGE / 2, PAGE / 2, 0); + + ctx.test_write(PAGE / 4, PAGE / 4, 24); + ctx.test_read(0, PAGE / 4, 42); + ctx.test_read(PAGE / 4, PAGE / 4, 24); + ctx.test_read(PAGE / 2, PAGE / 2, 0); + + // Zero memory. + ctx.test_write(0, PAGE, 0); + ctx.test_read(0, PAGE, 0); + + // Test out-of-bounds checks. + ctx.test_oob(0, PAGE + 1); + ctx.test_oob(1, PAGE); + ctx.test_oob(PAGE - 1, 2); + ctx.test_oob(PAGE, 1); + + // None of the writes in OOB should have any effect. + ctx.test_read(0, PAGE, 0); +} diff --git a/runtime/near-vm-logic/src/tests/alt_bn128.rs b/runtime/near-vm-logic/src/tests/alt_bn128.rs index cccc3ff6208..04a3e096515 100644 --- a/runtime/near-vm-logic/src/tests/alt_bn128.rs +++ b/runtime/near-vm-logic/src/tests/alt_bn128.rs @@ -90,13 +90,12 @@ fn test_alt_bn128_g1_multiexp() { fn check(input: &[u8], expected: Result<&[u8], &str>) { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); + let input = logic.internal_mem_write(input); - let res = logic.alt_bn128_g1_multiexp(input.len() as _, input.as_ptr() as _, 0); + let res = logic.alt_bn128_g1_multiexp(input.len, input.ptr, 0); if let Some(((), expected)) = check_result(res, expected) { - let len = logic.register_len(0).unwrap(); - let mut res = vec![0u8; len as usize]; - logic.read_register(0, res.as_mut_ptr() as _).unwrap(); - assert_eq!(res, expected) + let got = logic.registers().get_for_free(0).unwrap(); + assert_eq_points(&expected, got); } } #[track_caller] @@ -155,13 +154,12 @@ fn test_alt_bn128_g1_sum() { fn check(input: &[u8], expected: Result<&[u8], &str>) { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); + let input = logic.internal_mem_write(input); - let res = logic.alt_bn128_g1_sum(input.len() as _, input.as_ptr() as _, 0); + let res = logic.alt_bn128_g1_sum(input.len, input.ptr, 0); if let Some(((), expected)) = check_result(res, expected) { - let len = logic.register_len(0).unwrap(); - let mut res = vec![0u8; len as usize]; - logic.read_register(0, res.as_mut_ptr() as _).unwrap(); - assert_eq_points(&res, expected) + let got = logic.registers().get_for_free(0).unwrap(); + assert_eq_points(&expected, got); } } #[track_caller] @@ -220,8 +218,9 @@ fn test_alt_bn128_pairing_check() { fn check(input: &[u8], expected: Result) { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); + let input = logic.internal_mem_write(input); - let res = logic.alt_bn128_pairing_check(input.len() as _, input.as_ptr() as _); + let res = logic.alt_bn128_pairing_check(input.len, input.ptr); if let Some((res, expected)) = check_result(res, expected) { assert_eq!(res, expected) } diff --git a/runtime/near-vm-logic/src/tests/context.rs b/runtime/near-vm-logic/src/tests/context.rs index 0ca8d05f2b6..5ca0c2d7537 100644 --- a/runtime/near-vm-logic/src/tests/context.rs +++ b/runtime/near-vm-logic/src/tests/context.rs @@ -28,10 +28,8 @@ macro_rules! decl_test_bytes { fn $testname() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(create_context()); - let res = vec![0u8; $input.len()]; logic.$method(0).expect("read bytes into register from context should be ok"); - logic.read_register(0, res.as_ptr() as _).expect("read register should be ok"); - assert_eq!(res, $input); + logic.assert_read_register($input, 0); } }; } @@ -54,11 +52,10 @@ macro_rules! decl_test_u128 { fn $testname() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(create_context()); - let buf = [0u8; std::mem::size_of::()]; - logic.$method(buf.as_ptr() as _).expect("read from context should be ok"); - let res = u128::from_le_bytes(buf); - assert_eq!(res, $input); + logic.$method(0).expect("read from context should be ok"); + let got = logic.internal_mem_read(0, 16).try_into().unwrap(); + assert_eq!(u128::from_le_bytes(got), $input); } }; } diff --git a/runtime/near-vm-logic/src/tests/ed25519_verify.rs b/runtime/near-vm-logic/src/tests/ed25519_verify.rs index cb563ab08f1..2494485808b 100644 --- a/runtime/near-vm-logic/src/tests/ed25519_verify.rs +++ b/runtime/near-vm-logic/src/tests/ed25519_verify.rs @@ -60,21 +60,21 @@ fn check_ed25519_verify( logic.wrapped_internal_write_register(1, &signature).unwrap(); 1 } else { - signature.as_ptr() as u64 + logic.internal_mem_write(signature).ptr }; let message_ptr = if message_len == u64::MAX { logic.wrapped_internal_write_register(2, &message).unwrap(); 2 } else { - message.as_ptr() as u64 + logic.internal_mem_write(message).ptr }; let public_key_ptr = if public_key_len == u64::MAX { logic.wrapped_internal_write_register(3, &public_key).unwrap(); 3 } else { - public_key.as_ptr() as u64 + logic.internal_mem_write(public_key).ptr }; let result = logic.ed25519_verify( diff --git a/runtime/near-vm-logic/src/tests/fixtures.rs b/runtime/near-vm-logic/src/tests/fixtures.rs index 15a996e3c42..14b3df5add8 100644 --- a/runtime/near-vm-logic/src/tests/fixtures.rs +++ b/runtime/near-vm-logic/src/tests/fixtures.rs @@ -1,5 +1,6 @@ use crate::{VMContext, VMLimitConfig}; use near_primitives_core::config::ViewConfig; +use near_primitives_core::types::Gas; pub fn get_context(input: Vec, is_view: bool) -> VMContext { VMContext { @@ -24,3 +25,9 @@ pub fn get_context(input: Vec, is_view: bool) -> VMContext { output_data_receivers: vec![], } } + +pub fn get_context_with_prepaid_gas(prepaid_gas: Gas) -> VMContext { + let mut context = get_context(vec![], false); + context.prepaid_gas = prepaid_gas; + context +} diff --git a/runtime/near-vm-logic/src/tests/gas_counter.rs b/runtime/near-vm-logic/src/tests/gas_counter.rs index f564e106e67..3d3a838f06e 100644 --- a/runtime/near-vm-logic/src/tests/gas_counter.rs +++ b/runtime/near-vm-logic/src/tests/gas_counter.rs @@ -1,9 +1,9 @@ use crate::receipt_manager::ReceiptMetadata; -use crate::tests::fixtures::get_context; +use crate::tests::fixtures::get_context_with_prepaid_gas; use crate::tests::helpers::*; -use crate::tests::vm_logic_builder::VMLogicBuilder; +use crate::tests::vm_logic_builder::{TestVMLogic, VMLogicBuilder}; use crate::types::Gas; -use crate::{VMConfig, VMLogic}; +use crate::VMConfig; use expect_test::expect; use near_primitives::config::{ActionCosts, ExtCosts}; use near_primitives::runtime::fees::Fee; @@ -15,7 +15,7 @@ fn test_dont_burn_gas_when_exceeding_attached_gas_limit() { let gas_limit = 10u64.pow(14); let mut logic_builder = VMLogicBuilder::default().max_gas_burnt(gas_limit * 2); - let mut logic = logic_builder.build_with_prepaid_gas(gas_limit); + let mut logic = logic_builder.build(get_context_with_prepaid_gas(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) @@ -33,7 +33,7 @@ fn test_limit_wasm_gas_after_attaching_gas() { let op_limit = op_limit(gas_limit); let mut logic_builder = VMLogicBuilder::default().max_gas_burnt(gas_limit * 2); - let mut logic = logic_builder.build_with_prepaid_gas(gas_limit); + let mut logic = logic_builder.build(get_context_with_prepaid_gas(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) @@ -52,7 +52,7 @@ fn test_cant_burn_more_than_max_gas_burnt_gas() { let op_limit = op_limit(gas_limit); let mut logic_builder = VMLogicBuilder::default().max_gas_burnt(gas_limit); - let mut logic = logic_builder.build_with_prepaid_gas(gas_limit * 2); + let mut logic = logic_builder.build(get_context_with_prepaid_gas(gas_limit * 2)); logic.gas(op_limit * 3).expect_err("should fail with gas limit"); let outcome = logic.compute_outcome_and_distribute_gas(); @@ -67,7 +67,7 @@ fn test_cant_burn_more_than_prepaid_gas() { let op_limit = op_limit(gas_limit); let mut logic_builder = VMLogicBuilder::default().max_gas_burnt(gas_limit * 2); - let mut logic = logic_builder.build_with_prepaid_gas(gas_limit); + let mut logic = logic_builder.build(get_context_with_prepaid_gas(gas_limit)); logic.gas(op_limit * 3).expect_err("should fail with gas limit"); let outcome = logic.compute_outcome_and_distribute_gas(); @@ -82,7 +82,7 @@ fn test_hit_max_gas_burnt_limit() { let op_limit = op_limit(gas_limit); let mut logic_builder = VMLogicBuilder::default().max_gas_burnt(gas_limit); - let mut logic = logic_builder.build_with_prepaid_gas(gas_limit * 3); + let mut logic = logic_builder.build(get_context_with_prepaid_gas(gas_limit * 3)); promise_create(&mut logic, b"rick.test", 0, gas_limit / 2).expect("should create a promise"); logic.gas(op_limit * 2).expect_err("should fail with gas limit"); @@ -98,7 +98,7 @@ fn test_hit_prepaid_gas_limit() { let op_limit = op_limit(gas_limit); let mut logic_builder = VMLogicBuilder::default().max_gas_burnt(gas_limit * 3); - let mut logic = logic_builder.build_with_prepaid_gas(gas_limit); + let mut logic = logic_builder.build(get_context_with_prepaid_gas(gas_limit)); promise_create(&mut logic, b"rick.test", 0, gas_limit / 2).expect("should create a promise"); logic.gas(op_limit * 2).expect_err("should fail with gas limit"); @@ -125,7 +125,7 @@ fn function_call_weight_check(function_calls: &[(Gas, u64, Gas)]) { let gas_limit = 10_000_000_000; let mut logic_builder = VMLogicBuilder::free().max_gas_burnt(gas_limit); - let mut logic = logic_builder.build_with_prepaid_gas(gas_limit); + let mut logic = logic_builder.build(get_context_with_prepaid_gas(gas_limit)); let mut ratios = vec![]; @@ -211,7 +211,7 @@ fn function_call_no_weight_refund() { let gas_limit = 10u64.pow(14); let mut logic_builder = VMLogicBuilder::default().max_gas_burnt(gas_limit); - let mut logic = logic_builder.build_with_prepaid_gas(gas_limit); + let mut logic = logic_builder.build(get_context_with_prepaid_gas(gas_limit)); let index = promise_batch_create(&mut logic, "rick.test").expect("should create a promise"); promise_batch_action_function_call_weight(&mut logic, index, 0, 1000, 0) @@ -227,11 +227,16 @@ fn function_call_no_weight_refund() { fn test_overflowing_burn_gas_with_promises_gas() { let gas_limit = 3 * 10u64.pow(14); let mut logic_builder = VMLogicBuilder::default().max_gas_burnt(gas_limit); - let mut logic = logic_builder.build_with_prepaid_gas(gas_limit); + let mut logic = logic_builder.build(get_context_with_prepaid_gas(gas_limit)); + + let account_id = logic.internal_mem_write(b"rick.test"); + let args = logic.internal_mem_write(b""); + let num_100u128 = logic.internal_mem_write(&100u128.to_le_bytes()); + let num_10u128 = logic.internal_mem_write(&10u128.to_le_bytes()); let index = promise_batch_create(&mut logic, "rick.test").expect("should create a promise"); - logic.promise_batch_action_transfer(index, 100u128.to_le_bytes().as_ptr() as _).unwrap(); - let call_id = logic.promise_batch_then(index, 9, "rick.test".as_ptr() as _).unwrap(); + logic.promise_batch_action_transfer(index, num_100u128.ptr).unwrap(); + let call_id = logic.promise_batch_then(index, account_id.len, account_id.ptr).unwrap(); let needed_gas_charge = u64::max_value() - logic.gas_counter().used_gas() - 1; let function_name_len = @@ -239,10 +244,10 @@ fn test_overflowing_burn_gas_with_promises_gas() { let result = logic.promise_batch_action_function_call( call_id, function_name_len, - "x".as_ptr() as _, - 1, - "x".as_ptr() as _, - 10u128.to_le_bytes().as_ptr() as _, + /* function_name_ptr: */ 0, + args.len, + args.ptr, + num_10u128.ptr, 10000, ); assert!(matches!( @@ -256,24 +261,29 @@ fn test_overflowing_burn_gas_with_promises_gas() { fn test_overflowing_burn_gas_with_promises_gas_2() { let gas_limit = 3 * 10u64.pow(14); let mut logic_builder = VMLogicBuilder::default().max_gas_burnt(gas_limit); - let mut logic = logic_builder.build_with_prepaid_gas(gas_limit / 2); + let mut logic = logic_builder.build(get_context_with_prepaid_gas(gas_limit / 2)); + + let account_id = logic.internal_mem_write(b"rick.test"); + let args = logic.internal_mem_write(b""); + let num_100u128 = logic.internal_mem_write(&100u128.to_le_bytes()); + let index = promise_batch_create(&mut logic, "rick.test").expect("should create a promise"); - logic.promise_batch_action_transfer(index, 100u128.to_le_bytes().as_ptr() as _).unwrap(); - logic.promise_batch_then(index, 9, "rick.test".as_ptr() as _).unwrap(); + logic.promise_batch_action_transfer(index, num_100u128.ptr).unwrap(); + logic.promise_batch_then(index, account_id.len, account_id.ptr).unwrap(); let minimum_prepay = logic.gas_counter().used_gas(); - let mut logic = logic_builder.build_with_prepaid_gas(minimum_prepay); + let mut logic = logic_builder.build(get_context_with_prepaid_gas(minimum_prepay)); let index = promise_batch_create(&mut logic, "rick.test").expect("should create a promise"); - logic.promise_batch_action_transfer(index, 100u128.to_le_bytes().as_ptr() as _).unwrap(); - let call_id = logic.promise_batch_then(index, 9, "rick.test".as_ptr() as _).unwrap(); + logic.promise_batch_action_transfer(index, num_100u128.ptr).unwrap(); + let call_id = logic.promise_batch_then(index, account_id.len, account_id.ptr).unwrap(); let needed_gas_charge = u64::max_value() - logic.gas_counter().used_gas() - 1; let function_name_len = needed_gas_charge / logic.config().ext_costs.cost(ExtCosts::read_memory_byte); let result = logic.promise_batch_action_function_call( call_id, function_name_len, - "x".as_ptr() as _, - 1, - "x".as_ptr() as _, + /* function_name_ptr: */ 0, + args.len, + args.ptr, 10u128.to_le_bytes().as_ptr() as _, 10000, ); @@ -293,7 +303,7 @@ fn test_overflowing_burn_gas_with_promises_gas_2() { fn check_action_gas_exceeds_limit( cost: ActionCosts, num_action_paid: u64, - exercise_action: impl FnOnce(&mut VMLogic) -> Result<(), VMLogicError>, + exercise_action: impl FnOnce(&mut TestVMLogic) -> Result<(), VMLogicError>, ) { // Create a logic parametrized such that it will fail with out-of-gas when specified action is deducted. let gas_limit = 10u64.pow(13); @@ -304,7 +314,7 @@ fn check_action_gas_exceeds_limit( execution: 1, // exec part is `used`, make it small }; let mut logic_builder = VMLogicBuilder::default().max_gas_burnt(gas_limit).gas_fee(cost, fee); - let mut logic = logic_builder.build_with_prepaid_gas(gas_attached); + let mut logic = logic_builder.build(get_context_with_prepaid_gas(gas_attached)); let result = exercise_action(&mut logic); assert!(result.is_err(), "expected out-of-gas error for {cost:?} but was ok"); @@ -336,7 +346,7 @@ fn check_action_gas_exceeds_attached( cost: ActionCosts, num_action_paid: u64, expected: expect_test::Expect, - exercise_action: impl FnOnce(&mut VMLogic) -> Result<(), VMLogicError>, + exercise_action: impl FnOnce(&mut TestVMLogic) -> Result<(), VMLogicError>, ) { // Create a logic parametrized such that it will fail with out-of-gas when specified action is deducted. let gas_limit = 10u64.pow(14); @@ -347,7 +357,7 @@ fn check_action_gas_exceeds_attached( execution: gas_attached / num_action_paid + 1, }; let mut logic_builder = VMLogicBuilder::default().max_gas_burnt(gas_limit).gas_fee(cost, fee); - let mut logic = logic_builder.build_with_prepaid_gas(gas_attached); + let mut logic = logic_builder.build(get_context_with_prepaid_gas(gas_attached)); let result = exercise_action(&mut logic); assert!(result.is_err(), "expected out-of-gas error for {cost:?} but was ok"); @@ -399,36 +409,19 @@ fn out_of_gas_new_data_receipt() { ); } -fn create_action_receipt(logic: &mut VMLogic) -> Result<(), VMLogicError> { +fn create_action_receipt(logic: &mut TestVMLogic) -> Result<(), VMLogicError> { promise_batch_create(logic, "rick.test")?; Ok(()) } -fn create_promise_dependency(logic: &mut VMLogic) -> Result<(), VMLogicError> { +fn create_promise_dependency(logic: &mut TestVMLogic) -> Result<(), VMLogicError> { let account_id = "rick.test"; let idx = promise_batch_create(logic, account_id)?; - logic.promise_batch_then(idx, account_id.len() as _, account_id.as_ptr() as _)?; + let account_id = logic.internal_mem_write(account_id.as_bytes()); + logic.promise_batch_then(idx, account_id.len, account_id.ptr)?; Ok(()) } -impl VMLogicBuilder { - fn max_gas_burnt(mut self, max_gas_burnt: Gas) -> Self { - self.config.limit_config.max_gas_burnt = max_gas_burnt; - self - } - - fn gas_fee(mut self, cost: ActionCosts, fee: Fee) -> Self { - self.fees_config.action_fees[cost] = fee; - self - } - - fn build_with_prepaid_gas(&mut self, prepaid_gas: Gas) -> VMLogic<'_> { - let mut context = get_context(vec![], false); - context.prepaid_gas = prepaid_gas; - self.build(context) - } -} - /// 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 { diff --git a/runtime/near-vm-logic/src/tests/helpers.rs b/runtime/near-vm-logic/src/tests/helpers.rs index aa8a421e15f..33ed769dd51 100644 --- a/runtime/near-vm-logic/src/tests/helpers.rs +++ b/runtime/near-vm-logic/src/tests/helpers.rs @@ -1,82 +1,89 @@ -use crate::{with_ext_cost_counter, VMLogic}; +use crate::tests::TestVMLogic; + use near_primitives_core::{config::ExtCosts, types::Gas}; use near_vm_errors::VMLogicError; use std::collections::HashMap; type Result = ::std::result::Result; -pub fn promise_create( - logic: &mut crate::VMLogic<'_>, +pub(super) fn promise_create( + logic: &mut TestVMLogic<'_>, account_id: &[u8], amount: u128, gas: Gas, ) -> Result { - let method = b"promise_create"; - let args = b"args"; + let account_id = logic.internal_mem_write(account_id); + let method = logic.internal_mem_write(b"promise_create"); + let args = logic.internal_mem_write(b"args"); + let amount = logic.internal_mem_write(&amount.to_le_bytes()); + logic.promise_create( - account_id.len() as _, - account_id.as_ptr() as _, - method.len() as _, - method.as_ptr() as _, - args.len() as _, - args.as_ptr() as _, - amount.to_le_bytes().as_ptr() as _, + account_id.len, + account_id.ptr, + method.len, + method.ptr, + args.len, + args.ptr, + amount.ptr, gas, ) } #[allow(dead_code)] -pub fn promise_batch_create(logic: &mut VMLogic, account_id: &str) -> Result { - logic.promise_batch_create(account_id.len() as _, account_id.as_ptr() as _) +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 fn promise_batch_action_function_call( - logic: &mut VMLogic<'_>, +pub(super) fn promise_batch_action_function_call( + logic: &mut TestVMLogic<'_>, promise_index: u64, amount: u128, gas: Gas, ) -> Result<()> { - let method_id = b"promise_batch_action"; - let args = b"promise_batch_action_args"; + let method_id = logic.internal_mem_write(b"promise_batch_action"); + let args = logic.internal_mem_write(b"promise_batch_action_args"); + let amount = logic.internal_mem_write(&amount.to_le_bytes()); logic.promise_batch_action_function_call( promise_index, - method_id.len() as _, - method_id.as_ptr() as _, - args.len() as _, - args.as_ptr() as _, - amount.to_le_bytes().as_ptr() as _, + method_id.len, + method_id.ptr, + args.len, + args.ptr, + amount.ptr, gas, ) } #[allow(dead_code)] -pub fn promise_batch_action_function_call_weight( - logic: &mut VMLogic<'_>, +pub(super) fn promise_batch_action_function_call_weight( + logic: &mut TestVMLogic<'_>, promise_index: u64, amount: u128, gas: Gas, weight: u64, ) -> Result<()> { - let method_id = b"promise_batch_action"; - let args = b"promise_batch_action_args"; + let method_id = logic.internal_mem_write(b"promise_batch_action"); + let args = logic.internal_mem_write(b"promise_batch_action_args"); + let amount = logic.internal_mem_write(&amount.to_le_bytes()); logic.promise_batch_action_function_call_weight( promise_index, - method_id.len() as _, - method_id.as_ptr() as _, - args.len() as _, - args.as_ptr() as _, - amount.to_le_bytes().as_ptr() as _, + method_id.len, + method_id.ptr, + args.len, + args.ptr, + amount.ptr, gas, weight, ) } #[allow(dead_code)] -pub fn promise_batch_action_add_key_with_function_call( - logic: &mut VMLogic<'_>, +pub(super) fn promise_batch_action_add_key_with_function_call( + logic: &mut TestVMLogic<'_>, promise_index: u64, public_key: &[u8], nonce: u64, @@ -84,16 +91,21 @@ pub fn promise_batch_action_add_key_with_function_call( receiver_id: &[u8], method_names: &[u8], ) -> Result<()> { + let public_key = logic.internal_mem_write(public_key); + let receiver_id = logic.internal_mem_write(receiver_id); + let allowance = logic.internal_mem_write(&allowance.to_le_bytes()); + let method_names = logic.internal_mem_write(method_names); + logic.promise_batch_action_add_key_with_function_call( promise_index, - public_key.len() as _, - public_key.as_ptr() as _, + public_key.len, + public_key.ptr, nonce, - allowance.to_le_bytes().as_ptr() as _, - receiver_id.len() as _, - receiver_id.as_ptr() as _, - method_names.len() as _, - method_names.as_ptr() as _, + allowance.ptr, + receiver_id.len, + receiver_id.ptr, + method_names.len, + method_names.ptr, ) } @@ -110,12 +122,12 @@ macro_rules! map( }; ); -pub fn reset_costs_counter() { - with_ext_cost_counter(|cc| cc.clear()) +pub(super) fn reset_costs_counter() { + crate::with_ext_cost_counter(|cc| cc.clear()) } #[track_caller] -pub fn assert_costs(expected: HashMap) { - with_ext_cost_counter(|cc| assert_eq!(*cc, expected)); +pub(super) fn assert_costs(expected: HashMap) { + crate::with_ext_cost_counter(|cc| assert_eq!(*cc, expected)); reset_costs_counter(); } diff --git a/runtime/near-vm-logic/src/tests/miscs.rs b/runtime/near-vm-logic/src/tests/miscs.rs index de128c0882a..5bcf12e3944 100644 --- a/runtime/near-vm-logic/src/tests/miscs.rs +++ b/runtime/near-vm-logic/src/tests/miscs.rs @@ -12,19 +12,19 @@ use std::{fmt::Display, fs}; fn test_valid_utf8() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); - let string_bytes = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%".as_bytes().to_vec(); - let len = string_bytes.len() as u64; - logic.log_utf8(len, string_bytes.as_ptr() as _).expect("Valid utf-8 string_bytes"); + 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(); - assert_eq!(outcome.logs[0], String::from_utf8(string_bytes).unwrap()); + assert_eq!(outcome.logs[0], string); assert_costs(map! { ExtCosts::base: 1, ExtCosts::log_base: 1, - ExtCosts::log_byte: len, + ExtCosts::log_byte: bytes.len, ExtCosts::read_memory_base: 1, - ExtCosts::read_memory_byte: len, + ExtCosts::read_memory_byte: bytes.len, ExtCosts::utf8_decoding_base: 1, - ExtCosts::utf8_decoding_byte: len, + ExtCosts::utf8_decoding_byte: bytes.len, }); } @@ -32,17 +32,16 @@ fn test_valid_utf8() { fn test_invalid_utf8() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); - let string_bytes = [128].to_vec(); - let len = string_bytes.len() as u64; - assert_eq!(logic.log_utf8(len, string_bytes.as_ptr() as _), Err(HostError::BadUTF8.into())); + 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(); assert_eq!(outcome.logs.len(), 0); assert_costs(map! { ExtCosts::base: 1, ExtCosts::read_memory_base: 1, - ExtCosts::read_memory_byte: len, + ExtCosts::read_memory_byte: bytes.len, ExtCosts::utf8_decoding_base: 1, - ExtCosts::utf8_decoding_byte: len, + ExtCosts::utf8_decoding_byte: bytes.len, }); } @@ -50,40 +49,37 @@ fn test_invalid_utf8() { fn test_valid_null_terminated_utf8() { let mut logic_builder = VMLogicBuilder::default(); - let mut string_bytes = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%".as_bytes().to_vec(); - string_bytes.push(0u8); - let bytes_len = string_bytes.len(); - logic_builder.config.limit_config.max_total_log_length = string_bytes.len() as u64; + let cstring = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%\x00"; + let string = &cstring[..cstring.len() - 1]; + logic_builder.config.limit_config.max_total_log_length = string.len() as u64; let mut logic = logic_builder.build(get_context(vec![], false)); - logic - .log_utf8(u64::MAX, string_bytes.as_ptr() as _) - .expect("Valid null-terminated utf-8 string_bytes"); - string_bytes.pop(); + 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 len = bytes_len as u64; assert_costs(map! { ExtCosts::base: 1, ExtCosts::log_base: 1, - ExtCosts::log_byte: len - 1, - ExtCosts::read_memory_base: len, - ExtCosts::read_memory_byte: len, + ExtCosts::log_byte: string.len() as u64, + ExtCosts::read_memory_base: bytes.len, + ExtCosts::read_memory_byte: bytes.len, ExtCosts::utf8_decoding_base: 1, - ExtCosts::utf8_decoding_byte: len - 1, + ExtCosts::utf8_decoding_byte: string.len() as u64, }); - assert_eq!(outcome.logs[0], String::from_utf8(string_bytes.clone()).unwrap()); + assert_eq!(outcome.logs[0], string); } #[test] fn test_log_max_limit() { let mut logic_builder = VMLogicBuilder::default(); - let string_bytes = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%".as_bytes().to_vec(); - let limit = (string_bytes.len() - 1) as u64; + let string = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%"; + let limit = string.len() as u64 - 1; logic_builder.config.limit_config.max_total_log_length = limit; let mut logic = logic_builder.build(get_context(vec![], false)); + let bytes = logic.internal_mem_write(string.as_bytes()); assert_eq!( - logic.log_utf8(string_bytes.len() as _, string_bytes.as_ptr() as _), - Err(HostError::TotalLogLengthExceeded { length: string_bytes.len() as _, limit }.into()) + logic.log_utf8(bytes.len, bytes.ptr), + Err(HostError::TotalLogLengthExceeded { length: bytes.len, limit }.into()) ); assert_costs(map! { @@ -98,20 +94,19 @@ fn test_log_max_limit() { #[test] fn test_log_total_length_limit() { let mut logic_builder = VMLogicBuilder::default(); - let string_bytes = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%".as_bytes().to_vec(); + let string = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%".as_bytes(); let num_logs = 10; - let limit = string_bytes.len() as u64 * num_logs - 1; + let limit = string.len() as u64 * num_logs - 1; logic_builder.config.limit_config.max_total_log_length = limit; logic_builder.config.limit_config.max_number_logs = num_logs; let mut logic = logic_builder.build(get_context(vec![], false)); + let bytes = logic.internal_mem_write(string); for _ in 0..num_logs - 1 { - logic - .log_utf8(string_bytes.len() as _, string_bytes.as_ptr() as _) - .expect("total is still under the limit"); + logic.log_utf8(bytes.len, bytes.ptr).expect("total is still under the limit"); } assert_eq!( - logic.log_utf8(string_bytes.len() as _, string_bytes.as_ptr() as _), + logic.log_utf8(bytes.len, bytes.ptr), Err(HostError::TotalLogLengthExceeded { length: limit + 1, limit }.into()) ); @@ -122,60 +117,64 @@ fn test_log_total_length_limit() { #[test] fn test_log_number_limit() { let mut logic_builder = VMLogicBuilder::default(); - let string_bytes = "blabla".as_bytes().to_vec(); + let string = "blabla"; let max_number_logs = 3; logic_builder.config.limit_config.max_total_log_length = - (string_bytes.len() + 1) as u64 * (max_number_logs + 1); + (string.len() + 1) as u64 * (max_number_logs + 1); logic_builder.config.limit_config.max_number_logs = max_number_logs; let mut logic = logic_builder.build(get_context(vec![], false)); - let len = string_bytes.len() as u64; + let bytes = logic.internal_mem_write(string.as_bytes()); for _ in 0..max_number_logs { logic - .log_utf8(len, string_bytes.as_ptr() as _) + .log_utf8(bytes.len, bytes.ptr) .expect("Valid utf-8 string_bytes under the log number limit"); } assert_eq!( - logic.log_utf8(len, string_bytes.as_ptr() as _), + logic.log_utf8(bytes.len, bytes.ptr), Err(HostError::NumberOfLogsExceeded { limit: max_number_logs }.into()) ); assert_costs(map! { ExtCosts::base: max_number_logs + 1, ExtCosts::log_base: max_number_logs, - ExtCosts::log_byte: len * max_number_logs, + ExtCosts::log_byte: bytes.len * max_number_logs, ExtCosts::read_memory_base: max_number_logs, - ExtCosts::read_memory_byte: len * max_number_logs, + ExtCosts::read_memory_byte: bytes.len * max_number_logs, ExtCosts::utf8_decoding_base: max_number_logs, - ExtCosts::utf8_decoding_byte: len * max_number_logs, + ExtCosts::utf8_decoding_byte: bytes.len * max_number_logs, }); let outcome = logic.compute_outcome_and_distribute_gas(); assert_eq!(outcome.logs.len() as u64, max_number_logs); } +fn append_utf16(dst: &mut Vec, string: &str) { + for code_unit in string.encode_utf16() { + dst.extend_from_slice(&code_unit.to_le_bytes()); + } +} + #[test] fn test_log_utf16_number_limit() { - let mut logic_builder = VMLogicBuilder::default(); let string = "$ qò$`"; - let mut string_bytes: Vec = vec![0u8; 0]; - for u16_ in string.encode_utf16() { - string_bytes.push(u16_ as u8); - string_bytes.push((u16_ >> 8) as u8); - } + let mut bytes = Vec::new(); + append_utf16(&mut bytes, string); + + let mut logic_builder = VMLogicBuilder::default(); let max_number_logs = 3; logic_builder.config.limit_config.max_total_log_length = - (string_bytes.len() + 1) as u64 * (max_number_logs + 1); + (bytes.len() + 1) as u64 * (max_number_logs + 1); logic_builder.config.limit_config.max_number_logs = max_number_logs; let mut logic = logic_builder.build(get_context(vec![], false)); - let len = string_bytes.len() as u64; + let bytes = logic.internal_mem_write(&bytes); for _ in 0..max_number_logs { logic - .log_utf16(len, string_bytes.as_ptr() as _) + .log_utf16(bytes.len, bytes.ptr) .expect("Valid utf-16 string_bytes under the log number limit"); } assert_eq!( - logic.log_utf16(len, string_bytes.as_ptr() as _), + logic.log_utf16(bytes.len, bytes.ptr), Err(HostError::NumberOfLogsExceeded { limit: max_number_logs }.into()) ); @@ -184,9 +183,9 @@ fn test_log_utf16_number_limit() { ExtCosts::log_base: max_number_logs, ExtCosts::log_byte: string.len() as u64 * max_number_logs, ExtCosts::read_memory_base: max_number_logs, - ExtCosts::read_memory_byte: len * max_number_logs, + ExtCosts::read_memory_byte: bytes.len * max_number_logs, ExtCosts::utf16_decoding_base: max_number_logs, - ExtCosts::utf16_decoding_byte: len * max_number_logs, + ExtCosts::utf16_decoding_byte: bytes.len * max_number_logs, }); let outcome = logic.compute_outcome_and_distribute_gas(); @@ -196,37 +195,27 @@ fn test_log_utf16_number_limit() { #[test] fn test_log_total_length_limit_mixed() { let mut logic_builder = VMLogicBuilder::default(); - let utf8_bytes = "abc".as_bytes().to_vec(); let string = "abc"; let mut utf16_bytes: Vec = vec![0u8; 0]; - for u16_ in string.encode_utf16() { - utf16_bytes.push(u16_ as u8); - utf16_bytes.push((u16_ >> 8) as u8); - } - - let final_bytes = "abc".as_bytes().to_vec(); + append_utf16(&mut utf16_bytes, string); let num_logs_each = 10; - let limit = utf8_bytes.len() as u64 * num_logs_each - + string.as_bytes().len() as u64 * num_logs_each - + final_bytes.len() as u64 - - 1; + let limit = string.len() as u64 * (num_logs_each * 2 + 1) - 1; logic_builder.config.limit_config.max_total_log_length = limit; logic_builder.config.limit_config.max_number_logs = num_logs_each * 2 + 1; let mut logic = logic_builder.build(get_context(vec![], false)); + let utf8_bytes = logic.internal_mem_write(string.as_bytes()); + let utf16_bytes = logic.internal_mem_write(&utf16_bytes); + for _ in 0..num_logs_each { - logic - .log_utf16(utf16_bytes.len() as _, utf16_bytes.as_ptr() as _) - .expect("total is still under the limit"); + logic.log_utf16(utf16_bytes.len, utf16_bytes.ptr).expect("total is still under the limit"); - logic - .log_utf8(utf8_bytes.len() as _, utf8_bytes.as_ptr() as _) - .expect("total is still under the limit"); + logic.log_utf8(utf8_bytes.len, utf8_bytes.ptr).expect("total is still under the limit"); } assert_eq!( - logic.log_utf8(final_bytes.len() as _, final_bytes.as_ptr() as _), + logic.log_utf8(utf8_bytes.len, utf8_bytes.ptr), Err(HostError::TotalLogLengthExceeded { length: limit + 1, limit }.into()) ); @@ -237,22 +226,21 @@ fn test_log_total_length_limit_mixed() { #[test] fn test_log_utf8_max_limit_null_terminated() { let mut logic_builder = VMLogicBuilder::default(); - let mut string_bytes = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%".as_bytes().to_vec(); - let limit = (string_bytes.len() - 1) as u64; + let bytes = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%\x00".as_bytes(); + let limit = (bytes.len() - 2) as u64; logic_builder.config.limit_config.max_total_log_length = limit; let mut logic = logic_builder.build(get_context(vec![], false)); + let bytes = logic.internal_mem_write(bytes); - string_bytes.push(0u8); assert_eq!( - logic.log_utf8(u64::MAX, string_bytes.as_ptr() as _), + logic.log_utf8(u64::MAX, bytes.ptr), Err(HostError::TotalLogLengthExceeded { length: limit + 1, limit }.into()) ); - let len = string_bytes.len() as u64; assert_costs(map! { ExtCosts::base: 1, - ExtCosts::read_memory_base: len - 1 , - ExtCosts::read_memory_byte: len - 1, + ExtCosts::read_memory_base: bytes.len - 1, + ExtCosts::read_memory_byte: bytes.len - 1, ExtCosts::utf8_decoding_base: 1, }); @@ -264,23 +252,20 @@ fn test_log_utf8_max_limit_null_terminated() { fn test_valid_log_utf16() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); + let string = "$ qò$`"; - let mut utf16_bytes: Vec = vec![0u8; 0]; - for u16_ in string.encode_utf16() { - utf16_bytes.push(u16_ as u8); - utf16_bytes.push((u16_ >> 8) as u8); - } - logic - .log_utf16(utf16_bytes.len() as _, utf16_bytes.as_ptr() as _) - .expect("Valid utf-16 string_bytes"); + let mut bytes = Vec::new(); + append_utf16(&mut bytes, string); + let bytes = logic.internal_mem_write(&bytes); + + logic.log_utf16(bytes.len, bytes.ptr).expect("Valid utf-16 string_bytes"); - let len = utf16_bytes.len() as u64; assert_costs(map! { ExtCosts::base: 1, ExtCosts::read_memory_base: 1, - ExtCosts::read_memory_byte: len, + ExtCosts::read_memory_byte: bytes.len, ExtCosts::utf16_decoding_base: 1, - ExtCosts::utf16_decoding_byte: len, + ExtCosts::utf16_decoding_byte: bytes.len, ExtCosts::log_base: 1, ExtCosts::log_byte: string.len() as u64, }); @@ -293,40 +278,32 @@ fn test_valid_log_utf16_max_log_len_not_even() { let mut logic_builder = VMLogicBuilder::default(); logic_builder.config.limit_config.max_total_log_length = 5; let mut logic = logic_builder.build(get_context(vec![], false)); + let string = "ab"; - let mut utf16_bytes: Vec = Vec::new(); - for u16_ in string.encode_utf16() { - utf16_bytes.push(u16_ as u8); - utf16_bytes.push((u16_ >> 8) as u8); - } - utf16_bytes.extend_from_slice(&[0, 0]); - logic.log_utf16(u64::MAX, utf16_bytes.as_ptr() as _).expect("Valid utf-16 string_bytes"); + let mut bytes = Vec::new(); + append_utf16(&mut bytes, string); + append_utf16(&mut bytes, "\0"); + let bytes = logic.internal_mem_write(&bytes); + logic.log_utf16(u64::MAX, bytes.ptr).expect("Valid utf-16 bytes"); - let len = utf16_bytes.len() as u64; assert_costs(map! { ExtCosts::base: 1, - ExtCosts::read_memory_base: len / 2, - ExtCosts::read_memory_byte: len, + ExtCosts::read_memory_base: bytes.len / 2, + ExtCosts::read_memory_byte: bytes.len, ExtCosts::utf16_decoding_base: 1, - ExtCosts::utf16_decoding_byte: len - 2, + ExtCosts::utf16_decoding_byte: bytes.len - 2, ExtCosts::log_base: 1, - ExtCosts::log_byte: string.len() as u64 , + ExtCosts::log_byte: string.len() as u64, }); let string = "abc"; - let mut utf16_bytes: Vec = Vec::new(); - for u16_ in string.encode_utf16() { - utf16_bytes.push(u16_ as u8); - utf16_bytes.push((u16_ >> 8) as u8); - } - utf16_bytes.extend_from_slice(&[0, 0]); + let mut bytes = Vec::new(); + append_utf16(&mut bytes, string); + append_utf16(&mut bytes, "\0"); + let bytes = logic.internal_mem_write(&bytes); assert_eq!( - logic.log_utf16(u64::MAX, utf16_bytes.as_ptr() as _), - Err(HostError::TotalLogLengthExceeded { - length: 6, - limit: logic_builder.config.limit_config.max_total_log_length, - } - .into()) + logic.log_utf16(u64::MAX, bytes.ptr), + Err(HostError::TotalLogLengthExceeded { length: 6, limit: 5 }.into()) ); assert_costs(map! { @@ -340,11 +317,10 @@ fn test_valid_log_utf16_max_log_len_not_even() { #[test] fn test_log_utf8_max_limit_null_terminated_fail() { let mut logic_builder = VMLogicBuilder::default(); - let mut string_bytes = "abcd".as_bytes().to_vec(); - string_bytes.push(0u8); logic_builder.config.limit_config.max_total_log_length = 3; let mut logic = logic_builder.build(get_context(vec![], false)); - let res = logic.log_utf8(u64::MAX, string_bytes.as_ptr() as _); + let bytes = logic.internal_mem_write(b"abcdefgh\0"); + let res = logic.log_utf8(u64::MAX, bytes.ptr); assert_eq!(res, Err(HostError::TotalLogLengthExceeded { length: 4, limit: 3 }.into())); assert_costs(map! { ExtCosts::base: 1, @@ -358,27 +334,25 @@ fn test_log_utf8_max_limit_null_terminated_fail() { fn test_valid_log_utf16_null_terminated() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); + let string = "$ qò$`"; - let mut utf16_bytes: Vec = vec![0u8; 0]; - for u16_ in string.encode_utf16() { - utf16_bytes.push(u16_ as u8); - utf16_bytes.push((u16_ >> 8) as u8); - } - utf16_bytes.push(0); - utf16_bytes.push(0); - logic.log_utf16(u64::MAX, utf16_bytes.as_ptr() as _).expect("Valid utf-16 string_bytes"); + let mut bytes = Vec::new(); + append_utf16(&mut bytes, string); + bytes.extend_from_slice(&[0, 0]); + let bytes = logic.internal_mem_write(&bytes); + + logic.log_utf16(u64::MAX, bytes.ptr).expect("Valid utf-16 string_bytes"); - let len = utf16_bytes.len() as u64; let outcome = logic.compute_outcome_and_distribute_gas(); assert_eq!(outcome.logs[0], string); assert_costs(map! { ExtCosts::base: 1, - ExtCosts::read_memory_base: len / 2 , - ExtCosts::read_memory_byte: len, + ExtCosts::read_memory_base: bytes.len / 2 , + ExtCosts::read_memory_byte: bytes.len, ExtCosts::utf16_decoding_base: 1, - ExtCosts::utf16_decoding_byte: len - 2, + ExtCosts::utf16_decoding_byte: bytes.len - 2, ExtCosts::log_base: 1, - ExtCosts::log_byte: string.len() as u64 , + ExtCosts::log_byte: string.len() as u64, }); } @@ -386,21 +360,19 @@ fn test_valid_log_utf16_null_terminated() { fn test_invalid_log_utf16() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); - let utf16: Vec = vec![0xD834, 0xDD1E, 0x006d, 0x0075, 0xD800, 0x0069, 0x0063]; - let mut utf16_bytes: Vec = vec![]; - for u16_ in utf16 { - utf16_bytes.push(u16_ as u8); - utf16_bytes.push((u16_ >> 8) as u8); + let mut bytes: Vec = Vec::new(); + for u16_ in [0xD834u16, 0xDD1E, 0x006d, 0x0075, 0xD800, 0x0069, 0x0063] { + bytes.extend_from_slice(&u16_.to_le_bytes()); } - let res = logic.log_utf16(utf16_bytes.len() as _, utf16_bytes.as_ptr() as _); - let len = utf16_bytes.len() as u64; + let bytes = logic.internal_mem_write(&bytes); + let res = logic.log_utf16(bytes.len, bytes.ptr); assert_eq!(res, Err(HostError::BadUTF16.into())); assert_costs(map! { ExtCosts::base: 1, ExtCosts::read_memory_base: 1, - ExtCosts::read_memory_byte: len, + ExtCosts::read_memory_byte: bytes.len, ExtCosts::utf16_decoding_base: 1, - ExtCosts::utf16_decoding_byte: len, + ExtCosts::utf16_decoding_byte: bytes.len, }); } @@ -408,25 +380,21 @@ fn test_invalid_log_utf16() { fn test_valid_log_utf16_null_terminated_fail() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); - let string = "$ qò$`"; - let mut utf16_bytes: Vec = vec![]; - for u16_ in string.encode_utf16() { - utf16_bytes.push(u16_ as u8); - utf16_bytes.push((u16_ >> 8) as u8); - } - utf16_bytes.push(0); - utf16_bytes.push(0xD8u8); // Bad utf-16 - utf16_bytes.push(0); - utf16_bytes.push(0); - let res = logic.log_utf16(u64::MAX, utf16_bytes.as_ptr() as _); - let len = utf16_bytes.len() as u64; + + let mut bytes = Vec::new(); + append_utf16(&mut bytes, "$ qò$`"); + bytes.extend_from_slice(&[0x00, 0xD8]); // U+D800, unpaired surrogate + append_utf16(&mut bytes, "foobarbaz\0"); + let bytes = logic.internal_mem_write(&bytes); + + let res = logic.log_utf16(u64::MAX, bytes.ptr); assert_eq!(res, Err(HostError::BadUTF16.into())); assert_costs(map! { ExtCosts::base: 1, - ExtCosts::read_memory_base: len / 2, - ExtCosts::read_memory_byte: len, + ExtCosts::read_memory_base: bytes.len / 2, + ExtCosts::read_memory_byte: bytes.len, ExtCosts::utf16_decoding_base: 1, - ExtCosts::utf16_decoding_byte: len - 2, + ExtCosts::utf16_decoding_byte: bytes.len - 2, }); } @@ -434,23 +402,20 @@ fn test_valid_log_utf16_null_terminated_fail() { fn test_sha256() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); - let data = b"tesdsst"; + let data = logic.internal_mem_write(b"tesdsst"); - logic.sha256(data.len() as _, data.as_ptr() as _, 0).unwrap(); - let mut res = [0u8; 32]; - logic.read_register(0, (&mut res[..]).as_ptr() as _).expect("OK"); - assert_eq!( - res, - [ + logic.sha256(data.len, data.ptr, 0).unwrap(); + logic.assert_read_register( + &[ 18, 176, 115, 156, 45, 100, 241, 132, 180, 134, 77, 42, 105, 111, 199, 127, 118, 112, 92, 255, 88, 43, 83, 147, 122, 55, 26, 36, 42, 156, 160, 158, - ] + ], + 0, ); - let len = data.len() as u64; assert_costs(map! { ExtCosts::base: 1, ExtCosts::read_memory_base: 1, - ExtCosts::read_memory_byte: len, + ExtCosts::read_memory_byte: data.len, ExtCosts::write_memory_base: 1, ExtCosts::write_memory_byte: 32, ExtCosts::read_register_base: 1, @@ -458,7 +423,7 @@ fn test_sha256() { ExtCosts::write_register_base: 1, ExtCosts::write_register_byte: 32, ExtCosts::sha256_base: 1, - ExtCosts::sha256_byte: len, + ExtCosts::sha256_byte: data.len, }); } @@ -466,23 +431,20 @@ fn test_sha256() { fn test_keccak256() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); - let data = b"tesdsst"; - logic.keccak256(data.len() as _, data.as_ptr() as _, 0).unwrap(); - let mut res = [0u8; 32]; - logic.read_register(0, (&mut res[..]).as_ptr() as _).expect("OK"); - assert_eq!( - res.as_slice(), + let data = logic.internal_mem_write(b"tesdsst"); + logic.keccak256(data.len, data.ptr, 0).unwrap(); + logic.assert_read_register( &[ 104, 110, 58, 122, 230, 181, 215, 145, 231, 229, 49, 162, 123, 167, 177, 58, 26, 142, - 129, 173, 7, 37, 9, 26, 233, 115, 64, 102, 61, 85, 10, 159 - ] + 129, 173, 7, 37, 9, 26, 233, 115, 64, 102, 61, 85, 10, 159, + ], + 0, ); - let len = data.len() as u64; assert_costs(map! { ExtCosts::base: 1, ExtCosts::read_memory_base: 1, - ExtCosts::read_memory_byte: len, + ExtCosts::read_memory_byte: data.len, ExtCosts::write_memory_base: 1, ExtCosts::write_memory_byte: 32, ExtCosts::read_register_base: 1, @@ -490,7 +452,7 @@ fn test_keccak256() { ExtCosts::write_register_base: 1, ExtCosts::write_register_byte: 32, ExtCosts::keccak256_base: 1, - ExtCosts::keccak256_byte: len, + ExtCosts::keccak256_byte: data.len, }); } @@ -498,25 +460,22 @@ fn test_keccak256() { fn test_keccak512() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); - let data = b"tesdsst"; - logic.keccak512(data.len() as _, data.as_ptr() as _, 0).unwrap(); - let mut res = [0u8; 64]; - logic.read_register(0, (&mut res[..]).as_ptr() as _).expect("OK"); - assert_eq!( - res, - [ + let data = logic.internal_mem_write(b"tesdsst"); + logic.keccak512(data.len, data.ptr, 0).unwrap(); + logic.assert_read_register( + &[ 55, 134, 96, 137, 168, 122, 187, 95, 67, 76, 18, 122, 146, 11, 225, 106, 117, 194, 154, 157, 48, 160, 90, 146, 104, 209, 118, 126, 222, 230, 200, 125, 48, 73, 197, 236, 123, 173, 192, 197, 90, 153, 167, 121, 100, 88, 209, 240, 137, 86, 239, 41, 87, 128, 219, - 249, 136, 203, 220, 109, 46, 168, 234, 190 - ] + 249, 136, 203, 220, 109, 46, 168, 234, 190, + ], + 0, ); - let len = data.len() as u64; assert_costs(map! { ExtCosts::base: 1, ExtCosts::read_memory_base: 1, - ExtCosts::read_memory_byte: len, + ExtCosts::read_memory_byte: data.len, ExtCosts::write_memory_base: 1, ExtCosts::write_memory_byte: 64, ExtCosts::read_register_base: 1, @@ -524,7 +483,7 @@ fn test_keccak512() { ExtCosts::write_register_base: 1, ExtCosts::write_register_byte: 64, ExtCosts::keccak512_base: 1, - ExtCosts::keccak512_byte: len, + ExtCosts::keccak512_byte: data.len, }); } @@ -533,19 +492,16 @@ fn test_ripemd160() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); - let data = b"tesdsst"; - logic.ripemd160(data.len() as _, data.as_ptr() as _, 0).unwrap(); - let mut res = [0u8; 20]; - logic.read_register(0, (&mut res[..]).as_ptr() as _).expect("OK"); - assert_eq!( - res, - [21, 102, 156, 115, 232, 3, 58, 215, 35, 84, 129, 30, 143, 86, 212, 104, 70, 97, 14, 225,] + let data = logic.internal_mem_write(b"tesdsst"); + logic.ripemd160(data.len, data.ptr, 0).unwrap(); + logic.assert_read_register( + &[21, 102, 156, 115, 232, 3, 58, 215, 35, 84, 129, 30, 143, 86, 212, 104, 70, 97, 14, 225], + 0, ); - let len = data.len() as u64; assert_costs(map! { ExtCosts::base: 1, ExtCosts::read_memory_base: 1, - ExtCosts::read_memory_byte: len, + ExtCosts::read_memory_byte: data.len, ExtCosts::write_memory_base: 1, ExtCosts::write_memory_byte: 20, ExtCosts::read_register_base: 1, @@ -588,10 +544,10 @@ fn test_ecrecover() { { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); + let m = logic.internal_mem_write(&m); + let sig = logic.internal_mem_write(&sig); - let b = logic - .ecrecover(32, m.as_ptr() as _, 64, sig.as_ptr() as _, v as _, mc as _, 1) - .unwrap(); + let b = logic.ecrecover(m.len, m.ptr, sig.len, sig.ptr, v as _, mc as _, 1).unwrap(); assert_eq!(b, res.is_some() as u64); if let Some(res) = res { @@ -602,9 +558,7 @@ fn test_ecrecover() { ExtCosts::write_register_byte: 64, ExtCosts::ecrecover_base: 1, }); - let result = [0u8; 64]; - logic.read_register(1, result.as_ptr() as _).unwrap(); - assert_eq!(res, result); + logic.assert_read_register(&res, 1); } else { assert_costs(map! { ExtCosts::read_memory_base: 2, @@ -625,14 +579,12 @@ fn test_hash256_register() { logic.wrapped_internal_write_register(1, data).unwrap(); logic.sha256(u64::MAX, 1, 0).unwrap(); - let mut res = [0u8; 32]; - logic.read_register(0, (&mut res[..]).as_ptr() as _).unwrap(); - assert_eq!( - res, - [ + logic.assert_read_register( + &[ 18, 176, 115, 156, 45, 100, 241, 132, 180, 134, 77, 42, 105, 111, 199, 127, 118, 112, 92, 255, 88, 43, 83, 147, 122, 55, 26, 36, 42, 156, 160, 158, - ] + ], + 0, ); let len = data.len() as u64; @@ -652,74 +604,61 @@ fn test_hash256_register() { #[test] fn test_key_length_limit() { let mut logic_builder = VMLogicBuilder::default(); - let mut key = "a".repeat(1024).as_bytes().to_vec(); - let val = b"hello"; - let limit = key.len() as u64; + let limit = 1024; logic_builder.config.limit_config.max_length_storage_key = limit; let mut logic = logic_builder.build(get_context(vec![], false)); + // Under the limit. Valid calls. + let key = crate::MemSlice { ptr: 0, len: limit }; + let val = crate::MemSlice { ptr: 0, len: 5 }; logic - .storage_has_key(key.len() as _, key.as_ptr() as _) + .storage_has_key(key.len, key.ptr) .expect("storage_has_key: key length is under the limit"); logic - .storage_write(key.len() as _, key.as_ptr() as _, val.len() as _, val.as_ptr() as _, 0) - .expect("storage_read: key length is under the limit"); + .storage_write(key.len, key.ptr, val.len, val.ptr, 0) + .expect("storage_write: key length is under the limit"); + logic.storage_read(key.len, key.ptr, 0).expect("storage_read: key length is under the limit"); logic - .storage_read(key.len() as _, key.as_ptr() as _, 0) - .expect("storage_read: key length is under the limit"); - logic - .storage_remove(key.len() as _, key.as_ptr() as _, 0) + .storage_remove(key.len, key.ptr, 0) .expect("storage_remove: key length is under the limit"); + // Over the limit. Invalid calls. - key.push(b'a'); + let key = crate::MemSlice { ptr: 0, len: limit + 1 }; assert_eq!( - logic.storage_has_key(key.len() as _, key.as_ptr() as _), - Err(HostError::KeyLengthExceeded { length: key.len() as _, limit }.into()) + logic.storage_has_key(key.len, key.ptr), + Err(HostError::KeyLengthExceeded { length: key.len, limit }.into()) ); assert_eq!( - logic.storage_write( - key.len() as _, - key.as_ptr() as _, - val.len() as _, - val.as_ptr() as _, - 0 - ), - Err(HostError::KeyLengthExceeded { length: key.len() as _, limit }.into()) + logic.storage_write(key.len, key.ptr, val.len, val.ptr, 0), + Err(HostError::KeyLengthExceeded { length: key.len, limit }.into()) ); assert_eq!( - logic.storage_read(key.len() as _, key.as_ptr() as _, 0), - Err(HostError::KeyLengthExceeded { length: key.len() as _, limit }.into()) + logic.storage_read(key.len, key.ptr, 0), + Err(HostError::KeyLengthExceeded { length: key.len, limit }.into()) ); assert_eq!( - logic.storage_remove(key.len() as _, key.as_ptr() as _, 0), - Err(HostError::KeyLengthExceeded { length: key.len() as _, limit }.into()) + logic.storage_remove(key.len, key.ptr, 0), + Err(HostError::KeyLengthExceeded { length: key.len, limit }.into()) ); } #[test] fn test_value_length_limit() { let mut logic_builder = VMLogicBuilder::default(); - let mut val = "a".repeat(1024).as_bytes().to_vec(); - logic_builder.config.limit_config.max_length_storage_value = val.len() as u64; + let limit = 1024; + logic_builder.config.limit_config.max_length_storage_value = limit; let mut logic = logic_builder.build(get_context(vec![], false)); - let key = b"hello"; + let key = logic.internal_mem_write(b"hello"); + logic - .storage_write(key.len() as _, key.as_ptr() as _, val.len() as _, val.as_ptr() as _, 0) - .expect("Value length is under the limit"); - val.push(b'a'); + .storage_write(key.len, key.ptr, limit / 2, 0, 0) + .expect("Value length doesn’t exceed the limit"); + logic + .storage_write(key.len, key.ptr, limit, 0, 0) + .expect("Value length doesn’t exceed the limit"); assert_eq!( - logic.storage_write( - key.len() as _, - key.as_ptr() as _, - val.len() as _, - val.as_ptr() as _, - 0 - ), - Err(HostError::ValueLengthExceeded { - length: val.len() as u64, - limit: logic_builder.config.limit_config.max_length_storage_value - } - .into()) + logic.storage_write(key.len, key.ptr, limit + 1, 0, 0), + Err(HostError::ValueLengthExceeded { length: limit + 1, limit }.into()) ); } @@ -729,17 +668,17 @@ fn test_num_promises() { let num_promises = 10; logic_builder.config.limit_config.max_promises_per_function_call_action = num_promises; let mut logic = logic_builder.build(get_context(vec![], false)); - let account_id = b"alice"; + let account_id = logic.internal_mem_write(b"alice"); for _ in 0..num_promises { logic - .promise_batch_create(account_id.len() as _, account_id.as_ptr() as _) + .promise_batch_create(account_id.len, account_id.ptr) .expect("Number of promises is under the limit"); } assert_eq!( - logic.promise_batch_create(account_id.len() as _, account_id.as_ptr() as _), + logic.promise_batch_create(account_id.len, account_id.ptr), Err(HostError::NumberPromisesExceeded { number_of_promises: num_promises + 1, - limit: logic_builder.config.limit_config.max_promises_per_function_call_action + limit: num_promises } .into()) ); @@ -751,22 +690,20 @@ fn test_num_joined_promises() { let num_deps = 10; logic_builder.config.limit_config.max_number_input_data_dependencies = num_deps; let mut logic = logic_builder.build(get_context(vec![], false)); - let account_id = b"alice"; + let account_id = logic.internal_mem_write(b"alice"); let promise_id = logic - .promise_batch_create(account_id.len() as _, account_id.as_ptr() as _) + .promise_batch_create(account_id.len, account_id.ptr) .expect("Number of promises is under the limit"); + let promises = + logic.internal_mem_write(&promise_id.to_le_bytes().repeat(num_deps as usize + 1)); for num in 0..num_deps { - let promises = vec![promise_id; num as usize]; - logic - .promise_and(promises.as_ptr() as _, promises.len() as _) - .expect("Number of joined promises is under the limit"); + logic.promise_and(promises.ptr, num).expect("Number of joined promises is under the limit"); } - let promises = vec![promise_id; (num_deps + 1) as usize]; assert_eq!( - logic.promise_and(promises.as_ptr() as _, promises.len() as _), + logic.promise_and(promises.ptr, num_deps + 1), Err(HostError::NumberInputDataDependenciesExceeded { - number_of_input_data_dependencies: promises.len() as u64, - limit: logic_builder.config.limit_config.max_number_input_data_dependencies, + number_of_input_data_dependencies: num_deps + 1, + limit: num_deps, } .into()) ); @@ -778,27 +715,29 @@ fn test_num_input_dependencies_recursive_join() { let num_steps = 10; logic_builder.config.limit_config.max_number_input_data_dependencies = 1 << num_steps; let mut logic = logic_builder.build(get_context(vec![], false)); - let account_id = b"alice"; + let account_id = logic.internal_mem_write(b"alice"); let original_promise_id = logic - .promise_batch_create(account_id.len() as _, account_id.as_ptr() as _) + .promise_batch_create(account_id.len, account_id.ptr) .expect("Number of promises is under the limit"); let mut promise_id = original_promise_id; for _ in 1..num_steps { - let promises = vec![promise_id, promise_id]; + let promises_ptr = logic.internal_mem_write(&promise_id.to_le_bytes()).ptr; + logic.internal_mem_write(&promise_id.to_le_bytes()); promise_id = logic - .promise_and(promises.as_ptr() as _, promises.len() as _) + .promise_and(promises_ptr, 2) .expect("Number of joined promises is under the limit"); } // The length of joined promises is exactly the limit (1024). - let promises = vec![promise_id, promise_id]; - logic - .promise_and(promises.as_ptr() as _, promises.len() as _) - .expect("Number of joined promises is under the limit"); + let promises_ptr = logic.internal_mem_write(&promise_id.to_le_bytes()).ptr; + logic.internal_mem_write(&promise_id.to_le_bytes()); + logic.promise_and(promises_ptr, 2).expect("Number of joined promises is under the limit"); // The length of joined promises exceeding the limit by 1 (total 1025). - let promises = vec![promise_id, promise_id, original_promise_id]; + let promises_ptr = logic.internal_mem_write(&promise_id.to_le_bytes()).ptr; + logic.internal_mem_write(&promise_id.to_le_bytes()); + logic.internal_mem_write(&original_promise_id.to_le_bytes()); assert_eq!( - logic.promise_and(promises.as_ptr() as _, promises.len() as _), + logic.promise_and(promises_ptr, 3), Err(HostError::NumberInputDataDependenciesExceeded { number_of_input_data_dependencies: logic_builder .config @@ -814,47 +753,34 @@ fn test_num_input_dependencies_recursive_join() { #[test] fn test_return_value_limit() { let mut logic_builder = VMLogicBuilder::default(); - let mut val = "a".repeat(1024).as_bytes().to_vec(); - logic_builder.config.limit_config.max_length_returned_data = val.len() as u64; + let limit = 1024; + logic_builder.config.limit_config.max_length_returned_data = limit; let mut logic = logic_builder.build(get_context(vec![], false)); - logic - .value_return(val.len() as _, val.as_ptr() as _) - .expect("Returned value length is under the limit"); - val.push(b'a'); + + logic.value_return(limit, 0).expect("Returned value length is under the limit"); assert_eq!( - logic.value_return(val.len() as _, val.as_ptr() as _), - Err(HostError::ReturnedValueLengthExceeded { - length: val.len() as u64, - limit: logic_builder.config.limit_config.max_length_returned_data - } - .into()) + logic.value_return(limit + 1, 0), + Err(HostError::ReturnedValueLengthExceeded { length: limit + 1, limit }.into()) ); } #[test] fn test_contract_size_limit() { let mut logic_builder = VMLogicBuilder::default(); - let mut code = "a".repeat(1024).as_bytes().to_vec(); - logic_builder.config.limit_config.max_contract_size = code.len() as u64; + let limit = 1024; + logic_builder.config.limit_config.max_contract_size = limit; let mut logic = logic_builder.build(get_context(vec![], false)); - let account_id = b"alice"; + + let account_id = logic.internal_mem_write(b"alice"); + let promise_id = logic - .promise_batch_create(account_id.len() as _, account_id.as_ptr() as _) + .promise_batch_create(account_id.len, account_id.ptr) .expect("Number of promises is under the limit"); logic - .promise_batch_action_deploy_contract(promise_id, code.len() as u64, code.as_ptr() as _) + .promise_batch_action_deploy_contract(promise_id, limit, 0) .expect("The length of the contract code is under the limit"); - code.push(b'a'); assert_eq!( - logic.promise_batch_action_deploy_contract( - promise_id, - code.len() as u64, - code.as_ptr() as _ - ), - Err(HostError::ContractSizeExceeded { - size: code.len() as u64, - limit: logic_builder.config.limit_config.max_contract_size - } - .into()) + logic.promise_batch_action_deploy_contract(promise_id, limit + 1, 0), + Err(HostError::ContractSizeExceeded { size: limit + 1, limit }.into()) ); } diff --git a/runtime/near-vm-logic/src/tests/mod.rs b/runtime/near-vm-logic/src/tests/mod.rs index d14a862d0d5..7a9a28b3552 100644 --- a/runtime/near-vm-logic/src/tests/mod.rs +++ b/runtime/near-vm-logic/src/tests/mod.rs @@ -4,7 +4,7 @@ mod context; mod ed25519_verify; mod fixtures; mod gas_counter; -mod helpers; +pub(crate) mod helpers; mod iterators; mod miscs; mod promises; @@ -13,3 +13,5 @@ mod storage_read_write; mod storage_usage; mod view_method; mod vm_logic_builder; + +use vm_logic_builder::TestVMLogic; diff --git a/runtime/near-vm-logic/src/tests/promises.rs b/runtime/near-vm-logic/src/tests/promises.rs index add7875e3ba..2c54169c38e 100644 --- a/runtime/near-vm-logic/src/tests/promises.rs +++ b/runtime/near-vm-logic/src/tests/promises.rs @@ -41,9 +41,8 @@ fn test_promise_results() { assert_eq!(logic.promise_result(1, 0), Ok(2), "Failed promise must return code 2"); assert_eq!(logic.promise_result(2, 0), Ok(0), "Pending promise must return 3"); - let buffer = [0u8; 4]; - logic.read_register(0, buffer.as_ptr() as u64).unwrap(); - assert_eq!(&buffer, b"test", "Only promise with result should write data into register"); + // Only promise with result should write data into register + logic.assert_read_register(b"test", 0); } #[test] @@ -51,12 +50,12 @@ fn test_promise_batch_action_function_call() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); let index = promise_create(&mut logic, b"rick.test", 0, 0).expect("should create a promise"); + let index_ptr = logic.internal_mem_write(&index.to_le_bytes()).ptr; promise_batch_action_function_call(&mut logic, 123, 0, 0) .expect_err("shouldn't accept not existent promise index"); - let non_receipt = logic - .promise_and(index.to_le_bytes().as_ptr() as _, 1u64) - .expect("should create a non-receipt promise"); + let non_receipt = + logic.promise_and(index_ptr, 1u64).expect("should create a non-receipt promise"); promise_batch_action_function_call(&mut logic, non_receipt, 0, 0) .expect_err("shouldn't accept non-receipt promise index"); @@ -83,13 +82,13 @@ fn test_promise_batch_action_create_account() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); let index = promise_create(&mut logic, b"rick.test", 0, 0).expect("should create a promise"); + let index_ptr = logic.internal_mem_write(&index.to_le_bytes()).ptr; logic .promise_batch_action_create_account(123) .expect_err("shouldn't accept not existent promise index"); - let non_receipt = logic - .promise_and(index.to_le_bytes().as_ptr() as _, 1u64) - .expect("should create a non-receipt promise"); + let non_receipt = + logic.promise_and(index_ptr, 1u64).expect("should create a non-receipt promise"); logic .promise_batch_action_create_account(non_receipt) .expect_err("shouldn't accept non-receipt promise index"); @@ -121,20 +120,21 @@ fn test_promise_batch_action_deploy_contract() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); let index = promise_create(&mut logic, b"rick.test", 0, 0).expect("should create a promise"); - let code = b"sample"; + + let index_ptr = logic.internal_mem_write(&index.to_le_bytes()).ptr; + let code = logic.internal_mem_write(b"sample"); logic - .promise_batch_action_deploy_contract(123, code.len() as u64, code.as_ptr() as _) + .promise_batch_action_deploy_contract(123, code.len, code.ptr) .expect_err("shouldn't accept not existent promise index"); - let non_receipt = logic - .promise_and(index.to_le_bytes().as_ptr() as _, 1u64) - .expect("should create a non-receipt promise"); + let non_receipt = + logic.promise_and(index_ptr, 1u64).expect("should create a non-receipt promise"); logic - .promise_batch_action_deploy_contract(non_receipt, code.len() as u64, code.as_ptr() as _) + .promise_batch_action_deploy_contract(non_receipt, code.len, code.ptr) .expect_err("shouldn't accept non-receipt promise index"); logic - .promise_batch_action_deploy_contract(index, code.len() as u64, code.as_ptr() as _) + .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!( @@ -171,22 +171,23 @@ fn test_promise_batch_action_transfer() { let mut logic = logic_builder.build(context); let index = promise_create(&mut logic, b"rick.test", 0, 0).expect("should create a promise"); + let index_ptr = logic.internal_mem_write(&index.to_le_bytes()).ptr; + let num_110u128 = logic.internal_mem_write(&110u128.to_le_bytes()); + let num_1u128 = logic.internal_mem_write(&1u128.to_le_bytes()); + logic - .promise_batch_action_transfer(123, 110u128.to_le_bytes().as_ptr() as _) + .promise_batch_action_transfer(123, num_110u128.ptr) .expect_err("shouldn't accept not existent promise index"); - let non_receipt = logic - .promise_and(index.to_le_bytes().as_ptr() as _, 1u64) - .expect("should create a non-receipt promise"); + let non_receipt = + logic.promise_and(index_ptr, 1u64).expect("should create a non-receipt promise"); logic - .promise_batch_action_transfer(non_receipt, 110u128.to_le_bytes().as_ptr() as _) + .promise_batch_action_transfer(non_receipt, num_110u128.ptr) .expect_err("shouldn't accept non-receipt promise index"); logic - .promise_batch_action_transfer(index, 110u128.to_le_bytes().as_ptr() as _) + .promise_batch_action_transfer(index, num_110u128.ptr) .expect("should add an action to transfer money"); - logic - .promise_batch_action_transfer(index, 1u128.to_le_bytes().as_ptr() as _) - .expect_err("not enough 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!( [ @@ -227,33 +228,21 @@ fn test_promise_batch_action_stake() { .try_to_vec() .unwrap(); + let key = logic.internal_mem_write(&key); + let index_ptr = logic.internal_mem_write(&index.to_le_bytes()).ptr; + let num_110u128 = logic.internal_mem_write(&110u128.to_le_bytes()); + logic - .promise_batch_action_stake( - 123, - 110u128.to_le_bytes().as_ptr() as _, - key.len() as u64, - key.as_ptr() as _, - ) + .promise_batch_action_stake(123, num_110u128.ptr, key.len, key.ptr) .expect_err("shouldn't accept not existent promise index"); - let non_receipt = logic - .promise_and(index.to_le_bytes().as_ptr() as _, 1u64) - .expect("should create a non-receipt promise"); + let non_receipt = + logic.promise_and(index_ptr, 1u64).expect("should create a non-receipt promise"); logic - .promise_batch_action_stake( - non_receipt, - 110u128.to_le_bytes().as_ptr() as _, - key.len() as u64, - key.as_ptr() as _, - ) + .promise_batch_action_stake(non_receipt, num_110u128.ptr, key.len, key.ptr) .expect_err("shouldn't accept non-receipt promise index"); logic - .promise_batch_action_stake( - index, - 110u128.to_le_bytes().as_ptr() as _, - key.len() as u64, - key.as_ptr() as _, - ) + .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!([ @@ -289,12 +278,12 @@ fn test_promise_batch_action_add_key_with_function_call() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(context); let index = promise_create(&mut logic, b"rick.test", 0, 0).expect("should create a promise"); - let serialized_key = "ed25519:5do5nkAEVhL8iteDvXNgxi4pWK78Y7DDadX11ArFNyrf" + let index_ptr = logic.internal_mem_write(&index.to_le_bytes()).ptr; + let key = "ed25519:5do5nkAEVhL8iteDvXNgxi4pWK78Y7DDadX11ArFNyrf" .parse::() .unwrap() .try_to_vec() .unwrap(); - let key = &&serialized_key; let nonce = 1; let allowance = 999u128; let receiver_id = b"sam"; @@ -303,20 +292,19 @@ fn test_promise_batch_action_add_key_with_function_call() { promise_batch_action_add_key_with_function_call( &mut logic, 123, - key, + &key, nonce, allowance, receiver_id, method_names, ) .expect_err("shouldn't accept non-existent promise index"); - let non_receipt = logic - .promise_and(index.to_le_bytes().as_ptr() as _, 1u64) - .expect("should create a non-receipt promise"); + let non_receipt = + logic.promise_and(index_ptr, 1u64).expect("should create a non-receipt promise"); promise_batch_action_add_key_with_function_call( &mut logic, non_receipt, - key, + &key, nonce, allowance, receiver_id, @@ -327,7 +315,7 @@ fn test_promise_batch_action_add_key_with_function_call() { promise_batch_action_add_key_with_function_call( &mut logic, index, - key, + &key, nonce, allowance, receiver_id, @@ -382,18 +370,20 @@ fn test_promise_batch_then() { let account_id = b"rick.test"; let index = promise_create(&mut logic, account_id, 0, 0).expect("should create a promise"); + let account_id = logic.internal_mem_write(&account_id[..]); + let index_slice = logic.internal_mem_write(&index.to_le_bytes()); + logic - .promise_batch_then(123, account_id.len() as u64, account_id.as_ptr() as _) + .promise_batch_then(123, account_id.len, account_id.ptr) .expect_err("shouldn't accept non-existent promise index"); - let non_receipt = logic - .promise_and(index.to_le_bytes().as_ptr() as _, 1u64) - .expect("should create a non-receipt promise"); + let non_receipt = + logic.promise_and(index_slice.ptr, 1u64).expect("should create a non-receipt promise"); logic - .promise_batch_then(non_receipt, account_id.len() as u64, account_id.as_ptr() as _) + .promise_batch_then(non_receipt, account_id.len, account_id.ptr) .expect("should accept non-receipt promise index"); logic - .promise_batch_then(index, account_id.len() as u64, account_id.as_ptr() as _) + .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!([ diff --git a/runtime/near-vm-logic/src/tests/registers.rs b/runtime/near-vm-logic/src/tests/registers.rs index 060f7ada47c..103cccf7d0d 100644 --- a/runtime/near-vm-logic/src/tests/registers.rs +++ b/runtime/near-vm-logic/src/tests/registers.rs @@ -10,9 +10,7 @@ fn test_one_register() { logic.wrapped_internal_write_register(0, &[0, 1, 2]).unwrap(); assert_eq!(logic.register_len(0).unwrap(), 3u64); - let buffer = [0u8; 3]; - logic.read_register(0, buffer.as_ptr() as u64).unwrap(); - assert_eq!(buffer, [0u8, 1, 2]); + logic.assert_read_register(&[0, 1, 2], 0); } #[test] @@ -37,10 +35,7 @@ fn test_many_registers() { for i in 0..max_registers { let value = (i * 10).to_le_bytes(); logic.wrapped_internal_write_register(i, &value).unwrap(); - - let buffer = [0u8; std::mem::size_of::()]; - logic.read_register(i, buffer.as_ptr() as u64).unwrap(); - assert_eq!(i * 10, u64::from_le_bytes(buffer)); + logic.assert_read_register(&(i * 10).to_le_bytes(), i); } // One more register hits the boundary check. diff --git a/runtime/near-vm-logic/src/tests/storage_read_write.rs b/runtime/near-vm-logic/src/tests/storage_read_write.rs index a64430759ea..7339a07c82a 100644 --- a/runtime/near-vm-logic/src/tests/storage_read_write.rs +++ b/runtime/near-vm-logic/src/tests/storage_read_write.rs @@ -32,29 +32,22 @@ fn test_storage_read_with_register() { logic.wrapped_internal_write_register(1, key).unwrap(); logic.storage_read(u64::MAX, 1 as _, 0).expect("storage read ok"); - let res = [0u8; 3]; - logic.read_register(0, res.as_ptr() as _).unwrap(); - assert_eq!(&res, b"bar"); + logic.assert_read_register(val, 0); } #[test] fn test_storage_remove_with_register() { let mut logic_builder = VMLogicBuilder::default(); - - let key: &[u8] = b"foo"; - let val: &[u8] = b"bar"; - let mut logic = logic_builder.build(get_context(vec![], false)); - logic - .storage_write(key.len() as _, key.as_ptr() as _, val.len() as _, val.as_ptr() as _, 0) - .expect("storage write ok"); + let key = logic.internal_mem_write(b"foo"); + let val = logic.internal_mem_write(b"bar"); - logic.wrapped_internal_write_register(1, key).unwrap(); + logic.storage_write(key.len, key.ptr, val.len, val.ptr, 0).expect("storage write ok"); + + logic.wrapped_internal_write_register(1, b"foo").unwrap(); logic.storage_remove(u64::MAX, 1 as _, 0).expect("storage remove ok"); - let res = [0u8; 3]; - logic.read_register(0, res.as_ptr() as _).unwrap(); - assert_eq!(&res, b"bar"); + logic.assert_read_register(b"bar", 0); } #[test] diff --git a/runtime/near-vm-logic/src/tests/storage_usage.rs b/runtime/near-vm-logic/src/tests/storage_usage.rs index 4bb87f4f973..b941c122bff 100644 --- a/runtime/near-vm-logic/src/tests/storage_usage.rs +++ b/runtime/near-vm-logic/src/tests/storage_usage.rs @@ -6,23 +6,16 @@ fn test_storage_write_counter() { let mut logic_builder = VMLogicBuilder::default(); let data_record_cost = logic_builder.fees_config.storage_usage_config.num_extra_bytes_record; let mut logic = logic_builder.build(get_context(vec![], false)); - let key = b"foo"; - let val = b"bar"; + let key = logic.internal_mem_write(b"foo"); + let val = logic.internal_mem_write(b"bar"); - logic - .storage_write(key.len() as _, key.as_ptr() as _, val.len() as _, val.as_ptr() as _, 0) - .expect("storage write ok"); + logic.storage_write(key.len, key.ptr, val.len, val.ptr, 0).expect("storage write ok"); - let cost_expected = (data_record_cost as usize + key.len() + val.len()) as u64; + let cost_expected = data_record_cost + key.len + val.len; assert_eq!(logic.storage_usage().unwrap(), cost_expected); - let key = b"foo"; - let val = b"bar"; - - logic - .storage_write(key.len() as _, key.as_ptr() as _, val.len() as _, val.as_ptr() as _, 0) - .expect("storage write ok"); + logic.storage_write(key.len, key.ptr, val.len, val.ptr, 0).expect("storage write ok"); assert_eq!(logic.storage_usage().unwrap(), cost_expected); } @@ -31,15 +24,12 @@ fn test_storage_write_counter() { fn test_storage_remove() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); + let key = logic.internal_mem_write(b"foo"); + let val = logic.internal_mem_write(b"bar"); - let key = b"foo"; - let val = b"bar"; - - logic - .storage_write(key.len() as _, key.as_ptr() as _, val.len() as _, val.as_ptr() as _, 0) - .expect("storage write ok"); + logic.storage_write(key.len, key.ptr, val.len, val.ptr, 0).expect("storage write ok"); - logic.storage_remove(key.len() as _, key.as_ptr() as _, 0).expect("storage remove ok"); + logic.storage_remove(key.len, key.ptr, 0).expect("storage remove ok"); assert_eq!(logic.storage_usage().unwrap(), 0u64); } diff --git a/runtime/near-vm-logic/src/tests/vm_logic_builder.rs b/runtime/near-vm-logic/src/tests/vm_logic_builder.rs index c4715c26df1..473fa540aad 100644 --- a/runtime/near-vm-logic/src/tests/vm_logic_builder.rs +++ b/runtime/near-vm-logic/src/tests/vm_logic_builder.rs @@ -1,14 +1,13 @@ use crate::mocks::mock_external::MockedExternal; use crate::mocks::mock_memory::MockedMemory; -use crate::types::PromiseResult; -use crate::VMContext; -use crate::{VMConfig, VMLogic}; -use near_primitives_core::runtime::fees::RuntimeFeesConfig; +use crate::types::{Gas, PromiseResult}; +use crate::{ActionCosts, MemSlice, VMConfig, VMContext, VMLogic}; +use near_primitives_core::runtime::fees::{Fee, RuntimeFeesConfig}; use near_primitives_core::types::ProtocolVersion; -pub(crate) const LATEST_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::MAX; +pub(super) const LATEST_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::MAX; -pub struct VMLogicBuilder { +pub(super) struct VMLogicBuilder { pub ext: MockedExternal, pub config: VMConfig, pub fees_config: RuntimeFeesConfig, @@ -31,8 +30,8 @@ impl Default for VMLogicBuilder { } impl VMLogicBuilder { - pub fn build(&mut self, context: VMContext) -> VMLogic<'_> { - VMLogic::new_with_protocol_version( + pub fn build(&mut self, context: VMContext) -> TestVMLogic<'_> { + TestVMLogic::from(VMLogic::new_with_protocol_version( &mut self.ext, context, &self.config, @@ -40,8 +39,9 @@ impl VMLogicBuilder { &self.promise_results, &mut self.memory, self.current_protocol_version, - ) + )) } + pub fn free() -> Self { VMLogicBuilder { config: VMConfig::free(), @@ -52,4 +52,83 @@ impl VMLogicBuilder { current_protocol_version: LATEST_PROTOCOL_VERSION, } } + + pub fn max_gas_burnt(mut self, max_gas_burnt: Gas) -> Self { + self.config.limit_config.max_gas_burnt = max_gas_burnt; + self + } + + pub fn gas_fee(mut self, cost: ActionCosts, fee: Fee) -> Self { + self.fees_config.action_fees[cost] = fee; + self + } +} + +/// Wrapper around `VMLogic` which adds helper test methods. +pub(super) struct TestVMLogic<'a> { + logic: VMLogic<'a>, + /// Offset at which `internal_memory_write` will write next. + mem_write_offset: u64, +} + +impl<'a> std::convert::From> for TestVMLogic<'a> { + fn from(logic: VMLogic<'a>) -> Self { + Self { logic, mem_write_offset: 0 } + } +} + +impl<'a> std::ops::Deref for TestVMLogic<'a> { + type Target = VMLogic<'a>; + fn deref(&self) -> &Self::Target { + &self.logic + } +} + +impl std::ops::DerefMut for TestVMLogic<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.logic + } +} + +impl TestVMLogic<'_> { + /// Writes data into guest memory and returns pointer at its location. + /// + /// Subsequent calls to the method write buffers one after the other. It + /// makes it convenient to populate the memory with various different data + /// to later use in function calls. + pub(super) fn internal_mem_write(&mut self, data: &[u8]) -> MemSlice { + let ptr = self.mem_write_offset; + self.memory().set_for_free(ptr, data).unwrap(); + let len = data.len() as u64; + self.mem_write_offset += len; + MemSlice { len, ptr } + } + + /// Reads data from guest memory into a Vector. + pub(super) fn internal_mem_read(&mut self, ptr: u64, len: u64) -> Vec { + self.memory().view_for_free(MemSlice { ptr, len }).unwrap().into_owned() + } + + /// Calls `logic.read_register` and then on success reads data from guest + /// memory comparing it to expected value. + /// + /// The `read_register` call is made as if contract has made it. In + /// particular, gas is charged for it. Later reading of the contents of the + /// memory is done for free. Panics if the register is not set or contracts + /// runs out of gas. + /// + /// The value of the register is read onto the end of the guest memory + /// overriding anything that might already be there. + #[track_caller] + pub(super) fn assert_read_register(&mut self, want: &[u8], register_id: u64) { + let len = self.registers().get_len(register_id).unwrap(); + let ptr = MockedMemory::MEMORY_SIZE - len; + self.read_register(register_id, ptr).unwrap(); + let got = self.memory().view_for_free(MemSlice { ptr, len }).unwrap(); + assert_eq!(want, &got[..]); + } + + pub fn compute_outcome_and_distribute_gas(self) -> crate::VMOutcome { + self.logic.compute_outcome_and_distribute_gas() + } } diff --git a/runtime/near-vm-logic/src/vmstate.rs b/runtime/near-vm-logic/src/vmstate.rs index 91552c90e28..ee660205ee3 100644 --- a/runtime/near-vm-logic/src/vmstate.rs +++ b/runtime/near-vm-logic/src/vmstate.rs @@ -69,7 +69,7 @@ impl<'a> Memory<'a> { self.0.view_memory(slice).map_err(|_| HostError::MemoryAccessViolation.into()) } - #[cfg(feature = "sandbox")] + #[cfg(any(test, feature = "sandbox"))] /// Like [`Self::view`] but does not pay gas fees. pub(super) fn view_for_free(&self, slice: MemSlice) -> Result> { self.0.view_memory(slice).map_err(|_| HostError::MemoryAccessViolation.into()) @@ -95,6 +95,11 @@ impl<'a> Memory<'a> { self.0.write_memory(offset, buf).map_err(|_| HostError::MemoryAccessViolation.into()) } + #[cfg(test)] + pub(crate) fn set_for_free(&mut self, offset: u64, buf: &[u8]) -> Result<()> { + self.0.write_memory(offset, buf).map_err(|_| HostError::MemoryAccessViolation.into()) + } + memory_get!(u128, get_u128); memory_get!(u32, get_u32); memory_get!(u16, get_u16); @@ -142,6 +147,11 @@ impl Registers { } } + #[cfg(test)] + pub(super) fn get_for_free<'s>(&'s self, register_id: u64) -> Option<&'s [u8]> { + self.registers.get(®ister_id).map(|data| &data[..]) + } + /// Returns length of register with given index or None if no such register. pub(super) fn get_len(&self, register_id: u64) -> Option { self.registers.get(®ister_id).map(|data| data.len() as u64) diff --git a/runtime/near-vm-runner/src/memory.rs b/runtime/near-vm-runner/src/memory.rs index 7c62e7dbf74..aefd6ca6df8 100644 --- a/runtime/near-vm-runner/src/memory.rs +++ b/runtime/near-vm-runner/src/memory.rs @@ -65,5 +65,5 @@ impl MemoryLike for WasmerMemory { #[test] fn test_memory_like() { - crate::tests::test_memory_like(|| Box::new(WasmerMemory::new(1, 1))); + near_vm_logic::test_utils::test_memory_like(|| Box::new(WasmerMemory::new(1, 1))); } diff --git a/runtime/near-vm-runner/src/tests.rs b/runtime/near-vm-runner/src/tests.rs index d2285456ef7..bf396a0b8e5 100644 --- a/runtime/near-vm-runner/src/tests.rs +++ b/runtime/near-vm-runner/src/tests.rs @@ -9,7 +9,7 @@ mod wasm_validation; use crate::vm_kind::VMKind; use near_primitives::version::ProtocolVersion; -use near_vm_logic::{MemSlice, MemoryLike, VMContext}; +use near_vm_logic::VMContext; const CURRENT_ACCOUNT_ID: &str = "alice"; const SIGNER_ACCOUNT_ID: &str = "bob"; @@ -63,75 +63,3 @@ fn prepaid_loading_gas(bytes: usize) -> u64 { 0 } } - -/// Tests for implementation of MemoryLike interface. -/// -/// The `factory` returns a [`MemoryLike`] implementation to be tested. The -/// memory must be configured for indices `0..WASM_PAGE_SIZE` to be valid. -/// -/// Panics if any of the tests fails. -#[allow(dead_code)] -pub(crate) fn test_memory_like(factory: impl FnOnce() -> Box) { - // Hardcoded to work around build errors when wasmer_types is not available. - // Set to value of wasmer_types::WASM_PAGE_SIZE - const PAGE: u64 = 0x10000; - - struct TestContext { - mem: Box, - buf: [u8; PAGE as usize + 1], - } - - impl TestContext { - fn test_read(&mut self, ptr: u64, len: u64, value: u8) { - self.buf.fill(!value); - self.mem.fits_memory(MemSlice { ptr, len }).unwrap(); - self.mem.read_memory(ptr, &mut self.buf[..(len as usize)]).unwrap(); - assert!(self.buf[..(len as usize)].iter().all(|&v| v == value)); - } - - fn test_write(&mut self, ptr: u64, len: u64, value: u8) { - self.buf.fill(value); - self.mem.fits_memory(MemSlice { ptr, len }).unwrap(); - self.mem.write_memory(ptr, &self.buf[..(len as usize)]).unwrap(); - } - - fn test_oob(&mut self, ptr: u64, len: u64) { - self.buf.fill(42); - self.mem.fits_memory(MemSlice { ptr, len }).unwrap_err(); - self.mem.read_memory(ptr, &mut self.buf[..(len as usize)]).unwrap_err(); - assert!(self.buf[..(len as usize)].iter().all(|&v| v == 42)); - self.mem.write_memory(ptr, &self.buf[..(len as usize)]).unwrap_err(); - } - } - - let mut ctx = TestContext { mem: factory(), buf: [0; PAGE as usize + 1] }; - - // Test memory is initialised to zero. - ctx.test_read(0, PAGE, 0); - ctx.test_read(PAGE, 0, 0); - ctx.test_read(0, PAGE / 2, 0); - ctx.test_read(PAGE / 2, PAGE / 2, 0); - - // Test writing works. - ctx.test_write(0, PAGE / 2, 42); - ctx.test_read(0, PAGE / 2, 42); - ctx.test_read(PAGE / 2, PAGE / 2, 0); - - ctx.test_write(PAGE / 4, PAGE / 4, 24); - ctx.test_read(0, PAGE / 4, 42); - ctx.test_read(PAGE / 4, PAGE / 4, 24); - ctx.test_read(PAGE / 2, PAGE / 2, 0); - - // Zero memory. - ctx.test_write(0, PAGE, 0); - ctx.test_read(0, PAGE, 0); - - // Test out-of-bounds checks. - ctx.test_oob(0, PAGE + 1); - ctx.test_oob(1, PAGE); - ctx.test_oob(PAGE - 1, 2); - ctx.test_oob(PAGE, 1); - - // None of the writes in OOB should have any effect. - ctx.test_read(0, PAGE, 0); -} diff --git a/runtime/near-vm-runner/src/wasmer2_runner.rs b/runtime/near-vm-runner/src/wasmer2_runner.rs index c01e7755048..17cfc25e948 100644 --- a/runtime/near-vm-runner/src/wasmer2_runner.rs +++ b/runtime/near-vm-runner/src/wasmer2_runner.rs @@ -670,5 +670,5 @@ impl crate::runner::VM for Wasmer2VM { #[test] fn test_memory_like() { - crate::tests::test_memory_like(|| Box::new(Wasmer2Memory::new(1, 1).unwrap())); + near_vm_logic::test_utils::test_memory_like(|| Box::new(Wasmer2Memory::new(1, 1).unwrap())); } From bc5ea3c0b83ac7a65dfe05179dd4e0fb53df0306 Mon Sep 17 00:00:00 2001 From: nikurt <86772482+nikurt@users.noreply.github.com> Date: Sun, 15 Jan 2023 10:15:37 +0100 Subject: [PATCH 180/188] dump-state-parts can store state parts on S3 (#8307) * Sort state-viewer commands and add basic documentation for each command. * Sort state-viewer commands and add basic documentation for each command. * Sort state-viewer commands and add basic documentation for each command. * dump-state-parts can store parts on S3. This is experiment. * Comments * Bump dirs to 4 * ignore smartstring * Skip crates that were not encountered --- Cargo.lock | 196 ++++++++++++++++++++- Cargo.toml | 3 +- deny.toml | 8 +- tools/state-viewer/Cargo.toml | 1 + tools/state-viewer/src/cli.rs | 21 ++- tools/state-viewer/src/dump_state_parts.rs | 91 ++++++++-- 6 files changed, 298 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 93288979685..cacc8b57a48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -392,6 +392,22 @@ dependencies = [ "syn", ] +[[package]] +name = "attohttpc" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262c3f7f5d61249d8c00e5546e2685cd15ebeeb1bc0f3cc5449350a1cb07319e" +dependencies = [ + "http", + "log", + "native-tls", + "openssl", + "serde", + "serde_json", + "url", + "wildmatch", +] + [[package]] name = "atty" version = "0.2.14" @@ -444,6 +460,31 @@ dependencies = [ "tokio", ] +[[package]] +name = "aws-creds" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeeee1a5defa63cba39097a510dfe63ef53658fc8995202a610f6a8a4d03639" +dependencies = [ + "attohttpc", + "dirs", + "rust-ini", + "serde", + "serde-xml-rs", + "thiserror", + "time 0.3.9", + "url", +] + +[[package]] +name = "aws-region" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92a8af5850d0ea0916ca3e015ab86951ded0bf4b70fd27896e81ae1dfb0af37" +dependencies = [ + "thiserror", +] + [[package]] name = "backtrace" version = "0.3.65" @@ -558,6 +599,16 @@ dependencies = [ "generic-array 0.14.5", ] +[[package]] +name = "block_on_proc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b872f3528eeeb4370ee73b51194dc1cd93680c2d0eb6c7a223889038d2c1a167" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "bolero" version = "0.8.0" @@ -1478,9 +1529,9 @@ dependencies = [ [[package]] name = "dirs" -version = "3.0.2" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" dependencies = [ "dirs-sys", ] @@ -1502,6 +1553,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c97b9233581d84b8e1e689cdd3a47b6f69770084fc246e86a7f78b0d9c1d4a5" +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + [[package]] name = "dtoa" version = "0.4.8" @@ -2632,6 +2689,23 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "maybe-async" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6007f9dad048e0a224f27ca599d669fca8cfa0dac804725aab542b2eb032bce6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.5.0" @@ -2672,6 +2746,15 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "minidom" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dddfe21863f8d600ed2bd1096cb9b5cd6ff984be6185cf9d563fb4a107bffc5" +dependencies = [ + "rxml", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3304,7 +3387,7 @@ dependencies = [ "opentelemetry-semantic-conventions", "prometheus", "serde", - "smartstring", + "smartstring 1.0.1", "strum", "thiserror", "tokio", @@ -4027,6 +4110,16 @@ dependencies = [ "opentelemetry", ] +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.1", +] + [[package]] name = "os_str_bytes" version = "6.0.1" @@ -4817,6 +4910,7 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-native-tls", + "tokio-util 0.6.10", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -4979,6 +5073,48 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if 1.0.0", + "ordered-multimap", +] + +[[package]] +name = "rust-s3" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6009d9d4cf910505534d62d380a0aa305805a2af0b5c3ad59a3024a0715b847" +dependencies = [ + "async-trait", + "aws-creds", + "aws-region", + "base64", + "block_on_proc", + "cfg-if 1.0.0", + "hex", + "hmac", + "http", + "log", + "maybe-async", + "md5", + "minidom", + "percent-encoding", + "reqwest", + "serde", + "serde-xml-rs", + "serde_derive", + "sha2 0.10.2", + "thiserror", + "time 0.3.9", + "tokio", + "tokio-stream", + "url", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -5035,6 +5171,25 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" +[[package]] +name = "rxml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a071866b8c681dc2cfffa77184adc32b57b0caad4e620b6292609703bceb804" +dependencies = [ + "bytes", + "pin-project-lite", + "rxml_validation", + "smartstring 0.2.10", + "tokio", +] + +[[package]] +name = "rxml_validation" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53bc79743f9a66c2fb1f951cd83735f275d46bfe466259fbc5897bb60a0d00ee" + [[package]] name = "ryu" version = "1.0.10" @@ -5157,6 +5312,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde-xml-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65162e9059be2f6a3421ebbb4fef3e74b7d9e7c60c50a0e292c6239f19f1edfa" +dependencies = [ + "log", + "serde", + "thiserror", + "xml-rs", +] + [[package]] name = "serde_bytes" version = "0.11.6" @@ -5384,6 +5551,15 @@ dependencies = [ "syn", ] +[[package]] +name = "smartstring" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e714dff2b33f2321fdcd475b71cec79781a692d846f37f415fb395a1d2bcd48e" +dependencies = [ + "static_assertions", +] + [[package]] name = "smartstring" version = "1.0.1" @@ -5458,6 +5634,7 @@ dependencies = [ "rand 0.8.5", "rayon", "redis", + "rust-s3", "serde", "serde_json", "tempfile", @@ -5737,6 +5914,7 @@ dependencies = [ "itoa 1.0.2", "libc", "num_threads", + "serde", "time-macros", ] @@ -6695,6 +6873,12 @@ dependencies = [ "libc", ] +[[package]] +name = "wildmatch" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee583bdc5ff1cf9db20e9db5bb3ff4c3089a8f6b8b31aff265c9aba85812db86" + [[package]] name = "winapi" version = "0.3.9" @@ -6787,6 +6971,12 @@ dependencies = [ "libc", ] +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + [[package]] name = "xshell" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 31836f0424e..18551d08027 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,7 +105,7 @@ crossbeam-channel = "0.5" csv = "1.1.1" curve25519-dalek = "3" derive_more = "0.99.9" -dirs = "3" +dirs = "4" easy-ext = "0.2" ed25519-dalek = "1" elastic-array = "0.11" @@ -163,6 +163,7 @@ reqwest = { version = "0.11.0", features = ["blocking"] } ripemd = "0.1.1" rlimit = "0.7" rocksdb = { version = "0.19.0", default-features = false, features = ["snappy", "lz4", "zstd", "zlib", "jemalloc"] } +rust-s3 = { version = "0.32.3", features = ["blocking"] } rusqlite = {version = "0.27.0", features = ["bundled", "chrono", "functions"] } secp256k1 = { version = "0.24", features = ["recovery", "rand-std"] } semver = "1.0.4" diff --git a/deny.toml b/deny.toml index 7581c21a25d..3a3a6156531 100644 --- a/deny.toml +++ b/deny.toml @@ -17,11 +17,6 @@ deny = [ ] skip = [ - # See https://github.com/camshaft/bolero/issues/85 - { name = "crossbeam-channel", version = "=0.4.4" }, - { name = "crossbeam-utils", version = "=0.7.2" }, - { name = "strsim", version = "=0.8.0" }, - { name = "clap", version = "=2.34.0" }, # criterion uses clap=2.34.0 which relies on an older textwrap { name = "textwrap", version = "=0.11.0" }, @@ -100,4 +95,7 @@ skip = [ # redis we’re using uses ancient sha { name = "sha1", version = "=0.6.1" }, + + # rust-s3 is using an old version of smartstring + { name = "smartstring", version = "=0.2.10" }, ] diff --git a/tools/state-viewer/Cargo.toml b/tools/state-viewer/Cargo.toml index f91d2398613..4b7abff486a 100644 --- a/tools/state-viewer/Cargo.toml +++ b/tools/state-viewer/Cargo.toml @@ -14,6 +14,7 @@ once_cell.workspace = true rand.workspace = true rayon.workspace = true redis.workspace = true +rust-s3.workspace = true serde.workspace = true serde_json.workspace = true tempfile.workspace = true diff --git a/tools/state-viewer/src/cli.rs b/tools/state-viewer/src/cli.rs index 49c2120be0d..5e765c1422d 100644 --- a/tools/state-viewer/src/cli.rs +++ b/tools/state-viewer/src/cli.rs @@ -357,11 +357,27 @@ pub struct DumpStatePartsCmd { part_id: Option, /// Where to write the state parts to. #[clap(long)] - output_dir: PathBuf, + output_dir: Option, + /// S3 bucket to store state parts. + #[clap(long)] + s3_bucket: Option, + /// S3 region to store state parts. + #[clap(long)] + s3_region: Option, } impl DumpStatePartsCmd { pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { + assert_eq!( + self.s3_bucket.is_some(), + self.s3_region.is_some(), + "Need to provide either both or none of --s3-bucket and --s3-region" + ); + let s3 = if let Some(s3_bucket) = self.s3_bucket { + Some((s3_bucket, self.s3_region.unwrap())) + } else { + None + }; dump_state_parts( self.epoch_selection, self.shard_id, @@ -369,7 +385,8 @@ impl DumpStatePartsCmd { home_dir, near_config, store, - &self.output_dir, + self.output_dir, + s3, ); } } diff --git a/tools/state-viewer/src/dump_state_parts.rs b/tools/state-viewer/src/dump_state_parts.rs index 1dbad67dc4c..06a62c324db 100644 --- a/tools/state-viewer/src/dump_state_parts.rs +++ b/tools/state-viewer/src/dump_state_parts.rs @@ -10,7 +10,7 @@ use near_primitives_core::hash::CryptoHash; use near_primitives_core::types::{BlockHeight, EpochHeight, ShardId}; use near_store::Store; use nearcore::{NearConfig, NightshadeRuntime}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; use std::time::Instant; @@ -108,7 +108,8 @@ pub(crate) fn dump_state_parts( home_dir: &Path, near_config: NearConfig, store: Store, - output_dir: &Path, + output_dir: Option, + s3_bucket_and_region: Option<(String, String)>, ) { let runtime_adapter: Arc = Arc::new(NightshadeRuntime::from_config(home_dir, store.clone(), &near_config)); @@ -148,7 +149,19 @@ pub(crate) fn dump_state_parts( "Dumping state as seen at the beginning of the specified epoch.", ); - std::fs::create_dir_all(output_dir).unwrap(); + let part_storage: Box = if let Some(output_dir) = output_dir { + Box::new(FileSystemStorage::new(output_dir)) + } else { + let (s3_bucket, s3_region) = s3_bucket_and_region.unwrap(); + Box::new(S3Storage::new( + &s3_bucket, + &s3_region, + &near_config.client_config.chain_id, + epoch.epoch_height(), + shard_id, + )) + }; + for part_id in if let Some(part_id) = part_id { part_id..part_id + 1 } else { 0..num_parts } { let now = Instant::now(); assert!(part_id < num_parts, "part_id: {}, num_parts: {}", part_id, num_parts); @@ -160,14 +173,70 @@ pub(crate) fn dump_state_parts( PartId::new(part_id, num_parts), ) .unwrap(); - let filename = output_dir.join(format!("state_part_{:06}", part_id)); - let len = state_part.len(); + part_storage.store(&state_part, part_id, now); + } +} + +trait StatePartRecorder { + fn store(&self, state_part: &[u8], part_id: u64, timer: Instant); +} + +struct FileSystemStorage { + output_dir: PathBuf, +} + +impl FileSystemStorage { + fn new(output_dir: PathBuf) -> Self { + std::fs::create_dir_all(&output_dir).unwrap(); + Self { output_dir } + } +} + +impl StatePartRecorder for FileSystemStorage { + fn store(&self, state_part: &[u8], part_id: u64, timer: Instant) { + let filename = self.output_dir.join(format!("state_part_{:06}", part_id)); std::fs::write(&filename, state_part).unwrap(); - tracing::info!( - target: "dump-state-parts", - part_id, - part_length = len, - ?filename, - elapsed_sec = now.elapsed().as_secs_f64()); + let len = state_part.len(); + tracing::info!(target: "dump-state-parts", part_id, part_length = len, ?filename, elapsed_sec = timer.elapsed().as_secs_f64(), "Wrote a state part on disk"); + } +} + +struct S3Storage { + prefix: String, + bucket: s3::Bucket, +} + +impl S3Storage { + fn new( + s3_bucket: &str, + s3_region: &str, + chain_id: &str, + epoch_height: u64, + shard_id: u64, + ) -> Self { + let prefix = + format!("/chain_id={}/epoch_height={}/shard_id={}", chain_id, epoch_height, shard_id); + let bucket = s3::Bucket::new( + &s3_bucket, + s3_region.parse().unwrap(), + s3::creds::Credentials::default().unwrap(), + ) + .unwrap(); + + tracing::info!(target: "dump-state-parts", s3_bucket, s3_region, prefix, "Initialized an S3 bucket"); + Self { prefix, bucket } + } + + fn get_location(&self, part_id: u64) -> String { + format!("{}/state_part_{:06}", self.prefix, part_id) + } +} + +impl StatePartRecorder for S3Storage { + fn store(&self, state_part: &[u8], part_id: u64, timer: Instant) { + let location = self.get_location(part_id); + self.bucket.put_object_blocking(&location, &state_part).unwrap(); + let len = state_part.len(); + tracing::info!(target: "dump-state-parts", part_id, part_length = len, ?location, elapsed_sec = timer.elapsed().as_secs_f64(), "Wrote a state part to S3"); } } From e62bfed12b962821c546571d50776272a36cb961 Mon Sep 17 00:00:00 2001 From: Andrei Kashin Date: Mon, 16 Jan 2023 12:28:50 +0000 Subject: [PATCH 181/188] refactor: Get rid of JSON in parameter_table (#8344) This PR simplifies parameter parsing code by removing JSON usage and instead directly storing parameter values as YAML. Functionally, the code should behave the same for the users of `ParameterTable`. This is a part of refactoring for https://github.com/near/nearcore/issues/8264. I'm still pondering using a more specialized enum type to store parameter values, but not yet sure how the implementation will look like, so delaying this until I start implementing parameter weights in config. --- .../primitives/src/runtime/parameter_table.rs | 137 +++++++++--------- 1 file changed, 72 insertions(+), 65 deletions(-) diff --git a/core/primitives/src/runtime/parameter_table.rs b/core/primitives/src/runtime/parameter_table.rs index a61ba17dd1d..4d4edb9f6f6 100644 --- a/core/primitives/src/runtime/parameter_table.rs +++ b/core/primitives/src/runtime/parameter_table.rs @@ -4,17 +4,16 @@ use near_primitives_core::parameter::{FeeParameter, Parameter}; use near_primitives_core::runtime::fees::{RuntimeFeesConfig, StorageUsageConfig}; use num_rational::Rational; use serde::de::DeserializeOwned; -use serde_json::json; use std::any::Any; use std::collections::BTreeMap; pub(crate) struct ParameterTable { - parameters: BTreeMap, + parameters: BTreeMap, } /// Changes made to parameters between versions. pub(crate) struct ParameterTableDiff { - parameters: BTreeMap, + parameters: BTreeMap, } /// Error returned by ParameterTable::from_txt() that parses a runtime @@ -24,23 +23,21 @@ pub(crate) enum InvalidConfigError { #[error("could not parse `{1}` as a parameter")] UnknownParameter(#[source] strum::ParseError, String), #[error("could not parse `{1}` as a value")] - ValueParseError(#[source] serde_json::Error, String), - #[error("intermediate JSON created by parser does not match `RuntimeConfig`")] - WrongStructure(#[source] serde_json::Error), + ValueParseError(#[source] serde_yaml::Error, String), #[error("could not parse YAML that defines the structure of the config")] InvalidYaml(#[source] serde_yaml::Error), - #[error("config diff expected to contain old value `{1}` for parameter `{0}`")] - OldValueExists(Parameter, String), + #[error("config diff expected to contain old value `{1:?}` for parameter `{0}`")] + OldValueExists(Parameter, serde_yaml::Value), #[error( - "unexpected old value `{1}` for parameter `{0}` in config diff, previous version does not have such a value" + "unexpected old value `{1:?}` for parameter `{0}` in config diff, previous version does not have such a value" )] - NoOldValueExists(Parameter, String), - #[error("expected old value `{1}` but found `{2}` for parameter `{0}` in config diff")] - WrongOldValue(Parameter, String, String), + NoOldValueExists(Parameter, serde_yaml::Value), + #[error("expected old value `{1:?}` but found `{2:?}` for parameter `{0}` in config diff")] + WrongOldValue(Parameter, serde_yaml::Value, serde_yaml::Value), #[error("expected a value for `{0}` but found none")] MissingParameter(Parameter), - #[error("expected a value of type `{2}` for `{1}` but could not parse it from `{3}`")] - WrongValueType(#[source] serde_json::Error, Parameter, &'static str, String), + #[error("expected a value of type `{2}` for `{1}` but could not parse it from `{3:?}`")] + WrongValueType(#[source] serde_yaml::Error, Parameter, &'static str, serde_yaml::Value), } impl std::str::FromStr for ParameterTable { @@ -100,8 +97,8 @@ impl TryFrom<&ParameterTable> for RuntimeConfig { }, grow_mem_cost: params.get_parsed(Parameter::WasmGrowMemCost)?, regular_op_cost: params.get_parsed(Parameter::WasmRegularOpCost)?, - limit_config: serde_json::from_value(params.json_map(Parameter::vm_limits(), "")) - .map_err(InvalidConfigError::WrongStructure)?, + limit_config: serde_yaml::from_value(params.yaml_map(Parameter::vm_limits(), "")) + .map_err(InvalidConfigError::InvalidYaml)?, }, account_creation_config: AccountCreationConfig { min_allowed_top_level_account_length: params @@ -120,24 +117,24 @@ impl ParameterTable { for (key, (before, after)) in diff.parameters { if before.is_null() { match self.parameters.get(&key) { - Some(serde_json::Value::Null) | None => { + Some(serde_yaml::Value::Null) | None => { self.parameters.insert(key, after); } Some(old_value) => { - return Err(InvalidConfigError::OldValueExists(key, old_value.to_string())) + return Err(InvalidConfigError::OldValueExists(key, old_value.clone())) } } } else { match self.parameters.get(&key) { - Some(serde_json::Value::Null) | None => { - return Err(InvalidConfigError::NoOldValueExists(key, before.to_string())) + Some(serde_yaml::Value::Null) | None => { + return Err(InvalidConfigError::NoOldValueExists(key, before.clone())) } Some(old_value) => { if *old_value != before { return Err(InvalidConfigError::WrongOldValue( key, - old_value.to_string(), - before.to_string(), + old_value.clone(), + before.clone(), )); } else { self.parameters.insert(key, after); @@ -149,23 +146,23 @@ impl ParameterTable { Ok(()) } - fn json_map( + fn yaml_map( &self, params: impl Iterator, remove_prefix: &'static str, - ) -> serde_json::Value { - let mut json = serde_json::Map::new(); + ) -> serde_yaml::Value { + let mut yaml = serde_yaml::Mapping::new(); for param in params { let mut key: &'static str = param.into(); key = key.strip_prefix(remove_prefix).unwrap_or(key); if let Some(value) = self.get(*param) { - json.insert(key.to_owned(), value.clone()); + yaml.insert(key.into(), value.clone()); } } - json.into() + yaml.into() } - fn get(&self, key: Parameter) -> Option<&serde_json::Value> { + fn get(&self, key: Parameter) -> Option<&serde_yaml::Value> { self.parameters.get(&key) } @@ -174,9 +171,9 @@ impl ParameterTable { &self, cost: near_primitives_core::config::ActionCosts, ) -> near_primitives_core::runtime::fees::Fee { - let json = self.fee_json(FeeParameter::from(cost)); - serde_json::from_value::(json) - .expect("just constructed a Fee JSON") + let yaml = self.fee_yaml(FeeParameter::from(cost)); + serde_yaml::from_value::(yaml) + .expect("just constructed a Fee YAML") } /// Read and parse a parameter from the `ParameterTable`. @@ -185,12 +182,12 @@ impl ParameterTable { key: Parameter, ) -> Result { let value = self.parameters.get(&key).ok_or(InvalidConfigError::MissingParameter(key))?; - serde_json::from_value(value.clone()).map_err(|parse_err| { + serde_yaml::from_value(value.clone()).map_err(|parse_err| { InvalidConfigError::WrongValueType( parse_err, key, std::any::type_name::(), - value.to_string(), + value.clone(), ) }) } @@ -198,23 +195,36 @@ impl ParameterTable { /// Read and parse a parameter from the `ParameterTable`. fn get_u128(&self, key: Parameter) -> Result { let value = self.parameters.get(&key).ok_or(InvalidConfigError::MissingParameter(key))?; - - near_primitives_core::serialize::dec_format::deserialize(value).map_err(|parse_err| { - InvalidConfigError::WrongValueType( - parse_err, - key, - std::any::type_name::(), - value.to_string(), - ) - }) + match value { + // Values larger than u64 are stored as quoted strings, so we parse them as YAML + // document to leverage deserialization to u128. + serde_yaml::Value::String(v) => serde_yaml::from_str(v).map_err(|parse_err| { + InvalidConfigError::WrongValueType( + parse_err, + key, + std::any::type_name::(), + value.clone(), + ) + }), + // If the value is a number (or any other type), the usual conversion should work. + _ => serde_yaml::from_value(value.clone()).map_err(|parse_err| { + InvalidConfigError::WrongValueType( + parse_err, + key, + std::any::type_name::(), + value.clone(), + ) + }), + } } - fn fee_json(&self, key: FeeParameter) -> serde_json::Value { - json!( { - "send_sir": self.get(format!("{key}_send_sir").parse().unwrap()), - "send_not_sir": self.get(format!("{key}_send_not_sir").parse().unwrap()), - "execution": self.get(format!("{key}_execution").parse().unwrap()), - }) + fn fee_yaml(&self, key: FeeParameter) -> serde_yaml::Value { + serde_yaml::to_value(BTreeMap::from([ + ("send_sir", self.get(format!("{key}_send_sir").parse().unwrap())), + ("send_not_sir", self.get(format!("{key}_send_not_sir").parse().unwrap())), + ("execution", self.get(format!("{key}_execution").parse().unwrap())), + ])) + .expect("failed to construct fee yaml") } } @@ -241,13 +251,13 @@ impl std::str::FromStr for ParameterTableDiff { let old_value = if let Some(s) = &value.old { parse_parameter_txt_value(s)? } else { - serde_json::Value::Null + serde_yaml::Value::Null }; let new_value = if let Some(s) = &value.new { parse_parameter_txt_value(s)? } else { - serde_json::Value::Null + serde_yaml::Value::Null }; Ok((typed_key, (old_value, new_value))) @@ -261,9 +271,9 @@ impl std::str::FromStr for ParameterTableDiff { /// /// A value can be a positive integer or a string, both written without quotes. /// Integers can use underlines as separators (for readability). -fn parse_parameter_txt_value(value: &str) -> Result { +fn parse_parameter_txt_value(value: &str) -> Result { if value.is_empty() { - return Ok(serde_json::Value::Null); + return Ok(serde_yaml::Value::Null); } if value.bytes().all(|c| c.is_ascii_digit() || c == '_' as u8) { let mut raw_number = value.to_owned(); @@ -272,16 +282,13 @@ fn parse_parameter_txt_value(value: &str) -> Result { - assert_eq!(expected, "3200000000"); - assert_eq!(found, "3200000"); + assert_eq!(expected, serde_yaml::to_value(3200000000i64).unwrap()); + assert_eq!(found, serde_yaml::to_value(3200000).unwrap()); } ); } @@ -547,7 +554,7 @@ max_memory_pages: { new: 512 } &["min_allowed_top_level_account_length: { new: 1_600_000 }"] ), InvalidConfigError::OldValueExists(Parameter::MinAllowedTopLevelAccountLength, expected) => { - assert_eq!(expected, "3200000000"); + assert_eq!(expected, serde_yaml::to_value(3200000000i64).unwrap()); } ); } @@ -560,7 +567,7 @@ max_memory_pages: { new: 512 } &["wasm_regular_op_cost: { old: 3_200_000, new: 1_600_000 }"] ), InvalidConfigError::NoOldValueExists(Parameter::WasmRegularOpCost, found) => { - assert_eq!(found, "3200000"); + assert_eq!(found, serde_yaml::to_value(3200000).unwrap()); } ); } From 5a166d981d659c38a6415945aa52cf6d450bcc07 Mon Sep 17 00:00:00 2001 From: pompon0 Date: Mon, 16 Jan 2023 14:53:13 +0100 Subject: [PATCH 182/188] bumped protocol version (#8360) It will allow to introduce backward incompatible changes to network protocol (TIER1-related) in the next release (at version 59). Also AFAIR we agreed to preemptively bump the version at each release. --- chain/chain/src/tests/simple_chain.rs | 4 ++-- chain/jsonrpc/jsonrpc-tests/res/genesis_config.json | 2 +- core/primitives/src/version.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/chain/chain/src/tests/simple_chain.rs b/chain/chain/src/tests/simple_chain.rs index a337d76d5e3..8dc269a8608 100644 --- a/chain/chain/src/tests/simple_chain.rs +++ b/chain/chain/src/tests/simple_chain.rs @@ -45,7 +45,7 @@ fn build_chain() { if cfg!(feature = "nightly") { insta::assert_display_snapshot!(hash, @"HTpETHnBkxcX1h3eD87uC5YP5nV66E6UYPrJGnQHuRqt"); } else { - insta::assert_display_snapshot!(hash, @"2iGtRFjF6BcqPF6tDcfLLojRaNax2PKDLxRqRc3RxRn7"); + insta::assert_display_snapshot!(hash, @"7r5VSLXhkxHHEeiAAPQbKPGv3rr877obehGYwPbKZMA7"); } for i in 1..5 { @@ -75,7 +75,7 @@ fn build_chain() { if cfg!(feature = "nightly") { insta::assert_display_snapshot!(hash, @"HyDYbjs5tgeEDf1N1XB4m312VdCeKjHqeGQ7dc7Lqwv8"); } else { - insta::assert_display_snapshot!(hash, @"7BkghFM7ZA8piYHAWYu4vTY6vE1pkTwy14bqQnS138qE"); + insta::assert_display_snapshot!(hash, @"9772sSKzm1eGPV3pRi17YaZkotrcN6dAkJUn226CopTm"); } } diff --git a/chain/jsonrpc/jsonrpc-tests/res/genesis_config.json b/chain/jsonrpc/jsonrpc-tests/res/genesis_config.json index 4a81f4eccd2..03b9a0c4bbd 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": 57, + "protocol_version": 58, "genesis_time": "1970-01-01T00:00:00.000000000Z", "chain_id": "sample", "genesis_height": 0, diff --git a/core/primitives/src/version.rs b/core/primitives/src/version.rs index 70f60d87aa5..dbb7b6e23e5 100644 --- a/core/primitives/src/version.rs +++ b/core/primitives/src/version.rs @@ -156,7 +156,7 @@ pub const PEER_MIN_ALLOWED_PROTOCOL_VERSION: ProtocolVersion = STABLE_PROTOCOL_V /// 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 = 57; +const STABLE_PROTOCOL_VERSION: ProtocolVersion = 58; /// Largest protocol version supported by the current binary. pub const PROTOCOL_VERSION: ProtocolVersion = if cfg!(feature = "nightly_protocol") { From c9f78414a0e8ee49103267a12a2ead1ebf67ab3b Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Mon, 16 Jan 2023 15:46:52 +0100 Subject: [PATCH 183/188] fix: fix block_refcount_final validation (#8366) `block_refcount_final` was incorrectly refactored in #8299 to check for `sv.inner.genesis_blocks` and `sv.inner.block_refcount` being non-empty. It should check for more than 1 element instead. This was caught by [the nayduck test](https://nayduck.near.org/#/test/410756): ``` 2023-01-15T12:31:09.117757Z ERROR handle{handler="NetworkAdversarialMessage" actor="ClientActor" msg_type="NetworkAdversarialMessage"}: client: Storage Validation failed, [ErrorMessage { col: "BlockRefCount", key: "\"BLOCK_REFCOUNT\"", err: ValidationFailed { func_name: "f", error: "Found 1 Blocks that are not counted, e.g. (B7Sbqj2uD5M3yACTDgSDG9haw2L5vEdLsJiosuPZCDn1, 1)" } }] ``` See [zulip thread](https://near.zulipchat.com/#narrow/stream/295302-general/topic/failing.20nayduck.20tests) for more context. --- chain/chain/src/store_validator/validate.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/chain/chain/src/store_validator/validate.rs b/chain/chain/src/store_validator/validate.rs index c352e3c6bc1..a81455e0374 100644 --- a/chain/chain/src/store_validator/validate.rs +++ b/chain/chain/src/store_validator/validate.rs @@ -908,15 +908,21 @@ pub(crate) fn receipt_refcount_final(sv: &mut StoreValidator) -> Result<(), Stor } pub(crate) fn block_refcount_final(sv: &mut StoreValidator) -> Result<(), StoreValidatorError> { - if let Some(block_refcount) = sv.inner.block_refcount.iter().next() { + let block_refcount_len = sv.inner.block_refcount.len(); + if block_refcount_len >= 2 { err!( "Found {:?} Blocks that are not counted, e.g. {:?}", - sv.inner.block_refcount.len(), - block_refcount + block_refcount_len, + sv.inner.block_refcount.iter().next() ); } - if let Some(tail_block) = sv.inner.genesis_blocks.first() { - err!("Found {:?} Genesis Blocks, e.g. {:?}", sv.inner.genesis_blocks.len(), tail_block); + let genesis_blocks_len = sv.inner.genesis_blocks.len(); + if genesis_blocks_len >= 2 { + err!( + "Found {:?} Genesis Blocks, e.g. {:?}", + genesis_blocks_len, + sv.inner.genesis_blocks.first() + ); } Ok(()) } From b2cf09b46a4b531aaa584ffa370d90ccc9930b71 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Mon, 16 Jan 2023 16:17:55 +0100 Subject: [PATCH 184/188] store: extend test_encode_decode RawTrieNode test (#8225) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Firstly, add branch with no value to the test cases. Secondly, rather than testing just RawTrieNode encoding, test whole RawTrieNodeWithSize encoding. It’s always the latter that is being read and decoded from the storage so make sure that we test it. RawTrieNode is really just an implementation detail. --- core/store/src/trie/mod.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/core/store/src/trie/mod.rs b/core/store/src/trie/mod.rs index abd2e5ef353..e3844cca532 100644 --- a/core/store/src/trie/mod.rs +++ b/core/store/src/trie/mod.rs @@ -1112,13 +1112,14 @@ mod tests { fn test_encode_decode() { fn test(node: RawTrieNode, encoded: &[u8]) { let mut buf = Vec::new(); + let node = RawTrieNodeWithSize { node, memory_usage: 42 }; node.encode_into(&mut buf); assert_eq!(encoded, buf.as_slice()); - assert_eq!(node, RawTrieNode::decode(&buf).unwrap()); + assert_eq!(node, RawTrieNodeWithSize::decode(&buf).unwrap()); // Test that adding garbage at the end fails decoding. buf.push(b'!'); - let got = RawTrieNode::decode(&buf); + let got = RawTrieNodeWithSize::decode(&buf); assert!(got.is_err(), "got: {got:?}"); } @@ -1128,7 +1129,16 @@ mod tests { let encoded = [ 0, 3, 0, 0, 0, 1, 2, 3, 3, 0, 0, 0, 194, 40, 8, 24, 64, 219, 69, 132, 86, 52, 110, 175, 57, 198, 165, 200, 83, 237, 211, 11, 194, 83, 251, 33, 145, 138, 234, 226, 7, 242, 186, - 73, + 73, 42, 0, 0, 0, 0, 0, 0, 0, + ]; + test(node, &encoded); + + let mut children: [Option; 16] = Default::default(); + children[3] = Some(Trie::EMPTY_ROOT); + let node = RawTrieNode::Branch(children, None); + let encoded = [ + 1, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, ]; test(node, &encoded); @@ -1139,13 +1149,14 @@ mod tests { 2, 3, 0, 0, 0, 194, 40, 8, 24, 64, 219, 69, 132, 86, 52, 110, 175, 57, 198, 165, 200, 83, 237, 211, 11, 194, 83, 251, 33, 145, 138, 234, 226, 7, 242, 186, 73, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 42, 0, 0, 0, 0, 0, 0, 0, ]; test(node, &encoded); let node = RawTrieNode::Extension(vec![123, 245, 255], Trie::EMPTY_ROOT); let encoded = [ 3, 3, 0, 0, 0, 123, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, ]; test(node, &encoded); } From eaca22630fd6470a09b93a77de3651b0645d2264 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Mon, 16 Jan 2023 18:30:23 +0100 Subject: [PATCH 185/188] near-vm-logic: test log methods behaviour when memory access is violated (#8359) * near-vm-logic: test log methods behaviour when memory access is violated * wip * Apply suggestions from code review fix typos Co-authored-by: Jakob Meier --- runtime/near-vm-logic/src/tests/logs.rs | 510 ++++++++++++++++++ runtime/near-vm-logic/src/tests/miscs.rs | 390 -------------- runtime/near-vm-logic/src/tests/mod.rs | 1 + .../src/tests/vm_logic_builder.rs | 12 +- 4 files changed, 519 insertions(+), 394 deletions(-) create mode 100644 runtime/near-vm-logic/src/tests/logs.rs diff --git a/runtime/near-vm-logic/src/tests/logs.rs b/runtime/near-vm-logic/src/tests/logs.rs new file mode 100644 index 00000000000..b1d87967120 --- /dev/null +++ b/runtime/near-vm-logic/src/tests/logs.rs @@ -0,0 +1,510 @@ +use crate::tests::fixtures::get_context; +use crate::tests::helpers::*; +use crate::tests::vm_logic_builder::VMLogicBuilder; +use crate::{map, ExtCosts, MemSlice, VMLogic, VMLogicError}; +use near_vm_errors::HostError; + +#[test] +fn test_valid_utf8() { + let mut logic_builder = VMLogicBuilder::default(); + let mut logic = logic_builder.build(get_context(vec![], false)); + 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(); + assert_eq!(outcome.logs[0], string); + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::log_base: 1, + ExtCosts::log_byte: bytes.len, + ExtCosts::read_memory_base: 1, + ExtCosts::read_memory_byte: bytes.len, + ExtCosts::utf8_decoding_base: 1, + ExtCosts::utf8_decoding_byte: bytes.len, + }); +} + +#[test] +fn test_invalid_utf8() { + let mut logic_builder = VMLogicBuilder::default(); + let mut logic = logic_builder.build(get_context(vec![], false)); + 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(); + assert_eq!(outcome.logs.len(), 0); + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::read_memory_base: 1, + ExtCosts::read_memory_byte: bytes.len, + ExtCosts::utf8_decoding_base: 1, + ExtCosts::utf8_decoding_byte: bytes.len, + }); +} + +#[test] +fn test_valid_null_terminated_utf8() { + let mut logic_builder = VMLogicBuilder::default(); + + let cstring = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%\x00"; + let string = &cstring[..cstring.len() - 1]; + logic_builder.config.limit_config.max_total_log_length = string.len() as u64; + let mut logic = logic_builder.build(get_context(vec![], false)); + 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(); + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::log_base: 1, + ExtCosts::log_byte: string.len() as u64, + ExtCosts::read_memory_base: bytes.len, + ExtCosts::read_memory_byte: bytes.len, + ExtCosts::utf8_decoding_base: 1, + ExtCosts::utf8_decoding_byte: string.len() as u64, + }); + assert_eq!(outcome.logs[0], string); +} + +#[test] +fn test_log_max_limit() { + let mut logic_builder = VMLogicBuilder::default(); + let string = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%"; + let limit = string.len() as u64 - 1; + logic_builder.config.limit_config.max_total_log_length = limit; + let mut logic = logic_builder.build(get_context(vec![], false)); + let bytes = logic.internal_mem_write(string.as_bytes()); + + assert_eq!( + logic.log_utf8(bytes.len, bytes.ptr), + Err(HostError::TotalLogLengthExceeded { length: bytes.len, limit }.into()) + ); + + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::utf8_decoding_base: 1, + }); + + let outcome = logic.compute_outcome_and_distribute_gas(); + assert_eq!(outcome.logs.len(), 0); +} + +#[test] +fn test_log_total_length_limit() { + let mut logic_builder = VMLogicBuilder::default(); + let string = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%".as_bytes(); + let num_logs = 10; + let limit = string.len() as u64 * num_logs - 1; + logic_builder.config.limit_config.max_total_log_length = limit; + logic_builder.config.limit_config.max_number_logs = num_logs; + let mut logic = logic_builder.build(get_context(vec![], false)); + let bytes = logic.internal_mem_write(string); + + for _ in 0..num_logs - 1 { + logic.log_utf8(bytes.len, bytes.ptr).expect("total is still under the limit"); + } + assert_eq!( + logic.log_utf8(bytes.len, bytes.ptr), + Err(HostError::TotalLogLengthExceeded { length: limit + 1, limit }.into()) + ); + + let outcome = logic.compute_outcome_and_distribute_gas(); + assert_eq!(outcome.logs.len() as u64, num_logs - 1); +} + +#[test] +fn test_log_number_limit() { + let mut logic_builder = VMLogicBuilder::default(); + let string = "blabla"; + let max_number_logs = 3; + logic_builder.config.limit_config.max_total_log_length = + (string.len() + 1) as u64 * (max_number_logs + 1); + logic_builder.config.limit_config.max_number_logs = max_number_logs; + let mut logic = logic_builder.build(get_context(vec![], false)); + let bytes = logic.internal_mem_write(string.as_bytes()); + for _ in 0..max_number_logs { + logic + .log_utf8(bytes.len, bytes.ptr) + .expect("Valid utf-8 string_bytes under the log number limit"); + } + assert_eq!( + logic.log_utf8(bytes.len, bytes.ptr), + Err(HostError::NumberOfLogsExceeded { limit: max_number_logs }.into()) + ); + + assert_costs(map! { + ExtCosts::base: max_number_logs + 1, + ExtCosts::log_base: max_number_logs, + ExtCosts::log_byte: bytes.len * max_number_logs, + ExtCosts::read_memory_base: max_number_logs, + ExtCosts::read_memory_byte: bytes.len * max_number_logs, + ExtCosts::utf8_decoding_base: max_number_logs, + ExtCosts::utf8_decoding_byte: bytes.len * max_number_logs, + }); + + let outcome = logic.compute_outcome_and_distribute_gas(); + assert_eq!(outcome.logs.len() as u64, max_number_logs); +} + +fn append_utf16(dst: &mut Vec, string: &str) { + for code_unit in string.encode_utf16() { + dst.extend_from_slice(&code_unit.to_le_bytes()); + } +} + +#[test] +fn test_log_utf16_number_limit() { + let string = "$ qò$`"; + let mut bytes = Vec::new(); + append_utf16(&mut bytes, string); + + let mut logic_builder = VMLogicBuilder::default(); + let max_number_logs = 3; + logic_builder.config.limit_config.max_total_log_length = + (bytes.len() + 1) as u64 * (max_number_logs + 1); + logic_builder.config.limit_config.max_number_logs = max_number_logs; + + let mut logic = logic_builder.build(get_context(vec![], false)); + let bytes = logic.internal_mem_write(&bytes); + for _ in 0..max_number_logs { + logic + .log_utf16(bytes.len, bytes.ptr) + .expect("Valid utf-16 string_bytes under the log number limit"); + } + assert_eq!( + logic.log_utf16(bytes.len, bytes.ptr), + Err(HostError::NumberOfLogsExceeded { limit: max_number_logs }.into()) + ); + + assert_costs(map! { + ExtCosts::base: max_number_logs + 1, + ExtCosts::log_base: max_number_logs, + ExtCosts::log_byte: string.len() as u64 * max_number_logs, + ExtCosts::read_memory_base: max_number_logs, + ExtCosts::read_memory_byte: bytes.len * max_number_logs, + ExtCosts::utf16_decoding_base: max_number_logs, + ExtCosts::utf16_decoding_byte: bytes.len * max_number_logs, + }); + + let outcome = logic.compute_outcome_and_distribute_gas(); + assert_eq!(outcome.logs.len() as u64, max_number_logs); +} + +#[test] +fn test_log_total_length_limit_mixed() { + let mut logic_builder = VMLogicBuilder::default(); + + let string = "abc"; + let mut utf16_bytes: Vec = vec![0u8; 0]; + append_utf16(&mut utf16_bytes, string); + + let num_logs_each = 10; + let limit = string.len() as u64 * (num_logs_each * 2 + 1) - 1; + logic_builder.config.limit_config.max_total_log_length = limit; + logic_builder.config.limit_config.max_number_logs = num_logs_each * 2 + 1; + let mut logic = logic_builder.build(get_context(vec![], false)); + + let utf8_bytes = logic.internal_mem_write(string.as_bytes()); + let utf16_bytes = logic.internal_mem_write(&utf16_bytes); + + for _ in 0..num_logs_each { + logic.log_utf16(utf16_bytes.len, utf16_bytes.ptr).expect("total is still under the limit"); + + logic.log_utf8(utf8_bytes.len, utf8_bytes.ptr).expect("total is still under the limit"); + } + assert_eq!( + logic.log_utf8(utf8_bytes.len, utf8_bytes.ptr), + Err(HostError::TotalLogLengthExceeded { length: limit + 1, limit }.into()) + ); + + let outcome = logic.compute_outcome_and_distribute_gas(); + assert_eq!(outcome.logs.len() as u64, num_logs_each * 2); +} + +#[test] +fn test_log_utf8_max_limit_null_terminated() { + let mut logic_builder = VMLogicBuilder::default(); + let bytes = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%\x00".as_bytes(); + let limit = (bytes.len() - 2) as u64; + logic_builder.config.limit_config.max_total_log_length = limit; + let mut logic = logic_builder.build(get_context(vec![], false)); + let bytes = logic.internal_mem_write(bytes); + + assert_eq!( + logic.log_utf8(u64::MAX, bytes.ptr), + Err(HostError::TotalLogLengthExceeded { length: limit + 1, limit }.into()) + ); + + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::read_memory_base: bytes.len - 1, + ExtCosts::read_memory_byte: bytes.len - 1, + ExtCosts::utf8_decoding_base: 1, + }); + + let outcome = logic.compute_outcome_and_distribute_gas(); + assert_eq!(outcome.logs.len(), 0); +} + +#[test] +fn test_valid_log_utf16() { + let mut logic_builder = VMLogicBuilder::default(); + let mut logic = logic_builder.build(get_context(vec![], false)); + + let string = "$ qò$`"; + let mut bytes = Vec::new(); + append_utf16(&mut bytes, string); + let bytes = logic.internal_mem_write(&bytes); + + logic.log_utf16(bytes.len, bytes.ptr).expect("Valid utf-16 string_bytes"); + + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::read_memory_base: 1, + ExtCosts::read_memory_byte: bytes.len, + ExtCosts::utf16_decoding_base: 1, + ExtCosts::utf16_decoding_byte: bytes.len, + ExtCosts::log_base: 1, + ExtCosts::log_byte: string.len() as u64, + }); + let outcome = logic.compute_outcome_and_distribute_gas(); + assert_eq!(outcome.logs[0], string); +} + +#[test] +fn test_valid_log_utf16_max_log_len_not_even() { + let mut logic_builder = VMLogicBuilder::default(); + logic_builder.config.limit_config.max_total_log_length = 5; + let mut logic = logic_builder.build(get_context(vec![], false)); + + let string = "ab"; + let mut bytes = Vec::new(); + append_utf16(&mut bytes, string); + append_utf16(&mut bytes, "\0"); + let bytes = logic.internal_mem_write(&bytes); + logic.log_utf16(u64::MAX, bytes.ptr).expect("Valid utf-16 bytes"); + + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::read_memory_base: bytes.len / 2, + ExtCosts::read_memory_byte: bytes.len, + ExtCosts::utf16_decoding_base: 1, + ExtCosts::utf16_decoding_byte: bytes.len - 2, + ExtCosts::log_base: 1, + ExtCosts::log_byte: string.len() as u64, + }); + + let string = "abc"; + let mut bytes = Vec::new(); + append_utf16(&mut bytes, string); + append_utf16(&mut bytes, "\0"); + let bytes = logic.internal_mem_write(&bytes); + assert_eq!( + logic.log_utf16(u64::MAX, bytes.ptr), + Err(HostError::TotalLogLengthExceeded { length: 6, limit: 5 }.into()) + ); + + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::read_memory_base: 2, + ExtCosts::read_memory_byte: 2 * 2, + ExtCosts::utf16_decoding_base: 1, + }); +} + +#[test] +fn test_log_utf8_max_limit_null_terminated_fail() { + let mut logic_builder = VMLogicBuilder::default(); + logic_builder.config.limit_config.max_total_log_length = 3; + let mut logic = logic_builder.build(get_context(vec![], false)); + let bytes = logic.internal_mem_write(b"abcdefgh\0"); + let res = logic.log_utf8(u64::MAX, bytes.ptr); + assert_eq!(res, Err(HostError::TotalLogLengthExceeded { length: 4, limit: 3 }.into())); + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::read_memory_base: logic_builder.config.limit_config.max_total_log_length + 1, + ExtCosts::read_memory_byte: logic_builder.config.limit_config.max_total_log_length + 1, + ExtCosts::utf8_decoding_base: 1, + }); +} + +#[test] +fn test_valid_log_utf16_null_terminated() { + let mut logic_builder = VMLogicBuilder::default(); + let mut logic = logic_builder.build(get_context(vec![], false)); + + let string = "$ qò$`"; + let mut bytes = Vec::new(); + append_utf16(&mut bytes, string); + bytes.extend_from_slice(&[0, 0]); + let bytes = logic.internal_mem_write(&bytes); + + logic.log_utf16(u64::MAX, bytes.ptr).expect("Valid utf-16 string_bytes"); + + let outcome = logic.compute_outcome_and_distribute_gas(); + assert_eq!(outcome.logs[0], string); + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::read_memory_base: bytes.len / 2 , + ExtCosts::read_memory_byte: bytes.len, + ExtCosts::utf16_decoding_base: 1, + ExtCosts::utf16_decoding_byte: bytes.len - 2, + ExtCosts::log_base: 1, + ExtCosts::log_byte: string.len() as u64, + }); +} + +#[test] +fn test_invalid_log_utf16() { + let mut logic_builder = VMLogicBuilder::default(); + let mut logic = logic_builder.build(get_context(vec![], false)); + let mut bytes: Vec = Vec::new(); + for u16_ in [0xD834u16, 0xDD1E, 0x006d, 0x0075, 0xD800, 0x0069, 0x0063] { + bytes.extend_from_slice(&u16_.to_le_bytes()); + } + let bytes = logic.internal_mem_write(&bytes); + let res = logic.log_utf16(bytes.len, bytes.ptr); + assert_eq!(res, Err(HostError::BadUTF16.into())); + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::read_memory_base: 1, + ExtCosts::read_memory_byte: bytes.len, + ExtCosts::utf16_decoding_base: 1, + ExtCosts::utf16_decoding_byte: bytes.len, + }); +} + +#[test] +fn test_valid_log_utf16_null_terminated_fail() { + let mut logic_builder = VMLogicBuilder::default(); + let mut logic = logic_builder.build(get_context(vec![], false)); + + let mut bytes = Vec::new(); + append_utf16(&mut bytes, "$ qò$`"); + bytes.extend_from_slice(&[0x00, 0xD8]); // U+D800, unpaired surrogate + append_utf16(&mut bytes, "foobarbaz\0"); + let bytes = logic.internal_mem_write(&bytes); + + let res = logic.log_utf16(u64::MAX, bytes.ptr); + assert_eq!(res, Err(HostError::BadUTF16.into())); + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::read_memory_base: bytes.len / 2, + ExtCosts::read_memory_byte: bytes.len, + ExtCosts::utf16_decoding_base: 1, + ExtCosts::utf16_decoding_byte: bytes.len - 2, + }); +} + +mod utf8_mem_violation { + use super::*; + + fn check(read_ok: bool, test: fn(&mut VMLogic<'_>, MemSlice) -> Result<(), VMLogicError>) { + let mut logic_builder = VMLogicBuilder::default(); + let mut logic = logic_builder.build(get_context(vec![], false)); + + let bytes = b"foo bar \xff baz qux"; + let bytes = logic.internal_mem_write_at(64 * 1024 - bytes.len() as u64, bytes); + let err = if read_ok { HostError::BadUTF8 } else { HostError::MemoryAccessViolation }; + assert_eq!(Err(err.into()), test(&mut logic, bytes)); + } + + #[test] + fn test_good_read() { + // The data is read correctly but it has invalid UTF-8 thus it ends up + // with BadUTF8 error and user being charged for decoding. + check(true, |logic, slice| logic.log_utf8(slice.len, slice.ptr)); + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::read_memory_base: 1, + ExtCosts::read_memory_byte: 17, + ExtCosts::utf8_decoding_base: 1, + ExtCosts::utf8_decoding_byte: 17, + }); + } + + #[test] + fn test_read_past_end() { + // The data goes past the end of the memory resulting in memory access + // violation. User is not charged for UTF-8 decoding (except for the + // base cost which is always charged). + check(false, |logic, slice| logic.log_utf8(slice.len + 1, slice.ptr)); + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::read_memory_base: 1, + ExtCosts::read_memory_byte: 18, + ExtCosts::utf8_decoding_base: 1, + }); + } + + #[test] + fn test_nul_past_end() { + // The call goes past the end of the memory trying to find NUL byte + // resulting in memory access violation. User is not charged for UTF-8 + // decoding (except for the base cost which is always charged). + check(false, |logic, slice| logic.log_utf8(u64::MAX, slice.ptr)); + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::read_memory_base: 18, + ExtCosts::read_memory_byte: 18, + ExtCosts::utf8_decoding_base: 1, + }); + } +} + +mod utf16_mem_violation { + use super::*; + + fn check(read_ok: bool, test: fn(&mut VMLogic<'_>, MemSlice) -> Result<(), VMLogicError>) { + let mut logic_builder = VMLogicBuilder::default(); + let mut logic = logic_builder.build(get_context(vec![], false)); + + let mut bytes = Vec::new(); + append_utf16(&mut bytes, "$ qò$`"); + bytes.extend_from_slice(&[0x00, 0xD8]); // U+D800, unpaired surrogate + append_utf16(&mut bytes, "foobarbaz"); + let bytes = logic.internal_mem_write_at(64 * 1024 - bytes.len() as u64, &bytes); + let err = if read_ok { HostError::BadUTF16 } else { HostError::MemoryAccessViolation }; + assert_eq!(Err(err.into()), test(&mut logic, bytes)); + } + + #[test] + fn test_good_read() { + // The data is read correctly but it has invalid UTF-16 thus it ends up + // with BadUTF16 error and user being charged for decoding. + check(true, |logic, slice| logic.log_utf16(slice.len, slice.ptr)); + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::read_memory_base: 1, + ExtCosts::read_memory_byte: 32, + ExtCosts::utf16_decoding_base: 1, + ExtCosts::utf16_decoding_byte: 32, + }); + } + + #[test] + fn test_read_past_end() { + // The data goes past the end of the memory resulting in memory access + // violation. User is not charged for UTF-16 decoding (except for the + // base cost which is always charged). + check(false, |logic, slice| logic.log_utf16(slice.len + 2, slice.ptr)); + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::read_memory_base: 1, + ExtCosts::read_memory_byte: 34, + ExtCosts::utf16_decoding_base: 1, + }); + } + + #[test] + fn test_nul_past_end() { + // The call goes past the end of the memory trying to find NUL word + // resulting in memory access violation. User is not charged for UTF-16 + // decoding (except for the base cost which is always charged). + check(false, |logic, slice| logic.log_utf16(u64::MAX, slice.ptr)); + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::read_memory_base: 17, + ExtCosts::read_memory_byte: 34, + ExtCosts::utf16_decoding_base: 1, + }); + } +} diff --git a/runtime/near-vm-logic/src/tests/miscs.rs b/runtime/near-vm-logic/src/tests/miscs.rs index 5bcf12e3944..ea6a33b6cee 100644 --- a/runtime/near-vm-logic/src/tests/miscs.rs +++ b/runtime/near-vm-logic/src/tests/miscs.rs @@ -8,396 +8,6 @@ use serde::{de::Error, Deserialize, Deserializer}; use serde_json::from_slice; use std::{fmt::Display, fs}; -#[test] -fn test_valid_utf8() { - let mut logic_builder = VMLogicBuilder::default(); - let mut logic = logic_builder.build(get_context(vec![], false)); - 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(); - assert_eq!(outcome.logs[0], string); - assert_costs(map! { - ExtCosts::base: 1, - ExtCosts::log_base: 1, - ExtCosts::log_byte: bytes.len, - ExtCosts::read_memory_base: 1, - ExtCosts::read_memory_byte: bytes.len, - ExtCosts::utf8_decoding_base: 1, - ExtCosts::utf8_decoding_byte: bytes.len, - }); -} - -#[test] -fn test_invalid_utf8() { - let mut logic_builder = VMLogicBuilder::default(); - let mut logic = logic_builder.build(get_context(vec![], false)); - 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(); - assert_eq!(outcome.logs.len(), 0); - assert_costs(map! { - ExtCosts::base: 1, - ExtCosts::read_memory_base: 1, - ExtCosts::read_memory_byte: bytes.len, - ExtCosts::utf8_decoding_base: 1, - ExtCosts::utf8_decoding_byte: bytes.len, - }); -} - -#[test] -fn test_valid_null_terminated_utf8() { - let mut logic_builder = VMLogicBuilder::default(); - - let cstring = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%\x00"; - let string = &cstring[..cstring.len() - 1]; - logic_builder.config.limit_config.max_total_log_length = string.len() as u64; - let mut logic = logic_builder.build(get_context(vec![], false)); - 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(); - assert_costs(map! { - ExtCosts::base: 1, - ExtCosts::log_base: 1, - ExtCosts::log_byte: string.len() as u64, - ExtCosts::read_memory_base: bytes.len, - ExtCosts::read_memory_byte: bytes.len, - ExtCosts::utf8_decoding_base: 1, - ExtCosts::utf8_decoding_byte: string.len() as u64, - }); - assert_eq!(outcome.logs[0], string); -} - -#[test] -fn test_log_max_limit() { - let mut logic_builder = VMLogicBuilder::default(); - let string = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%"; - let limit = string.len() as u64 - 1; - logic_builder.config.limit_config.max_total_log_length = limit; - let mut logic = logic_builder.build(get_context(vec![], false)); - let bytes = logic.internal_mem_write(string.as_bytes()); - - assert_eq!( - logic.log_utf8(bytes.len, bytes.ptr), - Err(HostError::TotalLogLengthExceeded { length: bytes.len, limit }.into()) - ); - - assert_costs(map! { - ExtCosts::base: 1, - ExtCosts::utf8_decoding_base: 1, - }); - - let outcome = logic.compute_outcome_and_distribute_gas(); - assert_eq!(outcome.logs.len(), 0); -} - -#[test] -fn test_log_total_length_limit() { - let mut logic_builder = VMLogicBuilder::default(); - let string = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%".as_bytes(); - let num_logs = 10; - let limit = string.len() as u64 * num_logs - 1; - logic_builder.config.limit_config.max_total_log_length = limit; - logic_builder.config.limit_config.max_number_logs = num_logs; - let mut logic = logic_builder.build(get_context(vec![], false)); - let bytes = logic.internal_mem_write(string); - - for _ in 0..num_logs - 1 { - logic.log_utf8(bytes.len, bytes.ptr).expect("total is still under the limit"); - } - assert_eq!( - logic.log_utf8(bytes.len, bytes.ptr), - Err(HostError::TotalLogLengthExceeded { length: limit + 1, limit }.into()) - ); - - let outcome = logic.compute_outcome_and_distribute_gas(); - assert_eq!(outcome.logs.len() as u64, num_logs - 1); -} - -#[test] -fn test_log_number_limit() { - let mut logic_builder = VMLogicBuilder::default(); - let string = "blabla"; - let max_number_logs = 3; - logic_builder.config.limit_config.max_total_log_length = - (string.len() + 1) as u64 * (max_number_logs + 1); - logic_builder.config.limit_config.max_number_logs = max_number_logs; - let mut logic = logic_builder.build(get_context(vec![], false)); - let bytes = logic.internal_mem_write(string.as_bytes()); - for _ in 0..max_number_logs { - logic - .log_utf8(bytes.len, bytes.ptr) - .expect("Valid utf-8 string_bytes under the log number limit"); - } - assert_eq!( - logic.log_utf8(bytes.len, bytes.ptr), - Err(HostError::NumberOfLogsExceeded { limit: max_number_logs }.into()) - ); - - assert_costs(map! { - ExtCosts::base: max_number_logs + 1, - ExtCosts::log_base: max_number_logs, - ExtCosts::log_byte: bytes.len * max_number_logs, - ExtCosts::read_memory_base: max_number_logs, - ExtCosts::read_memory_byte: bytes.len * max_number_logs, - ExtCosts::utf8_decoding_base: max_number_logs, - ExtCosts::utf8_decoding_byte: bytes.len * max_number_logs, - }); - - let outcome = logic.compute_outcome_and_distribute_gas(); - assert_eq!(outcome.logs.len() as u64, max_number_logs); -} - -fn append_utf16(dst: &mut Vec, string: &str) { - for code_unit in string.encode_utf16() { - dst.extend_from_slice(&code_unit.to_le_bytes()); - } -} - -#[test] -fn test_log_utf16_number_limit() { - let string = "$ qò$`"; - let mut bytes = Vec::new(); - append_utf16(&mut bytes, string); - - let mut logic_builder = VMLogicBuilder::default(); - let max_number_logs = 3; - logic_builder.config.limit_config.max_total_log_length = - (bytes.len() + 1) as u64 * (max_number_logs + 1); - logic_builder.config.limit_config.max_number_logs = max_number_logs; - - let mut logic = logic_builder.build(get_context(vec![], false)); - let bytes = logic.internal_mem_write(&bytes); - for _ in 0..max_number_logs { - logic - .log_utf16(bytes.len, bytes.ptr) - .expect("Valid utf-16 string_bytes under the log number limit"); - } - assert_eq!( - logic.log_utf16(bytes.len, bytes.ptr), - Err(HostError::NumberOfLogsExceeded { limit: max_number_logs }.into()) - ); - - assert_costs(map! { - ExtCosts::base: max_number_logs + 1, - ExtCosts::log_base: max_number_logs, - ExtCosts::log_byte: string.len() as u64 * max_number_logs, - ExtCosts::read_memory_base: max_number_logs, - ExtCosts::read_memory_byte: bytes.len * max_number_logs, - ExtCosts::utf16_decoding_base: max_number_logs, - ExtCosts::utf16_decoding_byte: bytes.len * max_number_logs, - }); - - let outcome = logic.compute_outcome_and_distribute_gas(); - assert_eq!(outcome.logs.len() as u64, max_number_logs); -} - -#[test] -fn test_log_total_length_limit_mixed() { - let mut logic_builder = VMLogicBuilder::default(); - - let string = "abc"; - let mut utf16_bytes: Vec = vec![0u8; 0]; - append_utf16(&mut utf16_bytes, string); - - let num_logs_each = 10; - let limit = string.len() as u64 * (num_logs_each * 2 + 1) - 1; - logic_builder.config.limit_config.max_total_log_length = limit; - logic_builder.config.limit_config.max_number_logs = num_logs_each * 2 + 1; - let mut logic = logic_builder.build(get_context(vec![], false)); - - let utf8_bytes = logic.internal_mem_write(string.as_bytes()); - let utf16_bytes = logic.internal_mem_write(&utf16_bytes); - - for _ in 0..num_logs_each { - logic.log_utf16(utf16_bytes.len, utf16_bytes.ptr).expect("total is still under the limit"); - - logic.log_utf8(utf8_bytes.len, utf8_bytes.ptr).expect("total is still under the limit"); - } - assert_eq!( - logic.log_utf8(utf8_bytes.len, utf8_bytes.ptr), - Err(HostError::TotalLogLengthExceeded { length: limit + 1, limit }.into()) - ); - - let outcome = logic.compute_outcome_and_distribute_gas(); - assert_eq!(outcome.logs.len() as u64, num_logs_each * 2); -} - -#[test] -fn test_log_utf8_max_limit_null_terminated() { - let mut logic_builder = VMLogicBuilder::default(); - let bytes = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%\x00".as_bytes(); - let limit = (bytes.len() - 2) as u64; - logic_builder.config.limit_config.max_total_log_length = limit; - let mut logic = logic_builder.build(get_context(vec![], false)); - let bytes = logic.internal_mem_write(bytes); - - assert_eq!( - logic.log_utf8(u64::MAX, bytes.ptr), - Err(HostError::TotalLogLengthExceeded { length: limit + 1, limit }.into()) - ); - - assert_costs(map! { - ExtCosts::base: 1, - ExtCosts::read_memory_base: bytes.len - 1, - ExtCosts::read_memory_byte: bytes.len - 1, - ExtCosts::utf8_decoding_base: 1, - }); - - let outcome = logic.compute_outcome_and_distribute_gas(); - assert_eq!(outcome.logs.len(), 0); -} - -#[test] -fn test_valid_log_utf16() { - let mut logic_builder = VMLogicBuilder::default(); - let mut logic = logic_builder.build(get_context(vec![], false)); - - let string = "$ qò$`"; - let mut bytes = Vec::new(); - append_utf16(&mut bytes, string); - let bytes = logic.internal_mem_write(&bytes); - - logic.log_utf16(bytes.len, bytes.ptr).expect("Valid utf-16 string_bytes"); - - assert_costs(map! { - ExtCosts::base: 1, - ExtCosts::read_memory_base: 1, - ExtCosts::read_memory_byte: bytes.len, - ExtCosts::utf16_decoding_base: 1, - ExtCosts::utf16_decoding_byte: bytes.len, - ExtCosts::log_base: 1, - ExtCosts::log_byte: string.len() as u64, - }); - let outcome = logic.compute_outcome_and_distribute_gas(); - assert_eq!(outcome.logs[0], string); -} - -#[test] -fn test_valid_log_utf16_max_log_len_not_even() { - let mut logic_builder = VMLogicBuilder::default(); - logic_builder.config.limit_config.max_total_log_length = 5; - let mut logic = logic_builder.build(get_context(vec![], false)); - - let string = "ab"; - let mut bytes = Vec::new(); - append_utf16(&mut bytes, string); - append_utf16(&mut bytes, "\0"); - let bytes = logic.internal_mem_write(&bytes); - logic.log_utf16(u64::MAX, bytes.ptr).expect("Valid utf-16 bytes"); - - assert_costs(map! { - ExtCosts::base: 1, - ExtCosts::read_memory_base: bytes.len / 2, - ExtCosts::read_memory_byte: bytes.len, - ExtCosts::utf16_decoding_base: 1, - ExtCosts::utf16_decoding_byte: bytes.len - 2, - ExtCosts::log_base: 1, - ExtCosts::log_byte: string.len() as u64, - }); - - let string = "abc"; - let mut bytes = Vec::new(); - append_utf16(&mut bytes, string); - append_utf16(&mut bytes, "\0"); - let bytes = logic.internal_mem_write(&bytes); - assert_eq!( - logic.log_utf16(u64::MAX, bytes.ptr), - Err(HostError::TotalLogLengthExceeded { length: 6, limit: 5 }.into()) - ); - - assert_costs(map! { - ExtCosts::base: 1, - ExtCosts::read_memory_base: 2, - ExtCosts::read_memory_byte: 2 * 2, - ExtCosts::utf16_decoding_base: 1, - }); -} - -#[test] -fn test_log_utf8_max_limit_null_terminated_fail() { - let mut logic_builder = VMLogicBuilder::default(); - logic_builder.config.limit_config.max_total_log_length = 3; - let mut logic = logic_builder.build(get_context(vec![], false)); - let bytes = logic.internal_mem_write(b"abcdefgh\0"); - let res = logic.log_utf8(u64::MAX, bytes.ptr); - assert_eq!(res, Err(HostError::TotalLogLengthExceeded { length: 4, limit: 3 }.into())); - assert_costs(map! { - ExtCosts::base: 1, - ExtCosts::read_memory_base: logic_builder.config.limit_config.max_total_log_length + 1, - ExtCosts::read_memory_byte: logic_builder.config.limit_config.max_total_log_length + 1, - ExtCosts::utf8_decoding_base: 1, - }); -} - -#[test] -fn test_valid_log_utf16_null_terminated() { - let mut logic_builder = VMLogicBuilder::default(); - let mut logic = logic_builder.build(get_context(vec![], false)); - - let string = "$ qò$`"; - let mut bytes = Vec::new(); - append_utf16(&mut bytes, string); - bytes.extend_from_slice(&[0, 0]); - let bytes = logic.internal_mem_write(&bytes); - - logic.log_utf16(u64::MAX, bytes.ptr).expect("Valid utf-16 string_bytes"); - - let outcome = logic.compute_outcome_and_distribute_gas(); - assert_eq!(outcome.logs[0], string); - assert_costs(map! { - ExtCosts::base: 1, - ExtCosts::read_memory_base: bytes.len / 2 , - ExtCosts::read_memory_byte: bytes.len, - ExtCosts::utf16_decoding_base: 1, - ExtCosts::utf16_decoding_byte: bytes.len - 2, - ExtCosts::log_base: 1, - ExtCosts::log_byte: string.len() as u64, - }); -} - -#[test] -fn test_invalid_log_utf16() { - let mut logic_builder = VMLogicBuilder::default(); - let mut logic = logic_builder.build(get_context(vec![], false)); - let mut bytes: Vec = Vec::new(); - for u16_ in [0xD834u16, 0xDD1E, 0x006d, 0x0075, 0xD800, 0x0069, 0x0063] { - bytes.extend_from_slice(&u16_.to_le_bytes()); - } - let bytes = logic.internal_mem_write(&bytes); - let res = logic.log_utf16(bytes.len, bytes.ptr); - assert_eq!(res, Err(HostError::BadUTF16.into())); - assert_costs(map! { - ExtCosts::base: 1, - ExtCosts::read_memory_base: 1, - ExtCosts::read_memory_byte: bytes.len, - ExtCosts::utf16_decoding_base: 1, - ExtCosts::utf16_decoding_byte: bytes.len, - }); -} - -#[test] -fn test_valid_log_utf16_null_terminated_fail() { - let mut logic_builder = VMLogicBuilder::default(); - let mut logic = logic_builder.build(get_context(vec![], false)); - - let mut bytes = Vec::new(); - append_utf16(&mut bytes, "$ qò$`"); - bytes.extend_from_slice(&[0x00, 0xD8]); // U+D800, unpaired surrogate - append_utf16(&mut bytes, "foobarbaz\0"); - let bytes = logic.internal_mem_write(&bytes); - - let res = logic.log_utf16(u64::MAX, bytes.ptr); - assert_eq!(res, Err(HostError::BadUTF16.into())); - assert_costs(map! { - ExtCosts::base: 1, - ExtCosts::read_memory_base: bytes.len / 2, - ExtCosts::read_memory_byte: bytes.len, - ExtCosts::utf16_decoding_base: 1, - ExtCosts::utf16_decoding_byte: bytes.len - 2, - }); -} - #[test] fn test_sha256() { let mut logic_builder = VMLogicBuilder::default(); diff --git a/runtime/near-vm-logic/src/tests/mod.rs b/runtime/near-vm-logic/src/tests/mod.rs index 7a9a28b3552..42e1c15c99f 100644 --- a/runtime/near-vm-logic/src/tests/mod.rs +++ b/runtime/near-vm-logic/src/tests/mod.rs @@ -6,6 +6,7 @@ mod fixtures; mod gas_counter; pub(crate) mod helpers; mod iterators; +mod logs; mod miscs; mod promises; mod registers; diff --git a/runtime/near-vm-logic/src/tests/vm_logic_builder.rs b/runtime/near-vm-logic/src/tests/vm_logic_builder.rs index 473fa540aad..576c49019ff 100644 --- a/runtime/near-vm-logic/src/tests/vm_logic_builder.rs +++ b/runtime/near-vm-logic/src/tests/vm_logic_builder.rs @@ -97,11 +97,15 @@ impl TestVMLogic<'_> { /// makes it convenient to populate the memory with various different data /// to later use in function calls. pub(super) fn internal_mem_write(&mut self, data: &[u8]) -> MemSlice { - let ptr = self.mem_write_offset; + let slice = self.internal_mem_write_at(self.mem_write_offset, data); + self.mem_write_offset += slice.len; + slice + } + + /// Writes data into guest memory at given location. + pub(super) fn internal_mem_write_at(&mut self, ptr: u64, data: &[u8]) -> MemSlice { self.memory().set_for_free(ptr, data).unwrap(); - let len = data.len() as u64; - self.mem_write_offset += len; - MemSlice { len, ptr } + MemSlice { len: u64::try_from(data.len()).unwrap(), ptr } } /// Reads data from guest memory into a Vector. From 8a38b29f7172cb629154c193c76ef8a67a69fa45 Mon Sep 17 00:00:00 2001 From: Simonas Kazlauskas Date: Tue, 17 Jan 2023 14:35:18 +0200 Subject: [PATCH 186/188] wasmtime: upgrade to 4.0 (#8368) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This bumps the wasmparser dep of wasmtime significantly. Since this isn’t actually protocol-facing in anyway this also seems quite a straightforward upgrade in terms of possible concerns that might arise. --- Cargo.lock | 334 ++++++++++++------ Cargo.toml | 2 +- deny.toml | 10 +- runtime/near-vm-runner/src/imports.rs | 34 +- runtime/near-vm-runner/src/wasmtime_runner.rs | 97 ++--- 5 files changed, 292 insertions(+), 185 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cacc8b57a48..ce5460ab25c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -343,6 +343,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "assert_matches" version = "1.5.0" @@ -487,9 +493,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.65" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" +checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" dependencies = [ "addr2line", "cc", @@ -573,7 +579,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.5.2", "cc", "cfg-if 0.1.10", "constant_time_eq", @@ -1154,23 +1160,27 @@ dependencies = [ [[package]] name = "cranelift-bforest" -version = "0.84.0" +version = "0.91.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fa7c3188913c2d11a361e0431e135742372a2709a99b103e79758e11a0a797e" +checksum = "fc952b310b24444fc14ab8b9cbe3fafd7e7329e3eec84c3a9b11d2b5cf6f3be1" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-codegen" -version = "0.84.0" +version = "0.91.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29285f70fd396a8f64455a15a6e1d390322e4a5f5186de513141313211b0a23e" +checksum = "e73470419b33011e50dbf0f6439cbccbaabe9381de172da4e1b6efcda4bb8fa7" dependencies = [ + "arrayvec 0.7.2", + "bumpalo", "cranelift-bforest", "cranelift-codegen-meta", "cranelift-codegen-shared", + "cranelift-egraph", "cranelift-entity", + "cranelift-isle", "gimli", "log", "regalloc2", @@ -1180,33 +1190,47 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.84.0" +version = "0.91.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057eac2f202ec95aebfd8d495e88560ac085f6a415b3c6c28529dc5eb116a141" +checksum = "911a1872464108a11ac9965c2b079e61bbdf1bc2e0b9001264264add2e12a38f" dependencies = [ "cranelift-codegen-shared", ] [[package]] name = "cranelift-codegen-shared" -version = "0.84.0" +version = "0.91.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75d93869efd18874a9341cfd8ad66bcb08164e86357a694a0e939d29e87410b9" +checksum = "e036f3f07adb24a86fb46e977e8fe03b18bb16b1eada949cf2c48283e5f8a862" + +[[package]] +name = "cranelift-egraph" +version = "0.91.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6c623f4b5d2a6bad32c403f03765d4484a827eb93ee78f8cb6219ef118fd59" +dependencies = [ + "cranelift-entity", + "fxhash", + "hashbrown 0.12.3", + "indexmap", + "log", + "smallvec", +] [[package]] name = "cranelift-entity" -version = "0.84.0" +version = "0.91.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e34bd7a1fefa902c90a921b36323f17a398b788fa56a75f07a29d83b6e28808" +checksum = "74385eb5e405b3562f0caa7bcc4ab9a93c7958dd5bcd0e910bffb7765eacd6fc" dependencies = [ "serde", ] [[package]] name = "cranelift-frontend" -version = "0.84.0" +version = "0.91.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "457018dd2d6ee300953978f63215b5edf3ae42dbdf8c7c038972f10394599f72" +checksum = "8a4ac920422ee36bff2c66257fec861765e3d95a125cdf58d8c0f3bba7e40e61" dependencies = [ "cranelift-codegen", "log", @@ -1214,11 +1238,17 @@ dependencies = [ "target-lexicon 0.12.3", ] +[[package]] +name = "cranelift-isle" +version = "0.91.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c541263fb37ad2baa53ec8c37218ee5d02fa0984670d9419dedd8002ea68ff08" + [[package]] name = "cranelift-native" -version = "0.84.0" +version = "0.91.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bba027cc41bf1d0eee2ddf16caba2ee1be682d0214520fff0129d2c6557fda89" +checksum = "1de5d7a063e8563d670aaca38de16591a9b70dc66cbad4d49a7b4ae8395fd1ce" dependencies = [ "cranelift-codegen", "libc", @@ -1227,9 +1257,9 @@ dependencies = [ [[package]] name = "cranelift-wasm" -version = "0.84.0" +version = "0.91.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b17639ced10b9916c9be120d38c872ea4f9888aa09248568b10056ef0559bfa" +checksum = "dfbc4dd03b713b5d71b582915b8c272f4813cdd8c99a3e03d9ba70c44468a6e0" dependencies = [ "cranelift-codegen", "cranelift-entity", @@ -1237,7 +1267,7 @@ dependencies = [ "itertools", "log", "smallvec", - "wasmparser 0.84.0", + "wasmparser 0.95.0", "wasmtime-types", ] @@ -1834,11 +1864,10 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", "percent-encoding", ] @@ -2084,9 +2113,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ "ahash", ] @@ -2279,6 +2308,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "im" version = "15.1.0" @@ -2315,7 +2354,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", - "hashbrown 0.12.1", + "hashbrown 0.12.3", "serde", ] @@ -2410,9 +2449,13 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "0.5.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec58677acfea8a15352d42fc87d11d63596ade9239e0a7c9352914417515dbe6" +checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e" +dependencies = [ + "libc", + "windows-sys 0.42.0", +] [[package]] name = "ipnet" @@ -2503,9 +2546,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.125" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "libfuzzer-sys" @@ -2574,9 +2617,9 @@ checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] name = "linux-raw-sys" -version = "0.0.42" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5284f00d480e1c39af34e72f8ad60b94f47007e3481cd3b731c1d67190ddc7b7" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" [[package]] name = "local-channel" @@ -2603,7 +2646,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" dependencies = [ "scopeguard", - "serde", ] [[package]] @@ -2614,6 +2656,7 @@ checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" dependencies = [ "autocfg", "scopeguard", + "serde", ] [[package]] @@ -2712,6 +2755,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memfd" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b20a59d985586e4a5aef64564ac77299f8586d8be6cf9106a5a40207e8908efb" +dependencies = [ + "rustix", +] + [[package]] name = "memmap" version = "0.7.0" @@ -2779,7 +2831,7 @@ dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -3979,12 +4031,12 @@ checksum = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a" [[package]] name = "object" -version = "0.28.4" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" dependencies = [ "crc32fast", - "hashbrown 0.11.2", + "hashbrown 0.12.3", "indexmap", "memchr", ] @@ -4117,7 +4169,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" dependencies = [ "dlv-list", - "hashbrown 0.12.1", + "hashbrown 0.12.3", ] [[package]] @@ -4157,7 +4209,7 @@ dependencies = [ "paperclip-actix", "paperclip-core", "paperclip-macros", - "parking_lot 0.10.2", + "parking_lot 0.12.1", "semver 1.0.9", "serde", "serde_derive", @@ -4179,7 +4231,7 @@ dependencies = [ "once_cell", "paperclip-core", "paperclip-macros", - "parking_lot 0.10.2", + "parking_lot 0.12.1", "serde_json", ] @@ -4193,7 +4245,7 @@ dependencies = [ "mime", "once_cell", "paperclip-macros", - "parking_lot 0.10.2", + "parking_lot 0.12.1", "pin-project", "regex", "serde", @@ -4276,7 +4328,7 @@ dependencies = [ "libc", "redox_syscall 0.2.13", "smallvec", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -4293,9 +4345,9 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "petgraph" @@ -4803,9 +4855,9 @@ dependencies = [ [[package]] name = "regalloc2" -version = "0.1.3" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904196c12c9f55d3aea578613219f493ced8e05b3d0c6a42d11cb4142d8b4879" +checksum = "300d4fbfb40c1c66a78ba3ddd41c1110247cf52f97b87d0f2fc9209bd49b030c" dependencies = [ "fxhash", "log", @@ -4839,18 +4891,6 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" -[[package]] -name = "region" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877e54ea2adcd70d80e9179344c97f93ef0dffd6b03e1f4529e6e83ab2fa9ae0" -dependencies = [ - "bitflags", - "libc", - "mach", - "winapi", -] - [[package]] name = "region" version = "3.0.0" @@ -4948,7 +4988,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "517a3034eb2b1499714e9d1e49b2367ad567e07639b69776d35e259d9c27cca6" dependencies = [ "bytecheck", - "hashbrown 0.12.1", + "hashbrown 0.12.3", "ptr_meta", "rend", "rkyv_derive", @@ -5153,16 +5193,16 @@ dependencies = [ [[package]] name = "rustix" -version = "0.33.7" +version = "0.36.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938a344304321a9da4973b9ff4f9f8db9caf4597dfd9dda6a60b523340a0fff0" +checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", - "winapi", + "windows-sys 0.42.0", ] [[package]] @@ -6307,13 +6347,12 @@ checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" [[package]] name = "url" -version = "2.2.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", - "idna", - "matches", + "idna 0.3.0", "percent-encoding", ] @@ -6323,7 +6362,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d6937c33ec6039d8071bcf72933146b5bbe378d645d8fa59bdadabfc2a249" dependencies = [ - "idna", + "idna 0.2.3", "lazy_static", "regex", "serde", @@ -6554,7 +6593,7 @@ dependencies = [ "cfg-if 1.0.0", "enumset", "leb128", - "region 3.0.0", + "region", "rkyv", "thiserror", "wasmer-compiler-near", @@ -6652,7 +6691,7 @@ dependencies = [ "libc", "memoffset", "more-asserts", - "region 3.0.0", + "region", "rkyv", "thiserror", "wasmer-types-near", @@ -6680,6 +6719,16 @@ dependencies = [ "indexmap", ] +[[package]] +name = "wasmparser" +version = "0.95.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ea896273ea99b15132414be1da01ab0d8836415083298ecaffbe308eaac87a" +dependencies = [ + "indexmap", + "url", +] + [[package]] name = "wasmprinter" version = "0.2.34" @@ -6692,38 +6741,44 @@ dependencies = [ [[package]] name = "wasmtime" -version = "0.37.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfdd1101bdfa0414a19018ec0a091951a20b695d4d04f858d49f6c4cc53cd8dd" +checksum = "4abddf11816dd8f5e7310f6ebe5a2503b43f20ab2bf050b7d63f5b1bb96a81d9" dependencies = [ "anyhow", - "backtrace", "bincode", "cfg-if 1.0.0", "indexmap", - "lazy_static", "libc", "log", "object", "once_cell", "paste", "psm", - "region 2.2.0", "serde", "target-lexicon 0.12.3", - "wasmparser 0.84.0", + "wasmparser 0.95.0", "wasmtime-cranelift", "wasmtime-environ", "wasmtime-jit", "wasmtime-runtime", - "winapi", + "windows-sys 0.42.0", +] + +[[package]] +name = "wasmtime-asm-macros" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1f5206486f0467ba86e84d35996c4048b077cec2c9e5b322e7b853bdbe79334" +dependencies = [ + "cfg-if 1.0.0", ] [[package]] name = "wasmtime-cranelift" -version = "0.37.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e78edcfb0daa9a9579ac379d00e2d5a5b2a60c0d653c8c95e8412f2166acb9" +checksum = "9e5bcb1d5ef211726b11e1286fe96cb40c69044c3632e1d6c67805d88a2e1a34" dependencies = [ "anyhow", "cranelift-codegen", @@ -6733,39 +6788,37 @@ dependencies = [ "cranelift-wasm", "gimli", "log", - "more-asserts", "object", "target-lexicon 0.12.3", "thiserror", - "wasmparser 0.84.0", + "wasmparser 0.95.0", "wasmtime-environ", ] [[package]] name = "wasmtime-environ" -version = "0.37.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4201389132ec467981980549574b33fc70d493b40f2c045c8ce5c7b54fbad97e" +checksum = "dcab3fac5a2ff68ce9857166a7d7c0e5251b554839b9dda7ed3b5528e191936e" dependencies = [ "anyhow", "cranelift-entity", "gimli", "indexmap", "log", - "more-asserts", "object", "serde", "target-lexicon 0.12.3", "thiserror", - "wasmparser 0.84.0", + "wasmparser 0.95.0", "wasmtime-types", ] [[package]] name = "wasmtime-jit" -version = "0.37.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1587ca7752d00862faa540d00fd28e5ccf1ac61ba19756449193f1153cb2b127" +checksum = "a7d866e2a84ee164739b7ed7bd7cc9e1f918639d2ec5e2817a31e24c148cab20" dependencies = [ "addr2line", "anyhow", @@ -6775,61 +6828,69 @@ dependencies = [ "gimli", "log", "object", - "region 2.2.0", "rustc-demangle", - "rustix", "serde", "target-lexicon 0.12.3", - "thiserror", "wasmtime-environ", + "wasmtime-jit-icache-coherence", "wasmtime-runtime", - "winapi", + "windows-sys 0.42.0", ] [[package]] name = "wasmtime-jit-debug" -version = "0.37.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b27233ab6c8934b23171c64f215f902ef19d18c1712b46a0674286d1ef28d5dd" +checksum = "0104c2b1ce443f2a2806216fcdf6dce09303203ec5797a698d313063b31e5bc8" dependencies = [ - "lazy_static", + "once_cell", +] + +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22d9c2e92b0fc124d2cad6cb497a4c840580a7dd2414a37109e8c7cfe699c0ea" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "windows-sys 0.42.0", ] [[package]] name = "wasmtime-runtime" -version = "0.37.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d3b0b8f13db47db59d616e498fe45295819d04a55f9921af29561827bdb816" +checksum = "0a1f0f99297a94cb20c511d1d4e864d9b54794644016d2530dc797cacfa7224a" dependencies = [ "anyhow", - "backtrace", "cc", "cfg-if 1.0.0", "indexmap", "libc", "log", "mach", + "memfd", "memoffset", - "more-asserts", + "paste", "rand 0.8.5", - "region 2.2.0", "rustix", - "thiserror", + "wasmtime-asm-macros", "wasmtime-environ", "wasmtime-jit-debug", - "winapi", + "windows-sys 0.42.0", ] [[package]] name = "wasmtime-types" -version = "0.37.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1630d9dca185299bec7f557a7e73b28742fe5590caf19df001422282a0a98ad1" +checksum = "62f3d8ee409447cae51651fd812437a0047ed8d7f44e94171ee05ce7cb955c96" dependencies = [ "cranelift-entity", "serde", "thiserror", - "wasmparser 0.84.0", + "wasmparser 0.95.0", ] [[package]] @@ -6916,43 +6977,100 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + [[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + [[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + [[package]] name = "winreg" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index 18551d08027..210c31198f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -200,7 +200,7 @@ wasm-encoder = "0.11.0" wasm-smith = "0.10" wasmparser = "0.78" wasmprinter = "0.2" -wasmtime = { version = "0.37.0", default-features = false, features = ["cranelift", "wasm-backtrace"] } +wasmtime = { version = "4.0.0", default-features = false, features = ["cranelift"] } wat = "1.0.40" xshell = "0.2.1" xz2 = "0.1.6" diff --git a/deny.toml b/deny.toml index 3a3a6156531..92c8d3f6e2d 100644 --- a/deny.toml +++ b/deny.toml @@ -36,8 +36,6 @@ skip = [ # near-vm-runner and wasmer-compiler-near use 0.78.2 { name = "wasmparser", version = "=0.78.2" }, - # Wasmer 2.0 uses both region 2.2.0 and 3.0.0 via dependencies - { name = "region", version = "=2.2.0" }, # Need this specific version of pwasm-utils for backwards-compatible # stack limiting. { name = "pwasm-utils", version = "=0.12.0" }, @@ -45,6 +43,7 @@ skip = [ # wasmer and wasmtime { name = "target-lexicon", version = "=0.10.0" }, + { name = "wasmparser", version = "=0.84.0" }, # chain and param estimator { name = "num-rational", version = "=0.3.2" }, @@ -55,6 +54,10 @@ skip = [ { name = "lock_api", version = "=0.3.4" }, { name = "digest", version = "=0.8.1" }, + # old version of tokio, parking_lot + { name = "windows-sys", version = "=0.36.1" }, + { name = "windows_x86_64_msvc", version = "=0.36.1" }, + # chrono uses old time crate { name = "time", version = "=0.1.44" }, @@ -98,4 +101,7 @@ skip = [ # rust-s3 is using an old version of smartstring { name = "smartstring", version = "=0.2.10" }, + + # validator 0.12 ~ 0.16 is still using an old version of idna + { name = "idna", version = "=0.2.3" }, ] diff --git a/runtime/near-vm-runner/src/imports.rs b/runtime/near-vm-runner/src/imports.rs index 58589a8da23..2136be895e9 100644 --- a/runtime/near-vm-runner/src/imports.rs +++ b/runtime/near-vm-runner/src/imports.rs @@ -463,12 +463,29 @@ pub(crate) mod wasmer2 { pub(crate) mod wasmtime { use super::str_eq; use near_vm_logic::{ProtocolVersion, VMLogic, VMLogicError}; - use std::cell::{RefCell, UnsafeCell}; + use std::cell::UnsafeCell; use std::ffi::c_void; + /// This is a container from which an error can be taken out by value. This is necessary as + /// `anyhow` does not really give any opportunity to grab causes by value and the VM Logic + /// errors end up a couple layers deep in a causal chain. + #[derive(Debug)] + pub(crate) struct ErrorContainer(std::sync::Mutex>); + impl ErrorContainer { + pub(crate) fn take(&self) -> Option { + let mut guard = self.0.lock().unwrap_or_else(|e| e.into_inner()); + guard.take() + } + } + impl std::error::Error for ErrorContainer {} + impl std::fmt::Display for ErrorContainer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("VMLogic error occurred and is now stored in an opaque storage container") + } + } + thread_local! { static CALLER_CONTEXT: UnsafeCell<*mut c_void> = UnsafeCell::new(0 as *mut c_void); - static EMBEDDER_ERROR: RefCell> = RefCell::new(None); } pub(crate) fn link( @@ -485,7 +502,7 @@ pub(crate) mod wasmtime { $func:ident < [ $( $arg_name:ident : $arg_type:ident ),* ] -> [ $( $returns:ident ),* ] > ) => { #[allow(unused_parens)] - fn $func(caller: wasmtime::Caller<'_, ()>, $( $arg_name: $arg_type ),* ) -> Result<($( $returns ),*), wasmtime::Trap> { + fn $func(caller: wasmtime::Caller<'_, ()>, $( $arg_name: $arg_type ),* ) -> anyhow::Result<($( $returns ),*)> { const IS_GAS: bool = str_eq(stringify!($func), "gas"); let _span = if IS_GAS { None @@ -508,12 +525,7 @@ pub(crate) mod wasmtime { match logic.$func( $( $arg_name as $arg_type, )* ) { Ok(result) => Ok(result as ($( $returns ),* ) ), Err(err) => { - // Wasmtime doesn't have proper mechanism for wrapping custom errors - // into traps. So, just store error into TLS and use special exit code here. - EMBEDDER_ERROR.with(|embedder_error| { - *embedder_error.borrow_mut() = Some(err) - }); - Err(wasmtime::Trap::i32_exit(239)) + Err(ErrorContainer(std::sync::Mutex::new(Some(err))).into()) } } } @@ -523,10 +535,6 @@ pub(crate) mod wasmtime { } for_each_available_import!(protocol_version, add_import); } - - pub(crate) fn last_error() -> Option { - EMBEDDER_ERROR.with(|embedder_error| embedder_error.replace(None)) - } } /// Constant-time string equality, work-around for `"foo" == "bar"` not working diff --git a/runtime/near-vm-runner/src/wasmtime_runner.rs b/runtime/near-vm-runner/src/wasmtime_runner.rs index 029314b529a..d21bd6fe8fc 100644 --- a/runtime/near-vm-runner/src/wasmtime_runner.rs +++ b/runtime/near-vm-runner/src/wasmtime_runner.rs @@ -16,7 +16,7 @@ use std::borrow::Cow; use std::cell::RefCell; use std::ffi::c_void; use wasmtime::ExternType::Func; -use wasmtime::{Engine, Linker, Memory, MemoryType, Module, Store, TrapCode}; +use wasmtime::{Engine, Linker, Memory, MemoryType, Module, Store}; type Caller = wasmtime::Caller<'static, ()>; thread_local! { @@ -79,68 +79,43 @@ impl MemoryLike for WasmtimeMemory { } } -fn trap_to_error(trap: &wasmtime::Trap) -> Result { - if trap.i32_exit_status() == Some(239) { - match imports::wasmtime::last_error() { - Some(VMLogicError::HostError(h)) => Ok(FunctionCallError::HostError(h)), - Some(VMLogicError::ExternalError(s)) => Err(VMRunnerError::ExternalError(s)), - Some(VMLogicError::InconsistentStateError(e)) => { - Err(VMRunnerError::InconsistentStateError(e)) - } - None => panic!("Error is not properly set"), - } - } else { - Ok(match trap.trap_code() { - Some(TrapCode::StackOverflow) => FunctionCallError::WasmTrap(WasmTrap::StackOverflow), - Some(TrapCode::MemoryOutOfBounds) => { - FunctionCallError::WasmTrap(WasmTrap::MemoryOutOfBounds) - } - Some(TrapCode::TableOutOfBounds) => { - FunctionCallError::WasmTrap(WasmTrap::MemoryOutOfBounds) - } - Some(TrapCode::IndirectCallToNull) => { - FunctionCallError::WasmTrap(WasmTrap::IndirectCallToNull) - } - Some(TrapCode::BadSignature) => { - FunctionCallError::WasmTrap(WasmTrap::IncorrectCallIndirectSignature) - } - Some(TrapCode::IntegerOverflow) => { - FunctionCallError::WasmTrap(WasmTrap::IllegalArithmetic) - } - Some(TrapCode::IntegerDivisionByZero) => { - FunctionCallError::WasmTrap(WasmTrap::IllegalArithmetic) - } - Some(TrapCode::BadConversionToInteger) => { - FunctionCallError::WasmTrap(WasmTrap::IllegalArithmetic) - } - Some(TrapCode::UnreachableCodeReached) => { - FunctionCallError::WasmTrap(WasmTrap::Unreachable) - } - Some(TrapCode::Interrupt) => { - return Err(VMRunnerError::Nondeterministic("interrupt".to_string())); - } - _ => { - return Err(VMRunnerError::WasmUnknownError { - debug_message: "unknown trap".to_string(), - }); - } - }) - } -} - impl IntoVMError for anyhow::Error { fn into_vm_error(self) -> Result { let cause = self.root_cause(); - match cause.downcast_ref::() { - Some(trap) => trap_to_error(trap), - None => Ok(FunctionCallError::LinkError { msg: format!("{:#?}", cause) }), + if let Some(container) = cause.downcast_ref::() { + use {VMLogicError as LE, VMRunnerError as RE}; + return match container.take() { + Some(LE::HostError(h)) => Ok(FunctionCallError::HostError(h)), + Some(LE::ExternalError(s)) => Err(RE::ExternalError(s)), + Some(LE::InconsistentStateError(e)) => Err(RE::InconsistentStateError(e)), + None => panic!("error has already been taken out of the container?!"), + }; } - } -} - -impl IntoVMError for wasmtime::Trap { - fn into_vm_error(self) -> Result { - trap_to_error(&self) + if let Some(trap) = cause.downcast_ref::() { + use wasmtime::Trap as T; + let nondeterministic_message = 'nondet: { + return Ok(FunctionCallError::WasmTrap(match *trap { + T::StackOverflow => WasmTrap::StackOverflow, + T::MemoryOutOfBounds => WasmTrap::MemoryOutOfBounds, + T::TableOutOfBounds => WasmTrap::MemoryOutOfBounds, + T::IndirectCallToNull => WasmTrap::IndirectCallToNull, + T::BadSignature => WasmTrap::IncorrectCallIndirectSignature, + T::IntegerOverflow => WasmTrap::IllegalArithmetic, + T::IntegerDivisionByZero => WasmTrap::IllegalArithmetic, + T::BadConversionToInteger => WasmTrap::IllegalArithmetic, + T::UnreachableCodeReached => WasmTrap::Unreachable, + T::Interrupt => break 'nondet "interrupt", + T::HeapMisaligned => break 'nondet "heap misaligned", + t => { + return Err(VMRunnerError::WasmUnknownError { + debug_message: format!("unhandled trap type: {:?}", t), + }) + } + })); + }; + return Err(VMRunnerError::Nondeterministic(nondeterministic_message.into())); + } + Ok(FunctionCallError::LinkError { msg: format!("{:#?}", cause) }) } } @@ -156,7 +131,7 @@ pub fn get_engine(config: &mut wasmtime::Config) -> Engine { pub(super) fn default_config() -> wasmtime::Config { let mut config = wasmtime::Config::default(); - config.max_wasm_stack(1024 * 1024 * 1024).unwrap(); // wasm stack metering is implemented by pwasm-utils, we don't want wasmtime to trap before that + config.max_wasm_stack(1024 * 1024 * 1024); // wasm stack metering is implemented by instrumentation, we don't want wasmtime to trap before that config.wasm_threads(WASM_FEATURES.threads); config.wasm_reference_types(WASM_FEATURES.reference_types); config.wasm_simd(WASM_FEATURES.simd); @@ -277,7 +252,7 @@ impl crate::runner::VM for WasmtimeVM { } match linker.instantiate(&mut store, &module) { Ok(instance) => match instance.get_func(&mut store, method_name) { - Some(func) => match func.typed::<(), (), _>(&mut store) { + Some(func) => match func.typed::<(), ()>(&mut store) { Ok(run) => match run.call(&mut store, ()) { Ok(_) => Ok(VMOutcome::ok(logic)), Err(err) => Ok(VMOutcome::abort(logic, err.into_vm_error()?)), From c578db6a3ddcff09d9798ca1e19481da2259bde5 Mon Sep 17 00:00:00 2001 From: mzhangmzz <34969888+mzhangmzz@users.noreply.github.com> Date: Tue, 17 Jan 2023 10:54:10 -0500 Subject: [PATCH 187/188] don't ban peers if the block timestamp is in the future (#8327) --- chain/client/src/client.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/chain/client/src/client.rs b/chain/client/src/client.rs index fe77842cc8c..9ac095da75a 100644 --- a/chain/client/src/client.rs +++ b/chain/client/src/client.rs @@ -1072,7 +1072,12 @@ impl Client { Ok(()) } Err(e) if e.is_bad_data() => { - self.ban_peer(peer_id.clone(), ReasonForBan::BadBlockHeader); + // We don't ban a peer if the block timestamp is too much in the future since it's possible + // that a block is considered valid in one machine and invalid in another machine when their + // clocks are not synced. + if !matches!(e, near_chain::Error::InvalidBlockFutureTime(_)) { + self.ban_peer(peer_id.clone(), ReasonForBan::BadBlockHeader); + } Err(e) } Err(_) => { From dce92b8f2c35bd09fcff67fc2c0675e35fe97c8b Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Tue, 17 Jan 2023 18:23:31 +0100 Subject: [PATCH 188/188] feat(state-viewer): list accounts with contracts (#8371) A state viewer command to list all accounts with contracts deployed. So far it only shows the account names and the wasm size. The intention is to extend it for more sophisticated filtering. So far, the command finishes all four shards in 2 minutes on a mainnet archival RocksDB. --- Cargo.lock | 1 + tools/state-viewer/Cargo.toml | 1 + tools/state-viewer/src/cli.rs | 16 ++ tools/state-viewer/src/commands.rs | 33 ++++ tools/state-viewer/src/contract_accounts.rs | 163 ++++++++++++++++++++ tools/state-viewer/src/lib.rs | 1 + 6 files changed, 215 insertions(+) create mode 100644 tools/state-viewer/src/contract_accounts.rs diff --git a/Cargo.lock b/Cargo.lock index ce5460ab25c..1cd4d4235b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5679,6 +5679,7 @@ dependencies = [ "serde_json", "tempfile", "testlib", + "thiserror", "tracing", ] diff --git a/tools/state-viewer/Cargo.toml b/tools/state-viewer/Cargo.toml index 4b7abff486a..4429bd47b12 100644 --- a/tools/state-viewer/Cargo.toml +++ b/tools/state-viewer/Cargo.toml @@ -18,6 +18,7 @@ rust-s3.workspace = true serde.workspace = true serde_json.workspace = true tempfile.workspace = true +thiserror.workspace = true tracing.workspace = true near-chain = { path = "../../chain/chain" } diff --git a/tools/state-viewer/src/cli.rs b/tools/state-viewer/src/cli.rs index 5e765c1422d..6cf6256db2a 100644 --- a/tools/state-viewer/src/cli.rs +++ b/tools/state-viewer/src/cli.rs @@ -39,6 +39,9 @@ pub enum StateViewerSubCommand { CheckBlock, /// Looks up a certain chunk. Chunks(ChunksCmd), + /// List account names with contracts deployed. + #[clap(alias = "contract_accounts")] + ContractAccounts(ContractAccountsCmd), /// Dump contract data in storage of given account to binary file. #[clap(alias = "dump_account_storage")] DumpAccountStorage(DumpAccountStorageCmd), @@ -113,6 +116,7 @@ impl StateViewerSubCommand { StateViewerSubCommand::Chain(cmd) => cmd.run(home_dir, near_config, store), StateViewerSubCommand::CheckBlock => check_block_chunk_existence(near_config, store), StateViewerSubCommand::Chunks(cmd) => cmd.run(near_config, store), + StateViewerSubCommand::ContractAccounts(cmd) => cmd.run(home_dir, near_config, store), StateViewerSubCommand::DumpAccountStorage(cmd) => cmd.run(home_dir, near_config, store), StateViewerSubCommand::DumpCode(cmd) => cmd.run(home_dir, near_config, store), StateViewerSubCommand::DumpState(cmd) => cmd.run(home_dir, near_config, store), @@ -260,6 +264,18 @@ impl ChunksCmd { } } +#[derive(Parser)] +pub struct ContractAccountsCmd { + // TODO: add filter options, e.g. only contracts that execute certain + // actions +} + +impl ContractAccountsCmd { + pub fn run(self, home_dir: &Path, near_config: NearConfig, store: Store) { + contract_accounts(home_dir, store, near_config).unwrap(); + } +} + #[derive(Parser)] pub struct DumpAccountStorageCmd { #[clap(long)] diff --git a/tools/state-viewer/src/commands.rs b/tools/state-viewer/src/commands.rs index 9b8b53264ae..4f285f3f934 100644 --- a/tools/state-viewer/src/commands.rs +++ b/tools/state-viewer/src/commands.rs @@ -1,4 +1,5 @@ use crate::apply_chain_range::apply_chain_range; +use crate::contract_accounts::ContractAccount; use crate::state_dump::state_dump; use crate::state_dump::state_dump_redis; use crate::tx_dump::dump_tx_from_block; @@ -14,6 +15,7 @@ use near_network::iter_peers_from_store; use near_primitives::account::id::AccountId; use near_primitives::block::{Block, BlockHeader}; use near_primitives::hash::CryptoHash; +use near_primitives::shard_layout::ShardLayout; use near_primitives::shard_layout::ShardUId; use near_primitives::sharding::ChunkHash; use near_primitives::state_record::StateRecord; @@ -22,6 +24,7 @@ use near_primitives::types::{chunk_extra::ChunkExtra, BlockHeight, ShardId, Stat use near_primitives_core::types::Gas; use near_store::db::Database; use near_store::test_utils::create_test_store; +use near_store::TrieDBStorage; use near_store::{Store, Trie, TrieCache, TrieCachingStorage, TrieConfig}; use nearcore::{NearConfig, NightshadeRuntime}; use node_runtime::adapter::ViewRuntimeAdapter; @@ -841,3 +844,33 @@ fn format_hash(h: CryptoHash, show_full_hashes: bool) -> String { pub fn chunk_mask_to_str(mask: &[bool]) -> String { mask.iter().map(|f| if *f { '.' } else { 'X' }).collect() } + +pub(crate) fn contract_accounts( + home_dir: &Path, + store: Store, + near_config: NearConfig, +) -> anyhow::Result<()> { + let (_runtime, state_roots, _header) = load_trie(store.clone(), home_dir, &near_config); + + for (shard_id, &state_root) in state_roots.iter().enumerate() { + eprintln!("Starting shard {shard_id}"); + // TODO: This assumes simple nightshade layout, it will need an update when we reshard. + let shard_uid = ShardUId::from_shard_id_and_layout( + shard_id as u64, + &ShardLayout::get_simple_nightshade_layout(), + ); + // Use simple non-caching storage, we don't expect many duplicate lookups while iterating. + let storage = TrieDBStorage::new(store.clone(), shard_uid); + // We don't need flat state to traverse all accounts. + let flat_state = None; + let trie = Trie::new(Box::new(storage), state_root, flat_state); + + for contract in ContractAccount::in_trie(&trie)? { + match contract { + Ok(contract) => println!("{contract}"), + Err(err) => eprintln!("{err}"), + } + } + } + Ok(()) +} diff --git a/tools/state-viewer/src/contract_accounts.rs b/tools/state-viewer/src/contract_accounts.rs new file mode 100644 index 00000000000..5260a1aa17c --- /dev/null +++ b/tools/state-viewer/src/contract_accounts.rs @@ -0,0 +1,163 @@ +//! State viewer functions to list and filter accounts that have contracts +//! deployed. + +use near_primitives::hash::CryptoHash; +use near_primitives::trie_key::trie_key_parsers::parse_account_id_from_contract_code_key; +use near_primitives::trie_key::TrieKey; +use near_primitives::types::AccountId; +use near_store::{NibbleSlice, StorageError, Trie, TrieTraversalItem}; +use std::collections::VecDeque; +use std::sync::Arc; + +/// Output type for contract account queries with all relevant data around a +/// single contract. +pub(crate) struct ContractAccount { + pub(crate) account_id: AccountId, + pub(crate) source_wasm: Arc<[u8]>, +} + +#[derive(Debug, thiserror::Error)] +pub enum ContractAccountError { + #[error("could not parse key {1:?}")] + InvalidKey(#[source] std::io::Error, Vec), + #[error("failed loading contract code for account {1}")] + NoCode(#[source] StorageError, AccountId), +} + +impl std::fmt::Display for ContractAccount { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:<64} {:>9}", self.account_id, self.source_wasm.len()) + } +} + +impl ContractAccount { + /// Iterate over all contracts stored in the given trie, in lexicographic + /// order of the account IDs. + pub(crate) fn in_trie(trie: &Trie) -> anyhow::Result { + ContractAccountIterator::new(trie) + } + + fn from_contract_trie_node( + trie_key: &[u8], + value_hash: CryptoHash, + trie: &Trie, + ) -> Result { + let account_id = parse_account_id_from_contract_code_key(trie_key) + .map_err(|err| ContractAccountError::InvalidKey(err, trie_key.to_vec()))?; + let source_wasm = trie + .storage + .retrieve_raw_bytes(&value_hash) + .map_err(|err| ContractAccountError::NoCode(err, account_id.clone()))?; + Ok(Self { account_id, source_wasm }) + } +} + +pub(crate) struct ContractAccountIterator<'a> { + /// Trie nodes that point to the contracts. + contract_nodes: VecDeque, + trie: &'a Trie, +} + +impl<'a> ContractAccountIterator<'a> { + pub(crate) fn new(trie: &'a Trie) -> anyhow::Result { + let mut trie_iter = trie.iter()?; + // TODO(#8376): Consider changing the interface to TrieKey to make this easier. + // `TrieKey::ContractCode` requires a valid `AccountId`, we use "xx" + let key = TrieKey::ContractCode { account_id: "xx".parse()? }.to_vec(); + let (prefix, suffix) = key.split_at(key.len() - 2); + assert_eq!(suffix, "xx".as_bytes()); + + // `visit_nodes_interval` wants nibbles stored in `Vec` as input + let nibbles_before: Vec = NibbleSlice::new(prefix).iter().collect(); + let nibbles_after = { + let mut tmp = nibbles_before.clone(); + *tmp.last_mut().unwrap() += 1; + tmp + }; + + // finally, use trie iterator to find all contract nodes + let vec_of_nodes = trie_iter.visit_nodes_interval(&nibbles_before, &nibbles_after)?; + let contract_nodes = VecDeque::from(vec_of_nodes); + Ok(Self { contract_nodes, trie }) + } +} + +impl Iterator for ContractAccountIterator<'_> { + type Item = Result; + + fn next(&mut self) -> Option { + while let Some(item) = self.contract_nodes.pop_front() { + // only look at nodes with a value, ignoring intermediate nodes + // without values + if let TrieTraversalItem { hash, key: Some(trie_key) } = item { + let contract = ContractAccount::from_contract_trie_node(&trie_key, hash, self.trie); + return Some(contract); + } + } + None + } +} + +#[cfg(test)] +mod tests { + use super::ContractAccount; + use near_primitives::trie_key::TrieKey; + use near_store::test_utils::{create_tries, test_populate_trie}; + use near_store::{ShardUId, Trie}; + + #[test] + fn test_three_contracts() { + let tries = create_tries(); + let initial = vec![ + contract_tuple("caroline.near", 3), + contract_tuple("alice.near", 1), + contract_tuple("alice.nearx", 2), + // data right before contracts in trie order + account_tuple("xeno.near", 1), + // data right after contracts in trie order + access_key_tuple("alan.near", 1), + ]; + let root = test_populate_trie(&tries, &Trie::EMPTY_ROOT, ShardUId::single_shard(), initial); + let trie = tries.get_trie_for_shard(ShardUId::single_shard(), root); + + let contract_accounts: Vec<_> = + ContractAccount::in_trie(&trie).expect("failed creating iterator").collect(); + assert_eq!(3, contract_accounts.len(), "wrong number of contracts returned by iterator"); + + // expect reordering toe lexicographic order + let contract1 = contract_accounts[0].as_ref().expect("returned error instead of contract"); + let contract2 = contract_accounts[1].as_ref().expect("returned error instead of contract"); + let contract3 = contract_accounts[2].as_ref().expect("returned error instead of contract"); + assert_eq!(contract1.account_id.as_str(), "alice.near"); + assert_eq!(contract2.account_id.as_str(), "alice.nearx"); + assert_eq!(contract3.account_id.as_str(), "caroline.near"); + assert_eq!(&*contract1.source_wasm, &[1u8, 1, 1]); + assert_eq!(&*contract2.source_wasm, &[2u8, 2, 2]); + assert_eq!(&*contract3.source_wasm, &[3u8, 3, 3]); + } + + /// Create a test contract key-value pair to insert in the test trie. + fn contract_tuple(account: &str, num: u8) -> (Vec, Option>) { + ( + TrieKey::ContractCode { account_id: account.parse().unwrap() }.to_vec(), + Some(vec![num, num, num]), + ) + } + + /// Create a test account key-value pair to insert in the test trie. + fn account_tuple(account: &str, num: u8) -> (Vec, Option>) { + (TrieKey::Account { account_id: account.parse().unwrap() }.to_vec(), Some(vec![num, num])) + } + + /// Create a test access key key-value pair to insert in the test trie. + fn access_key_tuple(account: &str, num: u8) -> (Vec, Option>) { + ( + TrieKey::AccessKey { + account_id: account.parse().unwrap(), + public_key: near_crypto::PublicKey::empty(near_crypto::KeyType::ED25519), + } + .to_vec(), + Some(vec![num, num, num, num]), + ) + } +} diff --git a/tools/state-viewer/src/lib.rs b/tools/state-viewer/src/lib.rs index 555c65b8ff3..73fea0ab710 100644 --- a/tools/state-viewer/src/lib.rs +++ b/tools/state-viewer/src/lib.rs @@ -4,6 +4,7 @@ mod apply_chain_range; mod apply_chunk; pub mod cli; mod commands; +mod contract_accounts; mod dump_state_parts; mod epoch_info; mod rocksdb_stats;