diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 4861b7893e5554..d547abc096d65d 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -4393,10 +4393,10 @@ impl ReplayStage { fn record_rewards(bank: &Bank, rewards_recorder_sender: &Option) { if let Some(rewards_recorder_sender) = rewards_recorder_sender { - let rewards = bank.rewards.read().unwrap(); - if !rewards.is_empty() { + let rewards = bank.get_rewards_and_num_partitions(); + if rewards.should_record() { rewards_recorder_sender - .send(RewardsMessage::Batch((bank.slot(), rewards.clone()))) + .send(RewardsMessage::Batch((bank.slot(), rewards))) .unwrap_or_else(|err| warn!("rewards_recorder_sender failed: {:?}", err)); } rewards_recorder_sender diff --git a/core/src/rewards_recorder_service.rs b/core/src/rewards_recorder_service.rs index 3fc2c8dc5b5149..044fd2de53adc7 100644 --- a/core/src/rewards_recorder_service.rs +++ b/core/src/rewards_recorder_service.rs @@ -1,8 +1,9 @@ use { crossbeam_channel::{Receiver, RecvTimeoutError, Sender}, solana_ledger::blockstore::Blockstore, - solana_sdk::{clock::Slot, pubkey::Pubkey, reward_info::RewardInfo}, - solana_transaction_status::Reward, + solana_runtime::bank::KeyedRewardsAndNumPartitions, + solana_sdk::clock::Slot, + solana_transaction_status::{Reward, RewardsAndNumPartitions}, std::{ sync::{ atomic::{AtomicBool, AtomicU64, Ordering}, @@ -13,7 +14,7 @@ use { }, }; -pub type RewardsBatch = (Slot, Vec<(Pubkey, RewardInfo)>); +pub type RewardsBatch = (Slot, KeyedRewardsAndNumPartitions); pub type RewardsRecorderReceiver = Receiver; pub type RewardsRecorderSender = Sender; @@ -55,7 +56,13 @@ impl RewardsRecorderService { blockstore: &Blockstore, ) -> Result<(), RecvTimeoutError> { match rewards_receiver.recv_timeout(Duration::from_secs(1))? { - RewardsMessage::Batch((slot, rewards)) => { + RewardsMessage::Batch(( + slot, + KeyedRewardsAndNumPartitions { + keyed_rewards: rewards, + num_partitions, + }, + )) => { let rpc_rewards = rewards .into_iter() .map(|(pubkey, reward_info)| Reward { @@ -68,7 +75,13 @@ impl RewardsRecorderService { .collect(); blockstore - .write_rewards(slot, rpc_rewards) + .write_rewards( + slot, + RewardsAndNumPartitions { + rewards: rpc_rewards, + num_partitions, + }, + ) .expect("Expect database write to succeed"); } RewardsMessage::Complete(slot) => { diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 450f7b4f005f3c..0342a323905876 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -57,8 +57,9 @@ use { solana_storage_proto::{StoredExtendedRewards, StoredTransactionStatusMeta}, solana_transaction_status::{ ConfirmedTransactionStatusWithSignature, ConfirmedTransactionWithStatusMeta, Rewards, - TransactionStatusMeta, TransactionWithStatusMeta, VersionedConfirmedBlock, - VersionedConfirmedBlockWithEntries, VersionedTransactionWithStatusMeta, + RewardsAndNumPartitions, TransactionStatusMeta, TransactionWithStatusMeta, + VersionedConfirmedBlock, VersionedConfirmedBlockWithEntries, + VersionedTransactionWithStatusMeta, }, std::{ borrow::Cow, @@ -2678,7 +2679,7 @@ impl Blockstore { Hash::default() }; - let rewards = self + let (rewards, num_partitions) = self .rewards_cf .get_protobuf_or_bincode::(slot)? .unwrap_or_default() @@ -2699,6 +2700,7 @@ impl Blockstore { transactions: self .map_transactions_to_statuses(slot, slot_transaction_iterator)?, rewards, + num_partitions, block_time, block_height, }; @@ -3371,7 +3373,7 @@ impl Blockstore { .map(|result| result.map(|option| option.into())) } - pub fn write_rewards(&self, index: Slot, rewards: Rewards) -> Result<()> { + pub fn write_rewards(&self, index: Slot, rewards: RewardsAndNumPartitions) -> Result<()> { let rewards = rewards.into(); self.rewards_cf.put_protobuf(index, &rewards) } @@ -8302,6 +8304,7 @@ pub mod tests { blockhash: blockhash.to_string(), previous_blockhash: Hash::default().to_string(), rewards: vec![], + num_partitions: None, block_time: None, block_height: None, }; @@ -8316,6 +8319,7 @@ pub mod tests { blockhash: blockhash.to_string(), previous_blockhash: blockhash.to_string(), rewards: vec![], + num_partitions: None, block_time: None, block_height: None, }; @@ -8333,6 +8337,7 @@ pub mod tests { blockhash: blockhash.to_string(), previous_blockhash: blockhash.to_string(), rewards: vec![], + num_partitions: None, block_time: None, block_height: None, }; diff --git a/rpc-client/src/mock_sender.rs b/rpc-client/src/mock_sender.rs index 806d56d70a2f6d..8c5c58086e6fe6 100644 --- a/rpc-client/src/mock_sender.rs +++ b/rpc-client/src/mock_sender.rs @@ -406,6 +406,7 @@ impl RpcSender for MockSender { version: Some(TransactionVersion::LEGACY), }], rewards: Rewards::new(), + num_partitions: None, block_time: None, block_height: Some(428), })?, diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 18aafe957d01ef..551474c168d00a 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -35,7 +35,6 @@ //! already been signed and verified. #[allow(deprecated)] use solana_sdk::recent_blockhashes_account; -pub use solana_sdk::reward_type::RewardType; use { crate::{ bank::{ @@ -205,6 +204,9 @@ use { time::{Duration, Instant}, }, }; +pub use { + partitioned_epoch_rewards::KeyedRewardsAndNumPartitions, solana_sdk::reward_type::RewardType, +}; #[cfg(feature = "dev-context-only-utils")] use { solana_accounts_db::accounts_db::{ diff --git a/runtime/src/bank/partitioned_epoch_rewards/mod.rs b/runtime/src/bank/partitioned_epoch_rewards/mod.rs index f374b127050aef..99323c990a0c12 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/mod.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/mod.rs @@ -150,7 +150,36 @@ pub(super) struct CalculateRewardsAndDistributeVoteRewardsResult { pub(crate) type StakeRewards = Vec; +#[derive(Debug, PartialEq)] +pub struct KeyedRewardsAndNumPartitions { + pub keyed_rewards: Vec<(Pubkey, RewardInfo)>, + pub num_partitions: Option, +} + +impl KeyedRewardsAndNumPartitions { + pub fn should_record(&self) -> bool { + !self.keyed_rewards.is_empty() || self.num_partitions.is_some() + } +} + impl Bank { + pub fn get_rewards_and_num_partitions(&self) -> KeyedRewardsAndNumPartitions { + let keyed_rewards = self.rewards.read().unwrap().clone(); + let epoch_rewards_sysvar = self.get_epoch_rewards_sysvar(); + // If partitioned epoch rewards are active and this Bank is the + // epoch-boundary block, populate num_partitions + let epoch_schedule = self.epoch_schedule(); + let parent_epoch = epoch_schedule.get_epoch(self.parent_slot()); + let is_first_block_in_epoch = self.epoch() > parent_epoch; + + let num_partitions = (epoch_rewards_sysvar.active && is_first_block_in_epoch) + .then_some(epoch_rewards_sysvar.num_partitions); + KeyedRewardsAndNumPartitions { + keyed_rewards, + num_partitions, + } + } + pub(super) fn is_partitioned_rewards_feature_enabled(&self) -> bool { self.feature_set .is_active(&feature_set::enable_partitioned_epoch_reward::id()) @@ -248,6 +277,7 @@ mod tests { account::Account, epoch_schedule::EpochSchedule, native_token::LAMPORTS_PER_SOL, + reward_type::RewardType, signature::Signer, signer::keypair::Keypair, stake::instruction::StakeError, @@ -684,7 +714,7 @@ mod tests { /// Test that program execution that attempts to mutate a stake account /// incorrectly should fail during reward period. A credit should succeed, - /// but a withdrawal shoudl fail. + /// but a withdrawal should fail. #[test] fn test_program_execution_restricted_for_stake_account_in_reward_period() { use solana_sdk::transaction::TransactionError::InstructionError; @@ -800,4 +830,242 @@ mod tests { previous_bank = bank; } } + + #[test] + fn test_get_rewards_and_partitions() { + let starting_slot = SLOTS_PER_EPOCH - 1; + let num_rewards = 100; + let stake_account_stores_per_block = 50; + let RewardBank { bank, .. } = + create_reward_bank(num_rewards, stake_account_stores_per_block, starting_slot); + + assert!(bank.is_partitioned_rewards_feature_enabled()); + // Slot before the epoch boundary contains empty rewards (since fees are + // off), and no partitions because not at the epoch boundary + assert_eq!( + bank.get_rewards_and_num_partitions(), + KeyedRewardsAndNumPartitions { + keyed_rewards: vec![], + num_partitions: None, + } + ); + + let epoch_boundary_bank = Arc::new(Bank::new_from_parent( + bank, + &Pubkey::default(), + SLOTS_PER_EPOCH, + )); + assert!(epoch_boundary_bank.is_partitioned_rewards_feature_enabled()); + // Slot at the epoch boundary contains voting rewards only, as well as partition data + let KeyedRewardsAndNumPartitions { + keyed_rewards, + num_partitions, + } = epoch_boundary_bank.get_rewards_and_num_partitions(); + for (_pubkey, reward) in keyed_rewards.iter() { + assert_eq!(reward.reward_type, RewardType::Voting); + } + assert_eq!(keyed_rewards.len(), num_rewards); + assert_eq!( + num_partitions, + Some(num_rewards as u64 / stake_account_stores_per_block) + ); + + let mut total_staking_rewards = 0; + + let partition0_bank = Arc::new(Bank::new_from_parent( + epoch_boundary_bank, + &Pubkey::default(), + SLOTS_PER_EPOCH + 1, + )); + assert!(partition0_bank.is_partitioned_rewards_feature_enabled()); + // Slot after the epoch boundary contains first partition of staking + // rewards, and no partitions because not at the epoch boundary + let KeyedRewardsAndNumPartitions { + keyed_rewards, + num_partitions, + } = partition0_bank.get_rewards_and_num_partitions(); + for (_pubkey, reward) in keyed_rewards.iter() { + assert_eq!(reward.reward_type, RewardType::Staking); + } + total_staking_rewards += keyed_rewards.len(); + assert_eq!(num_partitions, None); + + let partition1_bank = Arc::new(Bank::new_from_parent( + partition0_bank, + &Pubkey::default(), + SLOTS_PER_EPOCH + 2, + )); + assert!(partition1_bank.is_partitioned_rewards_feature_enabled()); + // Slot 2 after the epoch boundary contains second partition of staking + // rewards, and no partitions because not at the epoch boundary + let KeyedRewardsAndNumPartitions { + keyed_rewards, + num_partitions, + } = partition1_bank.get_rewards_and_num_partitions(); + for (_pubkey, reward) in keyed_rewards.iter() { + assert_eq!(reward.reward_type, RewardType::Staking); + } + total_staking_rewards += keyed_rewards.len(); + assert_eq!(num_partitions, None); + + // All rewards are recorded + assert_eq!(total_staking_rewards, num_rewards); + + let bank = Bank::new_from_parent(partition1_bank, &Pubkey::default(), SLOTS_PER_EPOCH + 3); + assert!(bank.is_partitioned_rewards_feature_enabled()); + // Next slot contains empty rewards (since fees are off), and no + // partitions because not at the epoch boundary + assert_eq!( + bank.get_rewards_and_num_partitions(), + KeyedRewardsAndNumPartitions { + keyed_rewards: vec![], + num_partitions: None, + } + ); + } + + #[test] + fn test_get_rewards_and_partitions_before_feature() { + let starting_slot = SLOTS_PER_EPOCH - 1; + let num_rewards = 100; + + let validator_keypairs = (0..num_rewards) + .map(|_| ValidatorVoteKeypairs::new_rand()) + .collect::>(); + + let GenesisConfigInfo { + mut genesis_config, .. + } = create_genesis_config_with_vote_accounts( + 1_000_000_000, + &validator_keypairs, + vec![2_000_000_000; num_rewards], + ); + genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); + + // Set feature to inactive + genesis_config + .accounts + .remove(&feature_set::enable_partitioned_epoch_reward::id()); + + let bank = Bank::new_for_tests(&genesis_config); + + for validator_vote_keypairs in &validator_keypairs { + let vote_id = validator_vote_keypairs.vote_keypair.pubkey(); + let mut vote_account = bank.get_account(&vote_id).unwrap(); + // generate some rewards + let mut vote_state = Some(vote_state::from(&vote_account).unwrap()); + for i in 0..MAX_LOCKOUT_HISTORY + 42 { + if let Some(v) = vote_state.as_mut() { + vote_state::process_slot_vote_unchecked(v, i as u64) + } + let versioned = VoteStateVersions::Current(Box::new(vote_state.take().unwrap())); + vote_state::to(&versioned, &mut vote_account).unwrap(); + match versioned { + VoteStateVersions::Current(v) => { + vote_state = Some(*v); + } + _ => panic!("Has to be of type Current"), + }; + } + bank.store_account_and_update_capitalization(&vote_id, &vote_account); + } + + let (bank, bank_forks) = bank.wrap_with_bank_forks_for_tests(); + let bank = new_bank_from_parent_with_bank_forks( + &bank_forks, + bank, + &Pubkey::default(), + starting_slot, + ); + + assert!(!bank.is_partitioned_rewards_feature_enabled()); + // Slot before the epoch boundary contains empty rewards (since fees are + // off), and no partitions because feature is inactive + assert_eq!( + bank.get_rewards_and_num_partitions(), + KeyedRewardsAndNumPartitions { + keyed_rewards: vec![], + num_partitions: None, + } + ); + + let epoch_boundary_bank = Arc::new(Bank::new_from_parent( + bank, + &Pubkey::default(), + SLOTS_PER_EPOCH, + )); + assert!(!epoch_boundary_bank.is_partitioned_rewards_feature_enabled()); + // Slot at the epoch boundary contains voting rewards and staking rewards; still no partitions + let KeyedRewardsAndNumPartitions { + keyed_rewards, + num_partitions, + } = epoch_boundary_bank.get_rewards_and_num_partitions(); + let mut voting_rewards_count = 0; + let mut staking_rewards_count = 0; + for (_pubkey, reward) in keyed_rewards.iter() { + match reward.reward_type { + RewardType::Voting => { + voting_rewards_count += 1; + } + RewardType::Staking => { + staking_rewards_count += 1; + } + _ => {} + } + } + assert_eq!( + keyed_rewards.len(), + voting_rewards_count + staking_rewards_count + ); + assert_eq!(voting_rewards_count, num_rewards); + assert_eq!(staking_rewards_count, num_rewards); + assert!(num_partitions.is_none()); + + let bank = + Bank::new_from_parent(epoch_boundary_bank, &Pubkey::default(), SLOTS_PER_EPOCH + 1); + assert!(!bank.is_partitioned_rewards_feature_enabled()); + // Slot after the epoch boundary contains empty rewards (since fees are + // off), and no partitions because feature is inactive + assert_eq!( + bank.get_rewards_and_num_partitions(), + KeyedRewardsAndNumPartitions { + keyed_rewards: vec![], + num_partitions: None, + } + ); + } + + #[test] + fn test_rewards_and_partitions_should_record() { + let reward = RewardInfo { + reward_type: RewardType::Voting, + lamports: 55, + post_balance: 5555, + commission: Some(5), + }; + + let rewards_and_partitions = KeyedRewardsAndNumPartitions { + keyed_rewards: vec![], + num_partitions: None, + }; + assert!(!rewards_and_partitions.should_record()); + + let rewards_and_partitions = KeyedRewardsAndNumPartitions { + keyed_rewards: vec![(Pubkey::new_unique(), reward)], + num_partitions: None, + }; + assert!(rewards_and_partitions.should_record()); + + let rewards_and_partitions = KeyedRewardsAndNumPartitions { + keyed_rewards: vec![], + num_partitions: Some(42), + }; + assert!(rewards_and_partitions.should_record()); + + let rewards_and_partitions = KeyedRewardsAndNumPartitions { + keyed_rewards: vec![(Pubkey::new_unique(), reward)], + num_partitions: Some(42), + }; + assert!(rewards_and_partitions.should_record()); + } } diff --git a/storage-bigtable/src/bigtable.rs b/storage-bigtable/src/bigtable.rs index fdebb5ab8d0214..b4bfe040a30963 100644 --- a/storage-bigtable/src/bigtable.rs +++ b/storage-bigtable/src/bigtable.rs @@ -985,6 +985,7 @@ mod tests { parent_slot, transactions, rewards, + num_partitions, block_time, block_height, } = confirmed_block; @@ -995,6 +996,8 @@ mod tests { parent_slot, transactions: transactions.into_iter().map(|tx| tx.into()).collect(), rewards: rewards.into_iter().map(|r| r.into()).collect(), + num_partitions: num_partitions + .map(|num_partitions| generated::NumPartitions { num_partitions }), block_time: block_time.map(|timestamp| generated::UnixTimestamp { timestamp }), block_height: block_height.map(|block_height| generated::BlockHeight { block_height }), } @@ -1028,6 +1031,7 @@ mod tests { blockhash: Hash::default().to_string(), previous_blockhash: Hash::default().to_string(), rewards: vec![], + num_partitions: None, block_time: Some(1_234_567_890), block_height: Some(1), }; diff --git a/storage-bigtable/src/lib.rs b/storage-bigtable/src/lib.rs index 240ae44c3d07fe..3af928a626d834 100644 --- a/storage-bigtable/src/lib.rs +++ b/storage-bigtable/src/lib.rs @@ -141,6 +141,7 @@ impl From for StoredConfirmedBlock { parent_slot, transactions, rewards, + num_partitions: _num_partitions, block_time, block_height, } = confirmed_block; @@ -175,6 +176,7 @@ impl From for ConfirmedBlock { parent_slot, transactions: transactions.into_iter().map(|tx| tx.into()).collect(), rewards: rewards.into_iter().map(|reward| reward.into()).collect(), + num_partitions: None, block_time, block_height, } diff --git a/storage-proto/proto/confirmed_block.proto b/storage-proto/proto/confirmed_block.proto index 47548ea13bc6a4..6d26ce7bce2cad 100644 --- a/storage-proto/proto/confirmed_block.proto +++ b/storage-proto/proto/confirmed_block.proto @@ -10,6 +10,7 @@ message ConfirmedBlock { repeated Reward rewards = 5; UnixTimestamp block_time = 6; BlockHeight block_height = 7; + NumPartitions num_partitions = 8; } message ConfirmedTransaction { @@ -130,6 +131,7 @@ message Reward { message Rewards { repeated Reward rewards = 1; + NumPartitions num_partitions = 2; } message UnixTimestamp { @@ -139,3 +141,7 @@ message UnixTimestamp { message BlockHeight { uint64 block_height = 1; } + +message NumPartitions { + uint64 num_partitions = 1; +} diff --git a/storage-proto/src/convert.rs b/storage-proto/src/convert.rs index 8d6669e44b43f1..8315bcf99a4dac 100644 --- a/storage-proto/src/convert.rs +++ b/storage-proto/src/convert.rs @@ -16,8 +16,9 @@ use { }, solana_transaction_status::{ ConfirmedBlock, EntrySummary, InnerInstruction, InnerInstructions, Reward, RewardType, - TransactionByAddrInfo, TransactionStatusMeta, TransactionTokenBalance, - TransactionWithStatusMeta, VersionedConfirmedBlock, VersionedTransactionWithStatusMeta, + RewardsAndNumPartitions, TransactionByAddrInfo, TransactionStatusMeta, + TransactionTokenBalance, TransactionWithStatusMeta, VersionedConfirmedBlock, + VersionedTransactionWithStatusMeta, }, std::{ convert::{TryFrom, TryInto}, @@ -47,6 +48,16 @@ impl From> for generated::Rewards { fn from(rewards: Vec) -> Self { Self { rewards: rewards.into_iter().map(|r| r.into()).collect(), + num_partitions: None, + } + } +} + +impl From for generated::Rewards { + fn from(input: RewardsAndNumPartitions) -> Self { + Self { + rewards: input.rewards.into_iter().map(|r| r.into()).collect(), + num_partitions: input.num_partitions.map(|n| n.into()), } } } @@ -57,6 +68,17 @@ impl From for Vec { } } +impl From for (Vec, Option) { + fn from(rewards: generated::Rewards) -> Self { + ( + rewards.rewards.into_iter().map(|r| r.into()).collect(), + rewards + .num_partitions + .map(|generated::NumPartitions { num_partitions }| num_partitions), + ) + } +} + impl From for generated::Rewards { fn from(rewards: StoredExtendedRewards) -> Self { Self { @@ -67,6 +89,7 @@ impl From for generated::Rewards { r.into() }) .collect(), + num_partitions: None, } } } @@ -121,6 +144,12 @@ impl From for Reward { } } +impl From for generated::NumPartitions { + fn from(num_partitions: u64) -> Self { + Self { num_partitions } + } +} + impl From for generated::ConfirmedBlock { fn from(confirmed_block: VersionedConfirmedBlock) -> Self { let VersionedConfirmedBlock { @@ -129,6 +158,7 @@ impl From for generated::ConfirmedBlock { parent_slot, transactions, rewards, + num_partitions, block_time, block_height, } = confirmed_block; @@ -139,6 +169,7 @@ impl From for generated::ConfirmedBlock { parent_slot, transactions: transactions.into_iter().map(|tx| tx.into()).collect(), rewards: rewards.into_iter().map(|r| r.into()).collect(), + num_partitions: num_partitions.map(Into::into), block_time: block_time.map(|timestamp| generated::UnixTimestamp { timestamp }), block_height: block_height.map(|block_height| generated::BlockHeight { block_height }), } @@ -156,6 +187,7 @@ impl TryFrom for ConfirmedBlock { parent_slot, transactions, rewards, + num_partitions, block_time, block_height, } = confirmed_block; @@ -169,6 +201,8 @@ impl TryFrom for ConfirmedBlock { .map(|tx| tx.try_into()) .collect::, Self::Error>>()?, rewards: rewards.into_iter().map(|r| r.into()).collect(), + num_partitions: num_partitions + .map(|generated::NumPartitions { num_partitions }| num_partitions), block_time: block_time.map(|generated::UnixTimestamp { timestamp }| timestamp), block_height: block_height.map(|generated::BlockHeight { block_height }| block_height), }) diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index 7779cfc5ae9353..489a213c525a94 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -628,6 +628,11 @@ pub struct Reward { pub type Rewards = Vec; +pub struct RewardsAndNumPartitions { + pub rewards: Rewards, + pub num_partitions: Option, +} + #[derive(Debug, Error)] pub enum ConvertBlockError { #[error("transactions missing after converted, before: {0}, after: {1}")] @@ -641,6 +646,7 @@ pub struct ConfirmedBlock { pub parent_slot: Slot, pub transactions: Vec, pub rewards: Rewards, + pub num_partitions: Option, pub block_time: Option, pub block_height: Option, } @@ -654,6 +660,7 @@ pub struct VersionedConfirmedBlock { pub parent_slot: Slot, pub transactions: Vec, pub rewards: Rewards, + pub num_partitions: Option, pub block_time: Option, pub block_height: Option, } @@ -670,6 +677,7 @@ impl From for ConfirmedBlock { .map(TransactionWithStatusMeta::Complete) .collect(), rewards: block.rewards, + num_partitions: block.num_partitions, block_time: block.block_time, block_height: block.block_height, } @@ -704,6 +712,7 @@ impl TryFrom for VersionedConfirmedBlock { parent_slot: block.parent_slot, transactions: txs, rewards: block.rewards, + num_partitions: block.num_partitions, block_time: block.block_time, block_height: block.block_height, }) @@ -768,6 +777,7 @@ impl ConfirmedBlock { } else { None }, + num_reward_partitions: self.num_partitions, block_time: self.block_time, block_height: self.block_height, }) @@ -782,6 +792,7 @@ pub struct EncodedConfirmedBlock { pub parent_slot: Slot, pub transactions: Vec, pub rewards: Rewards, + pub num_partitions: Option, pub block_time: Option, pub block_height: Option, } @@ -794,6 +805,7 @@ impl From for EncodedConfirmedBlock { parent_slot: block.parent_slot, transactions: block.transactions.unwrap_or_default(), rewards: block.rewards.unwrap_or_default(), + num_partitions: block.num_reward_partitions, block_time: block.block_time, block_height: block.block_height, } @@ -812,6 +824,8 @@ pub struct UiConfirmedBlock { pub signatures: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub rewards: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub num_reward_partitions: Option, pub block_time: Option, pub block_height: Option, }