diff --git a/node/src/components/block_accumulator/tests.rs b/node/src/components/block_accumulator/tests.rs index 10c31857e6..9424605a6b 100644 --- a/node/src/components/block_accumulator/tests.rs +++ b/node/src/components/block_accumulator/tests.rs @@ -161,7 +161,7 @@ impl Reactor for MockReactor { _event_queue: EventQueueHandle, _rng: &mut NodeRng, ) -> Result<(Self, Effects), Self::Error> { - let (storage_config, storage_tempdir) = storage::Config::default_for_tests(); + let (storage_config, storage_tempdir) = storage::Config::new_for_tests(1); let storage_withdir = WithDir::new(storage_tempdir.path(), storage_config); let validator_matrix = ValidatorMatrix::new_with_validator(ALICE_SECRET_KEY.clone()); let block_accumulator_config = Config::default(); diff --git a/node/src/components/contract_runtime/tests.rs b/node/src/components/contract_runtime/tests.rs index 27a3925547..5c34ef2d29 100644 --- a/node/src/components/contract_runtime/tests.rs +++ b/node/src/components/contract_runtime/tests.rs @@ -94,7 +94,7 @@ impl reactor::Reactor for Reactor { _event_queue: EventQueueHandle, _rng: &mut NodeRng, ) -> Result<(Self, Effects), Self::Error> { - let (storage_config, storage_tempdir) = storage::Config::default_for_tests(); + let (storage_config, storage_tempdir) = storage::Config::new_for_tests(1); let storage_withdir = WithDir::new(storage_tempdir.path(), storage_config); let storage = Storage::new( &storage_withdir, diff --git a/node/src/components/fetcher/tests.rs b/node/src/components/fetcher/tests.rs index 39f171c339..7c7353858d 100644 --- a/node/src/components/fetcher/tests.rs +++ b/node/src/components/fetcher/tests.rs @@ -71,7 +71,7 @@ pub struct FetcherTestConfig { impl Default for FetcherTestConfig { fn default() -> Self { - let (storage_config, temp_dir) = storage::Config::default_for_tests(); + let (storage_config, temp_dir) = storage::Config::new_for_tests(1); FetcherTestConfig { fetcher_config: Default::default(), storage_config, diff --git a/node/src/components/gossiper/tests.rs b/node/src/components/gossiper/tests.rs index bb96ab6173..aba59f6ec7 100644 --- a/node/src/components/gossiper/tests.rs +++ b/node/src/components/gossiper/tests.rs @@ -157,7 +157,7 @@ impl reactor::Reactor for Reactor { event_queue: EventQueueHandle, rng: &mut NodeRng, ) -> Result<(Self, Effects), Self::Error> { - let (storage_config, storage_tempdir) = storage::Config::default_for_tests(); + let (storage_config, storage_tempdir) = storage::Config::new_for_tests(1); let storage_withdir = WithDir::new(storage_tempdir.path(), storage_config); let storage = Storage::new( &storage_withdir, diff --git a/node/src/components/storage/config.rs b/node/src/components/storage/config.rs index 91af7eaf24..75573d4a58 100644 --- a/node/src/components/storage/config.rs +++ b/node/src/components/storage/config.rs @@ -57,15 +57,24 @@ impl Default for Config { } impl Config { - /// Returns a default `Config` suitable for tests, along with a `TempDir` which must be kept - /// alive for the duration of the test since its destructor removes the dir from the filesystem. + /// Returns a `Config` suitable for tests, along with a `TempDir` which must be kept alive for + /// the duration of the test since its destructor removes the dir from the filesystem. + /// + /// `size_multiplier` is used to multiply the default DB sizes. #[cfg(test)] - pub(crate) fn default_for_tests() -> (Self, TempDir) { + pub(crate) fn new_for_tests(size_multiplier: u8) -> (Self, TempDir) { + if size_multiplier == 0 { + panic!("size_multiplier cannot be zero"); + } let tempdir = tempfile::tempdir().expect("should get tempdir"); let path = tempdir.path().join("lmdb"); let config = Config { path, + max_block_store_size: 1024 * 1024 * size_multiplier as usize, + max_deploy_store_size: 1024 * 1024 * size_multiplier as usize, + max_deploy_metadata_store_size: 1024 * 1024 * size_multiplier as usize, + max_state_store_size: 12 * 1024 * size_multiplier as usize, ..Default::default() }; (config, tempdir) diff --git a/node/src/components/transaction_acceptor/tests.rs b/node/src/components/transaction_acceptor/tests.rs index ec9dc0e41c..c30ee1c35f 100644 --- a/node/src/components/transaction_acceptor/tests.rs +++ b/node/src/components/transaction_acceptor/tests.rs @@ -661,7 +661,7 @@ impl reactor::Reactor for Reactor { _event_queue: EventQueueHandle, _rng: &mut NodeRng, ) -> Result<(Self, Effects), Self::Error> { - let (storage_config, storage_tempdir) = storage::Config::default_for_tests(); + let (storage_config, storage_tempdir) = storage::Config::new_for_tests(1); let storage_withdir = WithDir::new(storage_tempdir.path(), storage_config); let transaction_acceptor = diff --git a/node/src/failpoints_disabled.rs b/node/src/failpoints_disabled.rs index bece519c25..e7810dd92e 100644 --- a/node/src/failpoints_disabled.rs +++ b/node/src/failpoints_disabled.rs @@ -53,6 +53,11 @@ impl Failpoint { pub(crate) struct FailpointActivation; impl FailpointActivation { + #[allow(dead_code)] + pub(crate) fn new(_key: S) -> FailpointActivation { + FailpointActivation + } + pub(crate) fn key(&self) -> &str { "" } diff --git a/node/src/reactor/main_reactor.rs b/node/src/reactor/main_reactor.rs index f8350c4c8f..cba6d43c7c 100644 --- a/node/src/reactor/main_reactor.rs +++ b/node/src/reactor/main_reactor.rs @@ -194,6 +194,8 @@ pub(crate) struct MainReactor { upgrade_timeout: TimeDiff, sync_handling: SyncHandling, signature_gossip_tracker: SignatureGossipTracker, + + finality_signature_creation: bool, } impl reactor::Reactor for MainReactor { @@ -242,9 +244,13 @@ impl reactor::Reactor for MainReactor { MainEvent::MainReactorRequest(req) => { req.0.respond((self.state, self.last_progress)).ignore() } - MainEvent::MetaBlockAnnouncement(MetaBlockAnnouncement(meta_block)) => { - self.handle_meta_block(effect_builder, rng, meta_block) - } + MainEvent::MetaBlockAnnouncement(MetaBlockAnnouncement(meta_block)) => self + .handle_meta_block( + effect_builder, + rng, + self.finality_signature_creation, + meta_block, + ), MainEvent::UnexecutedBlockAnnouncement(UnexecutedBlockAnnouncement(block_height)) => { let only_from_available_block_range = true; if let Ok(Some(block_header)) = self @@ -1209,6 +1215,7 @@ impl reactor::Reactor for MainReactor { shutdown_for_upgrade_timeout: config.node.shutdown_for_upgrade_timeout, switched_to_shutdown_for_upgrade: Timestamp::from(0), upgrade_timeout: config.node.upgrade_timeout, + finality_signature_creation: true, }; info!("MainReactor: instantiated"); let effects = effect_builder @@ -1230,6 +1237,9 @@ impl reactor::Reactor for MainReactor { activation, ); } + if activation.key().starts_with("finality_signature_creation") { + self.finality_signature_creation = false; + } } } @@ -1262,6 +1272,7 @@ impl MainReactor { &mut self, effect_builder: EffectBuilder, rng: &mut NodeRng, + create_finality_signatures: bool, mut meta_block: MetaBlock, ) -> Effects { debug!( @@ -1378,6 +1389,7 @@ impl MainReactor { .mut_state() .register_we_have_tried_to_sign() .was_updated() + && create_finality_signatures { // When this node is a validator in this era, sign and announce. if let Some(finality_signature) = self diff --git a/node/src/reactor/main_reactor/tests.rs b/node/src/reactor/main_reactor/tests.rs index 7b6f2f1dca..6c36a1d0d2 100644 --- a/node/src/reactor/main_reactor/tests.rs +++ b/node/src/reactor/main_reactor/tests.rs @@ -1,31 +1,38 @@ use std::{ - collections::BTreeMap, convert::TryFrom, iter, net::SocketAddr, str::FromStr, sync::Arc, + collections::{BTreeMap, BTreeSet}, + convert::TryFrom, + iter, + net::SocketAddr, + str::FromStr, + sync::Arc, time::Duration, }; use either::Either; use num::Zero; use num_rational::Ratio; +use num_traits::One; use rand::Rng; use tempfile::TempDir; use tokio::time::{self, error::Elapsed}; use tracing::{error, info}; use casper_storage::{ - data_access_layer::{BidsRequest, BidsResult}, + data_access_layer::{BidsRequest, BidsResult, QueryRequest, QueryResult::*}, global_state::state::{StateProvider, StateReader}, + tracking_copy::TrackingCopyError, }; use casper_types::{ execution::{ExecutionResult, ExecutionResultV2, Transform, TransformKind}, system::{ auction::{BidAddr, BidKind, BidsExt, DelegationRate}, - AUCTION, + mint, AUCTION, }, testing::TestRng, AccountConfig, AccountsConfig, ActivationPoint, AddressableEntityHash, Block, BlockHash, - BlockHeader, BlockV2, CLValue, Chainspec, ChainspecRawBytes, Deploy, EraId, Key, Motes, - ProtocolVersion, PublicKey, SecretKey, StoredValue, SystemEntityRegistry, TimeDiff, Timestamp, - Transaction, TransactionHash, ValidatorConfig, U512, + BlockHeader, BlockV2, CLValue, Chainspec, ChainspecRawBytes, ConsensusProtocolName, Deploy, + EntityAddr, EraId, Key, Motes, ProtocolVersion, PublicKey, Rewards, SecretKey, StoredValue, + SystemEntityRegistry, TimeDiff, Timestamp, Transaction, TransactionHash, ValidatorConfig, U512, }; use crate::{ @@ -41,10 +48,11 @@ use crate::{ requests::{ContractRuntimeRequest, NetworkRequest}, EffectExt, }, + failpoints::FailpointActivation, protocol::Message, reactor::{ main_reactor::{Config, MainEvent, MainReactor, ReactorState}, - Runner, + Reactor, Runner, }, testing::{ self, filter_reactor::FilterReactor, network::TestingNetwork, ConditionCheckReactor, @@ -78,18 +86,33 @@ enum InitialStakes { AllEqual { count: usize, stake: u128 }, } -struct ChainspecOverride { +/// Options to allow overriding default chainspec and config settings. +struct ConfigsOverride { + era_duration: TimeDiff, minimum_block_time: TimeDiff, minimum_era_height: u64, unbonding_delay: u64, + round_seigniorage_rate: Ratio, + consensus_protocol: ConsensusProtocolName, + finders_fee: Ratio, + finality_signature_proportion: Ratio, + signature_rewards_max_delay: u64, + storage_multiplier: u8, } -impl Default for ChainspecOverride { +impl Default for ConfigsOverride { fn default() -> Self { - ChainspecOverride { + ConfigsOverride { + era_duration: TimeDiff::from_millis(0), // zero means use the default value minimum_block_time: "1second".parse().unwrap(), minimum_era_height: 2, unbonding_delay: 3, + round_seigniorage_rate: Ratio::new(1, 100), + consensus_protocol: ConsensusProtocolName::Zug, + finders_fee: Ratio::new(1, 4), + finality_signature_proportion: Ratio::new(1, 3), + signature_rewards_max_delay: 5, + storage_multiplier: 1, } } } @@ -114,7 +137,7 @@ impl TestFixture { /// /// Runs the network until all nodes are initialized (i.e. none of their reactor states are /// still `ReactorState::Initialize`). - async fn new(initial_stakes: InitialStakes, spec_override: Option) -> Self { + async fn new(initial_stakes: InitialStakes, spec_override: Option) -> Self { let mut rng = TestRng::new(); let stake_values = match initial_stakes { InitialStakes::FromVec(stakes) => { @@ -147,7 +170,7 @@ impl TestFixture { mut rng: TestRng, secret_keys: Vec>, stakes: BTreeMap, - spec_override: Option, + spec_override: Option, ) -> Self { testing::init_logging(); @@ -184,12 +207,31 @@ impl TestFixture { chainspec.core_config.era_duration = TimeDiff::from_millis(0); chainspec.core_config.auction_delay = 1; chainspec.core_config.validator_slots = 100; - let spec_override = spec_override.unwrap_or_default(); - chainspec.core_config.minimum_block_time = spec_override.minimum_block_time; - chainspec.core_config.minimum_era_height = spec_override.minimum_era_height; - chainspec.core_config.unbonding_delay = spec_override.unbonding_delay; + let ConfigsOverride { + era_duration, + minimum_block_time, + minimum_era_height, + unbonding_delay, + round_seigniorage_rate, + consensus_protocol, + finders_fee, + finality_signature_proportion, + signature_rewards_max_delay, + storage_multiplier, + } = spec_override.unwrap_or_default(); + if era_duration != TimeDiff::from_millis(0) { + chainspec.core_config.era_duration = era_duration; + } + chainspec.core_config.minimum_block_time = minimum_block_time; + chainspec.core_config.minimum_era_height = minimum_era_height; + chainspec.core_config.unbonding_delay = unbonding_delay; + chainspec.core_config.round_seigniorage_rate = round_seigniorage_rate; + chainspec.core_config.consensus_protocol = consensus_protocol; + chainspec.core_config.finders_fee = finders_fee; + chainspec.core_config.finality_signature_proportion = finality_signature_proportion; chainspec.highway_config.maximum_round_length = chainspec.core_config.minimum_block_time * 2; + chainspec.core_config.signature_rewards_max_delay = signature_rewards_max_delay; let mut fixture = TestFixture { rng, @@ -200,7 +242,8 @@ impl TestFixture { }; for secret_key in secret_keys { - let (config, storage_dir) = fixture.create_node_config(secret_key.as_ref(), None); + let (config, storage_dir) = + fixture.create_node_config(secret_key.as_ref(), None, storage_multiplier); fixture.add_node(secret_key, config, storage_dir).await; } @@ -263,6 +306,7 @@ impl TestFixture { &mut self, secret_key: &SecretKey, maybe_trusted_hash: Option, + storage_multiplier: u8, ) -> (Config, TempDir) { // Set the network configuration. let network_cfg = match self.node_contexts.first() { @@ -283,7 +327,7 @@ impl TestFixture { }; // Additionally set up storage in a temporary directory. - let (storage_cfg, temp_dir) = storage::Config::default_for_tests(); + let (storage_cfg, temp_dir) = storage::Config::new_for_tests(storage_multiplier); // ...and the secret key for our validator. { let secret_key_path = temp_dir.path().join("secret_key"); @@ -294,6 +338,8 @@ impl TestFixture { } cfg.storage = storage_cfg; cfg.node.trusted_hash = maybe_trusted_hash; + cfg.contract_runtime.max_global_state_size = + Some(1024 * 1024 * storage_multiplier as usize); (cfg, temp_dir) } @@ -764,7 +810,7 @@ async fn run_network() { #[tokio::test] async fn historical_sync_with_era_height_1() { let initial_stakes = InitialStakes::Random { count: 5 }; - let spec_override = ChainspecOverride { + let spec_override = ConfigsOverride { minimum_block_time: "4seconds".parse().unwrap(), ..Default::default() }; @@ -776,7 +822,7 @@ async fn historical_sync_with_era_height_1() { // Create a joiner node. let secret_key = SecretKey::random(&mut fixture.rng); let trusted_hash = *fixture.highest_complete_block().hash(); - let (mut config, storage_dir) = fixture.create_node_config(&secret_key, Some(trusted_hash)); + let (mut config, storage_dir) = fixture.create_node_config(&secret_key, Some(trusted_hash), 1); config.node.sync_handling = SyncHandling::Genesis; let joiner_id = fixture .add_node(Arc::new(secret_key), config, storage_dir) @@ -812,9 +858,9 @@ async fn historical_sync_with_era_height_1() { #[tokio::test] async fn should_not_historical_sync_no_sync_node() { let initial_stakes = InitialStakes::Random { count: 5 }; - let spec_override = ChainspecOverride { + let spec_override = ConfigsOverride { minimum_block_time: "4seconds".parse().unwrap(), - minimum_era_height: 1, + minimum_era_height: 2, ..Default::default() }; let mut fixture = TestFixture::new(initial_stakes, Some(spec_override)).await; @@ -833,7 +879,7 @@ async fn should_not_historical_sync_no_sync_node() { ); info!("joining node using block {trusted_height} {trusted_hash}"); let secret_key = SecretKey::random(&mut fixture.rng); - let (mut config, storage_dir) = fixture.create_node_config(&secret_key, Some(trusted_hash)); + let (mut config, storage_dir) = fixture.create_node_config(&secret_key, Some(trusted_hash), 1); config.node.sync_handling = SyncHandling::NoSync; let joiner_id = fixture .add_node(Arc::new(secret_key), config, storage_dir) @@ -916,8 +962,10 @@ async fn run_equivocator_network() { ]; // We configure the era to take 15 rounds. That should guarantee that the two nodes equivocate. - let spec_override = ChainspecOverride { + let spec_override = ConfigsOverride { minimum_era_height: 10, + consensus_protocol: ConsensusProtocolName::Highway, + storage_multiplier: 2, ..Default::default() }; @@ -1311,7 +1359,7 @@ async fn empty_block_validation_regression() { count: 4, stake: 100, }; - let spec_override = ChainspecOverride { + let spec_override = ConfigsOverride { minimum_era_height: 15, ..Default::default() }; @@ -1587,7 +1635,7 @@ async fn run_redelegate_bid_network() { charlie_stake.into(), ]); - let spec_override = ChainspecOverride { + let spec_override = ConfigsOverride { unbonding_delay: 1, minimum_era_height: 5, ..Default::default() @@ -1709,7 +1757,7 @@ async fn run_redelegate_bid_network() { #[tokio::test] async fn rewards_are_calculated() { let initial_stakes = InitialStakes::Random { count: 5 }; - let spec_override = ChainspecOverride { + let spec_override = ConfigsOverride { minimum_era_height: 3, ..Default::default() }; @@ -1724,3 +1772,674 @@ async fn rewards_are_calculated() { assert_ne!(reward, &U512::zero()); } } + +// Reactor pattern tests for simplified rewards + +// Fundamental network parameters that are not critical for assessing reward calculation correctness +const STAKE: u128 = 1000000000; +const PRIME_STAKES: [u128; 5] = [106907, 106921, 106937, 106949, 106957]; +const ERA_COUNT: u64 = 3; +const ERA_DURATION: u64 = 30000; //milliseconds +const MIN_HEIGHT: u64 = 10; +const BLOCK_TIME: u64 = 3000; //milliseconds +const TIME_OUT: u64 = 600; //seconds +const SEIGNIORAGE: (u64, u64) = (1u64, 100u64); +const REPRESENTATIVE_NODE_INDEX: usize = 0; +// Parameters we generally want to vary +const CONSENSUS_ZUG: ConsensusProtocolName = ConsensusProtocolName::Zug; +const CONSENSUS_HIGHWAY: ConsensusProtocolName = ConsensusProtocolName::Highway; +const FINDERS_FEE_ZERO: (u64, u64) = (0u64, 1u64); +const FINDERS_FEE_HALF: (u64, u64) = (1u64, 2u64); +//const FINDERS_FEE_ONE: (u64, u64) = (1u64, 1u64); +const FINALITY_SIG_PROP_ZERO: (u64, u64) = (0u64, 1u64); +const FINALITY_SIG_PROP_HALF: (u64, u64) = (1u64, 2u64); +const FINALITY_SIG_PROP_ONE: (u64, u64) = (1u64, 1u64); +const FILTERED_NODES_INDICES: &[usize] = &[3, 4]; +const FINALITY_SIG_LOOKBACK: u64 = 3; + +async fn run_rewards_network_scenario( + initial_stakes: impl Into>, + era_count: u64, + time_out: u64, //seconds + representative_node_index: usize, + filtered_nodes_indices: &[usize], + spec_override: ConfigsOverride, +) { + trait AsU512Ext { + fn into_u512(self) -> Ratio; + } + impl AsU512Ext for Ratio { + fn into_u512(self) -> Ratio { + Ratio::new(U512::from(*self.numer()), U512::from(*self.denom())) + } + } + + let initial_stakes = initial_stakes.into(); + + // Instantiate the chain + let mut fixture = + TestFixture::new(InitialStakes::FromVec(initial_stakes), Some(spec_override)).await; + + for i in filtered_nodes_indices { + let filtered_node = fixture.network.runners_mut().nth(*i).unwrap(); + filtered_node + .reactor_mut() + .inner_mut() + .activate_failpoint(&FailpointActivation::new("finality_signature_creation")); + } + + // Run the network for a specified number of eras + // TODO: Consider replacing era duration estimate with actual chainspec value + let timeout = Duration::from_secs(time_out); + fixture + .run_until_stored_switch_block_header(EraId::new(era_count - 1), timeout) + .await; + + // DATA COLLECTION + // Get the switch blocks and bid structs first + let switch_blocks = SwitchBlocks::collect(fixture.network.nodes(), era_count); + + // Representative node + // (this test should normally run a network at nominal performance with identical nodes) + let representative_node = fixture + .network + .nodes() + .values() + .nth(representative_node_index) + .unwrap(); + let representative_storage = &representative_node.main_reactor().storage; + let representative_runtime = &representative_node.main_reactor().contract_runtime; + + // Recover highest completed block height + let highest_completed_height = representative_storage + .highest_complete_block_height() + .expect("missing highest completed block"); + + // Get all the blocks + let blocks: Vec = (0..highest_completed_height + 1) + .map(|i| { + representative_storage + .read_block_by_height(i) + .expect("block not found") + .unwrap() + }) + .collect(); + + // Recover history of total supply + let mint_hash: AddressableEntityHash = { + let any_state_hash = *switch_blocks.headers[0].state_root_hash(); + representative_runtime + .engine_state() + .get_system_mint_hash(any_state_hash) + .expect("mint contract hash not found") + }; + + // Get total supply history + let total_supply: Vec = (0..highest_completed_height + 1) + .map(|height: u64| { + let state_hash = *representative_storage + .read_block_header_by_height(height, true) + .expect("failure to read block header") + .unwrap() + .state_root_hash(); + + let request = QueryRequest::new( + state_hash, + Key::AddressableEntity(EntityAddr::System(mint_hash.value())), + vec![mint::TOTAL_SUPPLY_KEY.to_owned()], + ); + + let result = representative_runtime.engine_state().run_query(request); + + match result { + Success { value, proofs: _ } => value + .as_cl_value() + .expect("failure to recover total supply as CL value") + .clone() + .into_t::() + .map_err(TrackingCopyError::CLValue), + ValueNotFound(_) => Err(TrackingCopyError::NamedKeyNotFound( + mint::TOTAL_SUPPLY_KEY.to_owned(), + )), + RootNotFound => Err(TrackingCopyError::Storage( + casper_storage::global_state::error::Error::RootNotFound, + )), + Failure(e) => Err(e), + } + .expect("failure to recover total supply") + }) + .collect(); + + // Tiny helper function + #[inline] + fn add_to_rewards( + recipient: PublicKey, + reward: Ratio, + rewards: &mut BTreeMap>, + ) { + match rewards.get_mut(&recipient) { + Some(value) => { + *value += reward; + } + None => { + rewards.insert(recipient, reward); + } + } + } + + let mut recomputed_total_supply = BTreeMap::new(); + recomputed_total_supply.insert(0, Ratio::from(total_supply[0])); + let recomputed_rewards: BTreeMap<_, _> = switch_blocks + .headers + .iter() + .enumerate() + .map(|(i, switch_block)| { + if switch_block.is_genesis() || switch_block.height() > highest_completed_height { + return (i, BTreeMap::new()); + } + let mut recomputed_era_rewards = BTreeMap::new(); + if !(switch_block.is_genesis()) { + let supply_carryover = recomputed_total_supply + .get(&(i - 1)) + .copied() + .expect("expected prior recomputed supply value"); + recomputed_total_supply.insert(i, supply_carryover); + } + + // It's not a genesis block, so we know there's something with a lower era id + let previous_switch_block_height = switch_blocks.headers[i - 1].height(); + let current_era_slated_weights = match switch_blocks.headers[i - 1].clone_era_end() { + Some(era_report) => era_report.next_era_validator_weights().clone(), + _ => panic!("unexpectedly absent era report"), + }; + let total_current_era_weights = current_era_slated_weights + .iter() + .fold(U512::zero(), move |acc, s| acc + s.1); + let (previous_era_slated_weights, total_previous_era_weights) = + if switch_blocks.headers[i - 1].is_genesis() { + (None, None) + } else { + match switch_blocks.headers[i - 2].clone_era_end() { + Some(era_report) => { + let next_weights = era_report.next_era_validator_weights().clone(); + let total_next_weights = next_weights + .iter() + .fold(U512::zero(), move |acc, s| acc + s.1); + (Some(next_weights), Some(total_next_weights)) + } + _ => panic!("unexpectedly absent era report"), + } + }; + + // TODO: Investigate whether the rewards pay out for the signatures + // _in the switch block itself_ + let rewarded_range = + previous_switch_block_height as usize + 1..switch_block.height() as usize + 1; + let rewarded_blocks = &blocks[rewarded_range]; + let block_reward = (Ratio::::one() + - fixture + .chainspec + .core_config + .finality_signature_proportion + .into_u512()) + * recomputed_total_supply[&(i - 1)] + * fixture + .chainspec + .core_config + .round_seigniorage_rate + .into_u512(); + let signatures_reward = fixture + .chainspec + .core_config + .finality_signature_proportion + .into_u512() + * recomputed_total_supply[&(i - 1)] + * fixture + .chainspec + .core_config + .round_seigniorage_rate + .into_u512(); + let previous_signatures_reward = if switch_blocks.headers[i - 1].is_genesis() { + None + } else { + Some( + fixture + .chainspec + .core_config + .finality_signature_proportion + .into_u512() + * recomputed_total_supply[&(i - 2)] + * fixture + .chainspec + .core_config + .round_seigniorage_rate + .into_u512(), + ) + }; + + rewarded_blocks.iter().for_each(|block: &Block| { + // Block production rewards + let proposer = block.proposer().clone(); + add_to_rewards(proposer.clone(), block_reward, &mut recomputed_era_rewards); + + // Recover relevant finality signatures + // TODO: Deal with the implicit assumption that lookback only look backs one + // previous era + block.rewarded_signatures().iter().enumerate().for_each( + |(offset, signatures_packed)| { + if block.height() as usize - offset - 1 + <= previous_switch_block_height as usize + && !switch_blocks.headers[i - 1].is_genesis() + { + let rewarded_contributors = signatures_packed.to_validator_set( + previous_era_slated_weights + .as_ref() + .expect("expected previous era weights") + .keys() + .cloned() + .collect::>(), + ); + rewarded_contributors.iter().for_each(|contributor| { + let contributor_proportion = Ratio::new( + previous_era_slated_weights + .as_ref() + .expect("expected previous era weights") + .get(contributor) + .copied() + .expect("expected current era validator"), + total_previous_era_weights + .expect("expected total previous era weight"), + ); + add_to_rewards( + proposer.clone(), + fixture.chainspec.core_config.finders_fee.into_u512() + * contributor_proportion + * previous_signatures_reward.unwrap(), + &mut recomputed_era_rewards, + ); + add_to_rewards( + contributor.clone(), + (Ratio::::one() + - fixture.chainspec.core_config.finders_fee.into_u512()) + * contributor_proportion + * previous_signatures_reward.unwrap(), + &mut recomputed_era_rewards, + ) + }); + } else { + let rewarded_contributors = signatures_packed.to_validator_set( + current_era_slated_weights + .keys() + .cloned() + .collect::>(), + ); + rewarded_contributors.iter().for_each(|contributor| { + let contributor_proportion = Ratio::new( + *current_era_slated_weights + .get(contributor) + .expect("expected current era validator"), + total_current_era_weights, + ); + add_to_rewards( + proposer.clone(), + fixture.chainspec.core_config.finders_fee.into_u512() + * contributor_proportion + * signatures_reward, + &mut recomputed_era_rewards, + ); + add_to_rewards( + contributor.clone(), + (Ratio::::one() + - fixture.chainspec.core_config.finders_fee.into_u512()) + * contributor_proportion + * signatures_reward, + &mut recomputed_era_rewards, + ); + }); + } + }, + ); + }); + + // Make sure we round just as we do in the real code, at the end of an era's + // calculation, right before minting and transferring + recomputed_era_rewards.iter_mut().for_each(|(_, reward)| { + let truncated_reward = reward.trunc(); + *reward = truncated_reward; + let era_end_supply = recomputed_total_supply + .get_mut(&i) + .expect("expected supply at end of era"); + *era_end_supply += truncated_reward; + }); + + (i, recomputed_era_rewards) + }) + .collect(); + + // Recalculated total supply is equal to observed total supply + switch_blocks.headers.iter().for_each(|header| { + if header.height() <= highest_completed_height { + assert_eq!( + Ratio::from(total_supply[header.height() as usize]), + *(recomputed_total_supply + .get(&(header.era_id().value() as usize)) + .expect("expected recalculated supply")), + "total supply does not match at height {}", + header.height() + ) + } + }); + + // Recalculated rewards are equal to observed rewards; total supply increase is equal to total + // rewards; + recomputed_rewards.iter().for_each(|(era, rewards)| { + if era > &0 && switch_blocks.headers[*era].height() <= highest_completed_height { + let observed_total_rewards = match switch_blocks.headers[*era] + .clone_era_end() + .expect("expected EraEnd") + .rewards() + { + Rewards::V1(v1_rewards) => v1_rewards + .iter() + .fold(U512::zero(), |acc, reward| U512::from(*reward.1) + acc), + Rewards::V2(v2_rewards) => v2_rewards + .iter() + .fold(U512::zero(), |acc, reward| *reward.1 + acc), + }; + let recomputed_total_rewards = rewards + .iter() + .fold(U512::zero(), |acc, x| x.1.to_integer() + acc); + assert_eq!( + Ratio::from(recomputed_total_rewards), + Ratio::from(observed_total_rewards), + "total rewards do not match at era {}", + era + ); + assert_eq!( + Ratio::from(recomputed_total_rewards), + recomputed_total_supply + .get(era) + .expect("expected recalculated supply") + - recomputed_total_supply + .get(&(era - 1)) + .expect("expected recalculated supply"), + "supply growth does not match rewards at era {}", + era + ) + } + }) +} + +#[tokio::test] +#[cfg_attr(not(feature = "failpoints"), ignore)] +async fn run_reward_network_zug_all_finality_small_prime_five_eras() { + run_rewards_network_scenario( + PRIME_STAKES, + 5, + TIME_OUT, + REPRESENTATIVE_NODE_INDEX, + &[], + ConfigsOverride { + consensus_protocol: CONSENSUS_ZUG, + era_duration: TimeDiff::from_millis(ERA_DURATION), + minimum_era_height: MIN_HEIGHT, + minimum_block_time: TimeDiff::from_millis(BLOCK_TIME), + round_seigniorage_rate: SEIGNIORAGE.into(), + finders_fee: FINDERS_FEE_ZERO.into(), + finality_signature_proportion: FINALITY_SIG_PROP_ONE.into(), + signature_rewards_max_delay: FINALITY_SIG_LOOKBACK, + ..Default::default() + }, + ) + .await; +} + +#[tokio::test] +#[cfg_attr(not(feature = "failpoints"), ignore)] +async fn run_reward_network_zug_all_finality_small_prime_five_eras_no_lookback() { + run_rewards_network_scenario( + PRIME_STAKES, + 5, + TIME_OUT, + REPRESENTATIVE_NODE_INDEX, + &[], + ConfigsOverride { + consensus_protocol: CONSENSUS_ZUG, + era_duration: TimeDiff::from_millis(ERA_DURATION), + minimum_era_height: MIN_HEIGHT, + minimum_block_time: TimeDiff::from_millis(BLOCK_TIME), + round_seigniorage_rate: SEIGNIORAGE.into(), + finders_fee: FINDERS_FEE_ZERO.into(), + finality_signature_proportion: FINALITY_SIG_PROP_ONE.into(), + signature_rewards_max_delay: 0, + ..Default::default() + }, + ) + .await; +} + +#[tokio::test] +#[cfg_attr(not(feature = "failpoints"), ignore)] +async fn run_reward_network_zug_no_finality_small_nominal_five_eras() { + run_rewards_network_scenario( + [STAKE, STAKE, STAKE, STAKE, STAKE], + 5, + TIME_OUT, + REPRESENTATIVE_NODE_INDEX, + &[], + ConfigsOverride { + consensus_protocol: CONSENSUS_ZUG, + era_duration: TimeDiff::from_millis(ERA_DURATION), + minimum_era_height: MIN_HEIGHT, + minimum_block_time: TimeDiff::from_millis(BLOCK_TIME), + round_seigniorage_rate: SEIGNIORAGE.into(), + finders_fee: FINDERS_FEE_ZERO.into(), + finality_signature_proportion: FINALITY_SIG_PROP_ZERO.into(), + signature_rewards_max_delay: FINALITY_SIG_LOOKBACK, + ..Default::default() + }, + ) + .await; +} + +#[tokio::test] +#[cfg_attr(not(feature = "failpoints"), ignore)] +async fn run_reward_network_zug_half_finality_half_finders_small_nominal_five_eras() { + run_rewards_network_scenario( + [STAKE, STAKE, STAKE, STAKE, STAKE], + 5, + TIME_OUT, + REPRESENTATIVE_NODE_INDEX, + &[], + ConfigsOverride { + consensus_protocol: CONSENSUS_ZUG, + era_duration: TimeDiff::from_millis(ERA_DURATION), + minimum_era_height: MIN_HEIGHT, + minimum_block_time: TimeDiff::from_millis(BLOCK_TIME), + round_seigniorage_rate: SEIGNIORAGE.into(), + finders_fee: FINDERS_FEE_HALF.into(), + finality_signature_proportion: FINALITY_SIG_PROP_HALF.into(), + signature_rewards_max_delay: FINALITY_SIG_LOOKBACK, + ..Default::default() + }, + ) + .await; +} + +#[tokio::test] +#[cfg_attr(not(feature = "failpoints"), ignore)] +async fn run_reward_network_zug_half_finality_half_finders_small_nominal_five_eras_no_lookback() { + run_rewards_network_scenario( + [STAKE, STAKE, STAKE, STAKE, STAKE], + 5, + TIME_OUT, + REPRESENTATIVE_NODE_INDEX, + &[], + ConfigsOverride { + consensus_protocol: CONSENSUS_ZUG, + era_duration: TimeDiff::from_millis(ERA_DURATION), + minimum_era_height: MIN_HEIGHT, + minimum_block_time: TimeDiff::from_millis(BLOCK_TIME), + round_seigniorage_rate: SEIGNIORAGE.into(), + finders_fee: FINDERS_FEE_HALF.into(), + finality_signature_proportion: FINALITY_SIG_PROP_HALF.into(), + signature_rewards_max_delay: 0, + ..Default::default() + }, + ) + .await; +} + +#[tokio::test] +#[cfg_attr(not(feature = "failpoints"), ignore)] +async fn run_reward_network_zug_all_finality_half_finders_small_nominal_five_eras_no_lookback() { + run_rewards_network_scenario( + [STAKE, STAKE, STAKE, STAKE, STAKE], + 5, + TIME_OUT, + REPRESENTATIVE_NODE_INDEX, + &[], + ConfigsOverride { + consensus_protocol: CONSENSUS_ZUG, + era_duration: TimeDiff::from_millis(ERA_DURATION), + minimum_era_height: MIN_HEIGHT, + minimum_block_time: TimeDiff::from_millis(BLOCK_TIME), + round_seigniorage_rate: SEIGNIORAGE.into(), + finders_fee: FINDERS_FEE_HALF.into(), + finality_signature_proportion: FINALITY_SIG_PROP_ONE.into(), + signature_rewards_max_delay: 0, + ..Default::default() + }, + ) + .await; +} + +#[tokio::test] +#[cfg_attr(not(feature = "failpoints"), ignore)] +async fn run_reward_network_zug_all_finality_half_finders() { + run_rewards_network_scenario( + [ + STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, + ], + ERA_COUNT, + TIME_OUT, + REPRESENTATIVE_NODE_INDEX, + FILTERED_NODES_INDICES, + ConfigsOverride { + consensus_protocol: CONSENSUS_ZUG, + era_duration: TimeDiff::from_millis(ERA_DURATION), + minimum_era_height: MIN_HEIGHT, + minimum_block_time: TimeDiff::from_millis(BLOCK_TIME), + round_seigniorage_rate: SEIGNIORAGE.into(), + finders_fee: FINDERS_FEE_HALF.into(), + finality_signature_proportion: FINALITY_SIG_PROP_ONE.into(), + signature_rewards_max_delay: FINALITY_SIG_LOOKBACK, + ..Default::default() + }, + ) + .await; +} + +#[tokio::test] +#[cfg_attr(not(feature = "failpoints"), ignore)] +async fn run_reward_network_zug_all_finality_half_finders_five_eras() { + run_rewards_network_scenario( + [ + STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, + ], + 5, + TIME_OUT, + REPRESENTATIVE_NODE_INDEX, + FILTERED_NODES_INDICES, + ConfigsOverride { + consensus_protocol: CONSENSUS_ZUG, + era_duration: TimeDiff::from_millis(ERA_DURATION), + minimum_era_height: MIN_HEIGHT, + minimum_block_time: TimeDiff::from_millis(BLOCK_TIME), + round_seigniorage_rate: SEIGNIORAGE.into(), + finders_fee: FINDERS_FEE_HALF.into(), + finality_signature_proportion: FINALITY_SIG_PROP_ONE.into(), + signature_rewards_max_delay: FINALITY_SIG_LOOKBACK, + ..Default::default() + }, + ) + .await; +} + +#[tokio::test] +#[cfg_attr(not(feature = "failpoints"), ignore)] +async fn run_reward_network_zug_all_finality_zero_finders() { + run_rewards_network_scenario( + [ + STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, + ], + ERA_COUNT, + TIME_OUT, + REPRESENTATIVE_NODE_INDEX, + FILTERED_NODES_INDICES, + ConfigsOverride { + consensus_protocol: CONSENSUS_ZUG, + era_duration: TimeDiff::from_millis(ERA_DURATION), + minimum_era_height: MIN_HEIGHT, + minimum_block_time: TimeDiff::from_millis(BLOCK_TIME), + round_seigniorage_rate: SEIGNIORAGE.into(), + finders_fee: FINDERS_FEE_ZERO.into(), + finality_signature_proportion: FINALITY_SIG_PROP_ONE.into(), + signature_rewards_max_delay: FINALITY_SIG_LOOKBACK, + ..Default::default() + }, + ) + .await; +} + +#[tokio::test] +#[cfg_attr(not(feature = "failpoints"), ignore)] +async fn run_reward_network_highway_all_finality_zero_finders() { + run_rewards_network_scenario( + [ + STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, + ], + ERA_COUNT, + TIME_OUT, + REPRESENTATIVE_NODE_INDEX, + FILTERED_NODES_INDICES, + ConfigsOverride { + consensus_protocol: CONSENSUS_HIGHWAY, + era_duration: TimeDiff::from_millis(ERA_DURATION), + minimum_era_height: MIN_HEIGHT, + minimum_block_time: TimeDiff::from_millis(BLOCK_TIME), + round_seigniorage_rate: SEIGNIORAGE.into(), + finders_fee: FINDERS_FEE_ZERO.into(), + finality_signature_proportion: FINALITY_SIG_PROP_ONE.into(), + signature_rewards_max_delay: FINALITY_SIG_LOOKBACK, + ..Default::default() + }, + ) + .await; +} + +#[tokio::test] +#[cfg_attr(not(feature = "failpoints"), ignore)] +async fn run_reward_network_highway_no_finality() { + run_rewards_network_scenario( + [ + STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, STAKE, + ], + ERA_COUNT, + TIME_OUT, + REPRESENTATIVE_NODE_INDEX, + FILTERED_NODES_INDICES, + ConfigsOverride { + consensus_protocol: CONSENSUS_HIGHWAY, + era_duration: TimeDiff::from_millis(ERA_DURATION), + minimum_era_height: MIN_HEIGHT, + minimum_block_time: TimeDiff::from_millis(BLOCK_TIME), + round_seigniorage_rate: SEIGNIORAGE.into(), + finders_fee: FINDERS_FEE_ZERO.into(), + finality_signature_proportion: FINALITY_SIG_PROP_ZERO.into(), + signature_rewards_max_delay: FINALITY_SIG_LOOKBACK, + ..Default::default() + }, + ) + .await; +} diff --git a/node/src/testing/filter_reactor.rs b/node/src/testing/filter_reactor.rs index 7a07c54988..90a51a9c50 100644 --- a/node/src/testing/filter_reactor.rs +++ b/node/src/testing/filter_reactor.rs @@ -13,6 +13,7 @@ use super::network::NetworkedReactor; use crate::{ components::network::Identity as NetworkIdentity, effect::{EffectBuilder, Effects}, + failpoints::FailpointActivation, reactor::{EventQueueHandle, Finalize, Reactor}, types::NodeId, NodeRng, @@ -86,6 +87,10 @@ impl Reactor for FilterReactor { Either::Right(event) => self.reactor.dispatch_event(effect_builder, rng, event), } } + + fn activate_failpoint(&mut self, activation: &FailpointActivation) { + self.reactor.activate_failpoint(activation); + } } impl Finalize for FilterReactor {