diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_end/update_state_cache/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_end/update_state_cache/v0/mod.rs index 8e3b8d0e285..0c7a6547c91 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_end/update_state_cache/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_end/update_state_cache/v0/mod.rs @@ -54,6 +54,11 @@ where // Persist block state self.store_platform_state(&block_platform_state, Some(transaction), platform_version)?; + // Whatever the store wrote is now what is on disk for this block, so the + // next block only has to write the full record if it changes something + // heavy itself. + block_platform_state.heavy_fields_dirty = false; + let block_platform_state = Arc::new(block_platform_state); self.state.store(block_platform_state); diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rs index 0c5750d8947..06234489047 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rs @@ -109,6 +109,23 @@ where .. } = &masternode_diff; + // Core advances a block without any masternode changing far more often + // than not. Returning before the first mutable borrow keeps the platform + // state clean, which is what lets the block skip rewriting the full saved + // state (over a megabyte on mainnet) to disk. + if !start_from_scratch + && added_mns.is_empty() + && removed_mns.is_empty() + && updated_mns.is_empty() + { + return Ok( + update_state_masternode_list_outcome::v0::UpdateStateMasternodeListOutcome { + masternode_list_diff: masternode_diff, + removed_masternodes: BTreeMap::new(), + }, + ); + } + //todo: clean up let added_hpmns = added_mns.iter().filter_map(|masternode| { if masternode.node_type == MasternodeType::Evo { diff --git a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_quorum_info/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_quorum_info/v0/mod.rs index 0d6cf494ef7..f834c03f420 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_quorum_info/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_quorum_info/v0/mod.rs @@ -117,27 +117,34 @@ where .into_iter() .collect(); - let mut removed_a_validator_set = false; + // Checked before taking a mutable borrow: on most blocks Core reports the + // same quorums as the block before, and taking the borrow marks the whole + // platform state as needing a full rewrite to disk. + let removed_a_validator_set = block_platform_state + .validator_sets() + .keys() + .any(|quorum_hash| !validator_quorums_list.contains_key::(quorum_hash)); // Remove validator_sets entries that are no longer valid for the core block height - block_platform_state - .validator_sets_mut() - .retain(|quorum_hash, _| { - let retain = validator_quorums_list.contains_key::(quorum_hash); - removed_a_validator_set |= !retain; - - if !retain { - tracing::trace!( - ?quorum_hash, - quorum_type = ?self.config.validator_set.quorum_type, - "removed validator set {} with quorum type {}", - quorum_hash, - self.config.validator_set.quorum_type - ) - } + if removed_a_validator_set { + block_platform_state + .validator_sets_mut() + .retain(|quorum_hash, _| { + let retain = validator_quorums_list.contains_key::(quorum_hash); + + if !retain { + tracing::trace!( + ?quorum_hash, + quorum_type = ?self.config.validator_set.quorum_type, + "removed validator set {} with quorum type {}", + quorum_hash, + self.config.validator_set.quorum_type + ) + } - retain - }); + retain + }); + } // Fetch quorum info and their keys from the RPC for new quorums let mut quorum_infos = validator_quorums_list @@ -192,25 +199,28 @@ where let is_validator_set_updated = !new_validator_sets.is_empty() || removed_a_validator_set; - // Add new validator_sets entries - block_platform_state - .validator_sets_mut() - .extend(new_validator_sets); - - // Sort all validator sets into deterministic order by core block height of creation - block_platform_state - .validator_sets_mut() - .sort_by(|_, quorum_a, _, quorum_b| { - let primary_comparison = quorum_b.core_height().cmp(&quorum_a.core_height()); - if primary_comparison == std::cmp::Ordering::Equal { - quorum_b - .quorum_hash() - .cmp(quorum_a.quorum_hash()) - .then_with(|| quorum_b.core_height().cmp(&quorum_a.core_height())) - } else { - primary_comparison - } - }); + // Add new validator_sets entries. Nothing added and nothing removed means + // the map is already the one the previous block sorted, so leave it be. + if is_validator_set_updated { + block_platform_state + .validator_sets_mut() + .extend(new_validator_sets); + + // Sort all validator sets into deterministic order by core block height of creation + block_platform_state + .validator_sets_mut() + .sort_by(|_, quorum_a, _, quorum_b| { + let primary_comparison = quorum_b.core_height().cmp(&quorum_a.core_height()); + if primary_comparison == std::cmp::Ordering::Equal { + quorum_b + .quorum_hash() + .cmp(quorum_a.quorum_hash()) + .then_with(|| quorum_b.core_height().cmp(&quorum_a.core_height())) + } else { + primary_comparison + } + }); + } // Update Chain Lock quorums @@ -231,7 +241,7 @@ where } else { self.update_quorums_from_quorum_list( quorum_set_type, - block_platform_state.chain_lock_validating_quorums_mut(), + block_platform_state, platform_state, &extended_quorum_list, last_committed_core_height, @@ -266,7 +276,7 @@ where } else { self.update_quorums_from_quorum_list( quorum_set_type, - block_platform_state.instant_lock_validating_quorums_mut(), + block_platform_state, platform_state, &extended_quorum_list, last_committed_core_height, @@ -319,7 +329,7 @@ where fn update_quorums_from_quorum_list( &self, quorum_set_type: QuorumSetType, - quorum_set: &mut SignatureVerificationQuorumSet, + block_platform_state: &mut PlatformState, platform_state: Option<&PlatformState>, full_quorum_list: &ExtendedQuorumListResult, last_committed_core_height: u32, @@ -341,6 +351,25 @@ where }) .collect(); + // Core reports the same quorums on most blocks. Decide read-only whether + // anything moved, because reaching for the mutable quorum set marks the + // whole platform state as needing a full rewrite to disk. + { + let current = + quorum_set_by_type(block_platform_state, &quorum_set_type).current_quorums(); + let unchanged = current.len() == quorums_list.len() + && current.iter().all(|(quorum_hash, quorum)| { + quorums_list + .get(quorum_hash) + .is_some_and(|index| *index == quorum.index) + }); + if unchanged { + return Ok(false); + } + } + + let quorum_set = quorum_set_by_type_mut(block_platform_state, &quorum_set_type); + let mut removed_a_validating_quorum = false; // Remove validating_quorums entries that are no longer valid for the core block height diff --git a/packages/rs-drive-abci/src/execution/storage/fetch_platform_state/v0/mod.rs b/packages/rs-drive-abci/src/execution/storage/fetch_platform_state/v0/mod.rs index a815e0266a6..c63a7331107 100644 --- a/packages/rs-drive-abci/src/execution/storage/fetch_platform_state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/storage/fetch_platform_state/v0/mod.rs @@ -1,8 +1,11 @@ use crate::error::Error; use crate::platform_types::platform::Platform; +use crate::platform_types::platform_state::recent::PlatformStateRecent; use crate::platform_types::platform_state::PlatformState; +use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0Getters; use dpp::serialization::PlatformDeserializableFromVersionedStructure; use dpp::version::PlatformVersion; +use dpp::ProtocolError; use drive::drive::Drive; use drive::query::TransactionArg; @@ -12,23 +15,53 @@ impl Platform { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { - drive + let Some(bytes) = drive .fetch_platform_state_bytes(transaction, platform_version) .map_err(Error::Drive)? - .map(|bytes| { - let result = PlatformState::versioned_deserialize(&bytes, platform_version) - .map_err(Error::Protocol); + else { + return Ok(None); + }; - if result.is_err() { - tracing::error!( - bytes = hex::encode(&bytes), - "Unable deserialize platform state for version {}", - platform_version.protocol_version - ); - } - - result + let mut state = PlatformState::versioned_deserialize(&bytes, platform_version) + .inspect_err(|_| { + tracing::error!( + bytes = hex::encode(&bytes), + "Unable deserialize platform state for version {}", + platform_version.protocol_version + ); }) - .transpose() + .map_err(Error::Protocol)?; + + // The full record is only rewritten when a heavy field changes, so a + // newer small record holds the block info and quorum hashes for the + // blocks since. An older one (or none, on a database written before this + // existed) is ignored: the full record already has those fields. + if let Some(recent_bytes) = drive + .fetch_platform_state_recent_bytes(transaction) + .map_err(Error::Drive)? + { + let (recent, _): (PlatformStateRecent, _) = bincode::decode_from_slice( + &recent_bytes, + bincode::config::standard() + .with_big_endian() + .with_no_limit(), + ) + .map_err(|e| { + Error::Protocol(ProtocolError::PlatformDeserializationError(format!( + "unable to deserialize recent platform state: {e}" + ))) + })?; + + if recent.height() + >= state + .last_committed_block_info + .as_ref() + .map(|i| i.basic_info().height) + { + recent.apply_to(&mut state); + } + } + + Ok(Some(state)) } } diff --git a/packages/rs-drive-abci/src/execution/storage/store_platform_state/v0/mod.rs b/packages/rs-drive-abci/src/execution/storage/store_platform_state/v0/mod.rs index a6d79b2b483..998a5df7e0c 100644 --- a/packages/rs-drive-abci/src/execution/storage/store_platform_state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/storage/store_platform_state/v0/mod.rs @@ -1,8 +1,11 @@ use crate::error::Error; use crate::platform_types::platform::Platform; +use crate::platform_types::platform_state::recent::PlatformStateRecent; use crate::platform_types::platform_state::PlatformState; +use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0Getters; use dpp::serialization::PlatformSerializable; use dpp::version::PlatformVersion; +use dpp::ProtocolError; use drive::query::TransactionArg; impl Platform { @@ -13,21 +16,55 @@ impl Platform { platform_version: &PlatformVersion, ) -> Result<(), Error> { #[cfg(feature = "testing-config")] - { - if self.config.testing_configs.store_platform_state { + let should_store = self.config.testing_configs.store_platform_state; + #[cfg(not(feature = "testing-config"))] + let should_store = true; + + if should_store { + // The masternode lists, validator sets and quorum sets are most of + // the record — over a megabyte on mainnet — and only change when + // Core's do, which is a minority of blocks. While replaying history + // the full record is rewritten only when one of them moved, and the + // small record below carries the per-block fields in between. Both + // are written in the block's transaction, so a reader never sees + // them disagree. + // + // Once the node is at the tip the full record is written every block + // again, so a node that is up to date always has a complete record on + // disk and an older drive-abci — which knows nothing about the small + // record — can still read it. Skipping is confined to a node that is + // catching up, where the remedy for any format trouble is the resync + // it is already doing. + if state.heavy_fields_dirty + || !state + .last_committed_block_info + .as_ref() + .is_some_and(|info| { + crate::utils::is_historical_block(info.basic_info().time_ms) + }) + { + let bytes = state.serialize_to_bytes()?; self.drive - .store_platform_state_bytes( - &state.serialize_to_bytes()?, - transaction, - platform_version, - ) + .store_platform_state_bytes(&bytes, transaction, platform_version) .map_err(Error::Drive)?; } + + let recent: PlatformStateRecent = state.into(); + let recent_bytes = bincode::encode_to_vec( + recent, + bincode::config::standard() + .with_big_endian() + .with_no_limit(), + ) + .map_err(|e| { + Error::Protocol(ProtocolError::PlatformSerializationError(format!( + "unable to serialize recent platform state: {e}" + ))) + })?; + self.drive + .store_platform_state_recent_bytes(&recent_bytes, transaction) + .map_err(Error::Drive)?; } - #[cfg(not(feature = "testing-config"))] - self.drive - .store_platform_state_bytes(&state.serialize_to_bytes()?, transaction, platform_version) - .map_err(Error::Drive)?; // We need to persist new protocol version as well be able to read block state self.drive diff --git a/packages/rs-drive-abci/src/platform_types/platform_state/accessors.rs b/packages/rs-drive-abci/src/platform_types/platform_state/accessors.rs index 3130a567aa9..a0ac9955341 100644 --- a/packages/rs-drive-abci/src/platform_types/platform_state/accessors.rs +++ b/packages/rs-drive-abci/src/platform_types/platform_state/accessors.rs @@ -399,6 +399,10 @@ impl PlatformStateV0Methods for PlatformState { /// Sets the current protocol version in consensus. fn set_current_protocol_version_in_consensus(&mut self, version: ProtocolVersion) { self.current_protocol_version_in_consensus = version; + // The protocol version chooses the structure the full record is written + // in, so a change has to rewrite it rather than leave an older structure + // on disk with a newer version recorded beside it. + self.heavy_fields_dirty = true; } /// Sets the next epoch protocol version. @@ -419,26 +423,31 @@ impl PlatformStateV0Methods for PlatformState { /// Sets the current validator sets. fn set_validator_sets(&mut self, sets: IndexMap) { self.validator_sets = sets; + self.heavy_fields_dirty = true; } /// Sets the current chain lock validating quorums. fn set_chain_lock_validating_quorums(&mut self, quorums: SignatureVerificationQuorumSet) { self.chain_lock_validating_quorums = quorums; + self.heavy_fields_dirty = true; } /// Sets the current instant lock validating quorums. fn set_instant_lock_validating_quorums(&mut self, quorums: SignatureVerificationQuorumSet) { self.instant_lock_validating_quorums = quorums; + self.heavy_fields_dirty = true; } /// Sets the full masternode list. fn set_full_masternode_list(&mut self, list: BTreeMap) { self.full_masternode_list = list; + self.heavy_fields_dirty = true; } /// Sets the list of high performance masternodes. fn set_hpmn_masternode_list(&mut self, list: BTreeMap) { self.hpmn_masternode_list = list; + self.heavy_fields_dirty = true; } /// Sets the platform initialization information. @@ -451,6 +460,7 @@ impl PlatformStateV0Methods for PlatformState { } fn current_protocol_version_in_consensus_mut(&mut self) -> &mut ProtocolVersion { + self.heavy_fields_dirty = true; &mut self.current_protocol_version_in_consensus } @@ -467,22 +477,27 @@ impl PlatformStateV0Methods for PlatformState { } fn validator_sets_mut(&mut self) -> &mut IndexMap { + self.heavy_fields_dirty = true; &mut self.validator_sets } fn chain_lock_validating_quorums_mut(&mut self) -> &mut SignatureVerificationQuorumSet { + self.heavy_fields_dirty = true; &mut self.chain_lock_validating_quorums } fn instant_lock_validating_quorums_mut(&mut self) -> &mut SignatureVerificationQuorumSet { + self.heavy_fields_dirty = true; &mut self.instant_lock_validating_quorums } fn full_masternode_list_mut(&mut self) -> &mut BTreeMap { + self.heavy_fields_dirty = true; &mut self.full_masternode_list } fn hpmn_masternode_list_mut(&mut self) -> &mut BTreeMap { + self.heavy_fields_dirty = true; &mut self.hpmn_masternode_list } @@ -606,6 +621,7 @@ impl PlatformStateV0Methods for PlatformState { } fn previous_fee_versions_mut(&mut self) -> &mut CachedEpochIndexFeeVersions { + self.heavy_fields_dirty = true; &mut self.previous_fee_versions } } diff --git a/packages/rs-drive-abci/src/platform_types/platform_state/mod.rs b/packages/rs-drive-abci/src/platform_types/platform_state/mod.rs index 81f438fe51a..c25b7eed6b0 100644 --- a/packages/rs-drive-abci/src/platform_types/platform_state/mod.rs +++ b/packages/rs-drive-abci/src/platform_types/platform_state/mod.rs @@ -1,6 +1,7 @@ mod accessors; mod masternode_list_changes; mod platform_state_for_saving; +pub mod recent; use crate::error::Error; @@ -64,6 +65,14 @@ pub struct PlatformState { /// previous FeeVersions pub previous_fee_versions: CachedEpochIndexFeeVersions, + + /// True when a field carried only by the full saved record has changed since + /// the state was last written in full. The masternode lists, validator sets + /// and quorum sets are over a megabyte on mainnet and change on a minority of + /// blocks, so the full record is rewritten only when this is set; every block + /// still writes the small record holding the block info and quorum hashes. + /// Not part of the saved record: a state read back from disk starts dirty. + pub heavy_fields_dirty: bool, } fn hex_encoded_validator_sets(validator_sets: &IndexMap) -> String { @@ -149,6 +158,7 @@ impl PlatformState { hpmn_masternode_list: Default::default(), genesis_block_info: None, previous_fee_versions: Default::default(), + heavy_fields_dirty: true, }; Ok(state) @@ -162,7 +172,7 @@ impl PlatformSerializable for PlatformState { let platform_version = self.current_platform_version()?; let config = config::standard().with_big_endian().with_no_limit(); let platform_state_for_saving: PlatformStateForSaving = - self.clone().try_into_platform_versioned(platform_version)?; + self.try_into_platform_versioned(platform_version)?; bincode::encode_to_vec(platform_state_for_saving, config).map_err(|e| { ProtocolError::PlatformSerializationError(format!( "unable to serialize PlatformState: {}", @@ -198,6 +208,31 @@ impl PlatformDeserializableFromVersionedStructure for PlatformState { } } +impl TryFromPlatformVersioned<&PlatformState> for PlatformStateForSaving { + type Error = Error; + fn try_from_platform_versioned( + value: &PlatformState, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .structs + .platform_state_for_saving_structure_default + { + 0 => { + let saving_v1: PlatformStateForSavingV1 = value.try_into()?; + Ok(saving_v1.into()) + } + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "PlatformStateForSaving::try_from_platform_versioned(&PlatformState)" + .to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} + impl TryFromPlatformVersioned for PlatformStateForSaving { type Error = Error; fn try_from_platform_versioned( @@ -281,6 +316,32 @@ mod tests { .expect("failed to deserialize state"); } + /// Serializing through the borrowed conversion must produce exactly the + /// bytes the owned conversion produced, since it writes the same aux record. + #[test] + fn borrowed_serialization_matches_owned() { + let serialized_state = + hex::decode(PLATFORM_STATE_V8_DEVNET.deref()).expect("failed to decode hex"); + + let state = PlatformState::versioned_deserialize(&serialized_state, &PLATFORM_V9) + .expect("failed to deserialize state"); + + let platform_version = state + .current_platform_version() + .expect("state must know its version"); + let config = config::standard().with_big_endian().with_no_limit(); + let owned: PlatformStateForSaving = state + .clone() + .try_into_platform_versioned(platform_version) + .expect("owned conversion"); + let owned_bytes = bincode::encode_to_vec(owned, config).expect("owned encode"); + + assert_eq!( + state.serialize_to_bytes().expect("borrowed serialize"), + owned_bytes + ); + } + #[test] fn should_deserialize_state_stored_in_version_8_from_devnet() { let serialized_state = diff --git a/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v0/mod.rs b/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v0/mod.rs index d3e4f334b74..e9f28576faf 100644 --- a/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v0/mod.rs +++ b/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v0/mod.rs @@ -87,6 +87,8 @@ impl From for PlatformState { .into_keys() .map(|epoch_index| (epoch_index, FeeVersion::first())) .collect(), + // a state read back from disk has not been written in full since + heavy_fields_dirty: true, } } } diff --git a/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v1/mod.rs b/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v1/mod.rs index 14230624b12..edfcb29ae32 100644 --- a/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v1/mod.rs +++ b/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v1/mod.rs @@ -53,6 +53,65 @@ pub struct PlatformStateForSavingV1 { pub previous_fee_versions: EpochIndexFeeVersionsForStorage, } +impl TryFrom<&PlatformState> for PlatformStateForSavingV1 { + type Error = Error; + + /// Builds the saving form from a borrowed state. + /// + /// The owned conversion below is what serialization used to go through, and + /// reaching it from `&PlatformState` meant cloning the whole state first — + /// two full copies of the masternode lists and validator sets per block, on + /// a path that runs once per block. This clones each field once instead. + fn try_from(value: &PlatformState) -> Result { + let platform_version = value.current_platform_version()?; + Ok(PlatformStateForSavingV1 { + genesis_block_info: value.genesis_block_info, + last_committed_block_info: value.last_committed_block_info.clone(), + current_protocol_version_in_consensus: value.current_protocol_version_in_consensus, + next_epoch_protocol_version: value.next_epoch_protocol_version, + current_validator_set_quorum_hash: value + .current_validator_set_quorum_hash + .to_byte_array() + .into(), + next_validator_set_quorum_hash: value + .next_validator_set_quorum_hash + .map(|quorum_hash| quorum_hash.to_byte_array().into()), + validator_sets: value + .validator_sets + .iter() + .map(|(k, v)| (k.to_byte_array().into(), v.clone())) + .collect(), + chain_lock_validating_quorums: value.chain_lock_validating_quorums.clone().into(), + instant_lock_validating_quorums: value.instant_lock_validating_quorums.clone().into(), + full_masternode_list: value + .full_masternode_list + .iter() + .map(|(k, v)| { + Ok(( + k.to_byte_array().into(), + v.clone().try_into_platform_versioned(platform_version)?, + )) + }) + .collect::, Error>>()?, + hpmn_masternode_list: value + .hpmn_masternode_list + .iter() + .map(|(k, v)| { + Ok(( + k.to_byte_array().into(), + v.clone().try_into_platform_versioned(platform_version)?, + )) + }) + .collect::, Error>>()?, + previous_fee_versions: value + .previous_fee_versions + .iter() + .map(|(epoch_index, fee_version)| (*epoch_index, fee_version.fee_version_number)) + .collect(), + }) + } +} + impl TryFrom for PlatformStateForSavingV1 { type Error = Error; @@ -147,6 +206,8 @@ impl From for PlatformState { ) }) .collect(), + // a state read back from disk has not been written in full since + heavy_fields_dirty: true, } } } diff --git a/packages/rs-drive-abci/src/platform_types/platform_state/recent.rs b/packages/rs-drive-abci/src/platform_types/platform_state/recent.rs new file mode 100644 index 00000000000..d19e4694a4a --- /dev/null +++ b/packages/rs-drive-abci/src/platform_types/platform_state/recent.rs @@ -0,0 +1,86 @@ +//! The part of the platform state that changes on every block. +//! +//! The full saved state is over a megabyte on mainnet — masternode lists, +//! validator sets and the chain-lock and instant-lock quorum sets — and those +//! parts only change when Core's masternode list or quorums do. This record +//! carries the rest, so a block that changed nothing heavy writes a couple of +//! hundred bytes instead of rewriting the whole state. + +use crate::platform_types::platform_state::PlatformState; +use bincode::{Decode, Encode}; +use dpp::block::block_info::BlockInfo; +use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0Getters; +use dpp::block::extended_block_info::ExtendedBlockInfo; +use dpp::dashcore::hashes::Hash; +use dpp::dashcore::QuorumHash; +use dpp::platform_value::Bytes32; +use dpp::util::deserializer::ProtocolVersion; + +/// Versioned per-block platform state record. +#[derive(Clone, Debug, Encode, Decode)] +pub enum PlatformStateRecent { + /// Version 0 + V0(PlatformStateRecentV0), +} + +/// Version 0 of the per-block platform state record. +#[derive(Clone, Debug, Encode, Decode)] +pub struct PlatformStateRecentV0 { + /// Information about the genesis block + pub genesis_block_info: Option, + /// Information about the last block + pub last_committed_block_info: Option, + /// Current version + pub current_protocol_version_in_consensus: ProtocolVersion, + /// Upcoming protocol version + pub next_epoch_protocol_version: ProtocolVersion, + /// Current quorum + pub current_validator_set_quorum_hash: Bytes32, + /// Next quorum + pub next_validator_set_quorum_hash: Option, +} + +impl From<&PlatformState> for PlatformStateRecent { + fn from(state: &PlatformState) -> Self { + PlatformStateRecent::V0(PlatformStateRecentV0 { + genesis_block_info: state.genesis_block_info, + last_committed_block_info: state.last_committed_block_info.clone(), + current_protocol_version_in_consensus: state.current_protocol_version_in_consensus, + next_epoch_protocol_version: state.next_epoch_protocol_version, + current_validator_set_quorum_hash: state + .current_validator_set_quorum_hash + .to_byte_array() + .into(), + next_validator_set_quorum_hash: state + .next_validator_set_quorum_hash + .map(|hash| hash.to_byte_array().into()), + }) + } +} + +impl PlatformStateRecent { + /// Overwrite the per-block fields of `state` with the ones in this record. + /// + /// The heavy fields are left alone: they came from a full record written at + /// or before the height this record was written at, and are unchanged since. + pub fn apply_to(self, state: &mut PlatformState) { + let PlatformStateRecent::V0(v0) = self; + state.genesis_block_info = v0.genesis_block_info; + state.last_committed_block_info = v0.last_committed_block_info; + state.current_protocol_version_in_consensus = v0.current_protocol_version_in_consensus; + state.next_epoch_protocol_version = v0.next_epoch_protocol_version; + state.current_validator_set_quorum_hash = + QuorumHash::from_byte_array(v0.current_validator_set_quorum_hash.to_buffer()); + state.next_validator_set_quorum_hash = v0 + .next_validator_set_quorum_hash + .map(|bytes| QuorumHash::from_byte_array(bytes.to_buffer())); + } + + /// The height this record was written at, if it has block info. + pub fn height(&self) -> Option { + let PlatformStateRecent::V0(v0) = self; + v0.last_committed_block_info + .as_ref() + .map(|info| info.basic_info().height) + } +} diff --git a/packages/rs-drive-abci/src/utils/replay.rs b/packages/rs-drive-abci/src/utils/replay.rs index 47b89a3d96d..cf651e44589 100644 --- a/packages/rs-drive-abci/src/utils/replay.rs +++ b/packages/rs-drive-abci/src/utils/replay.rs @@ -33,9 +33,7 @@ mod tests { #[test] fn a_block_from_a_year_ago_is_historical() { - assert!(is_historical_block( - now_ms() - 365 * 24 * 60 * 60 * 1000 - )); + assert!(is_historical_block(now_ms() - 365 * 24 * 60 * 60 * 1000)); } #[test] diff --git a/packages/rs-drive/src/drive/platform_state/mod.rs b/packages/rs-drive/src/drive/platform_state/mod.rs index d6a0ce16c49..7a4b178726b 100644 --- a/packages/rs-drive/src/drive/platform_state/mod.rs +++ b/packages/rs-drive/src/drive/platform_state/mod.rs @@ -1,4 +1,37 @@ +use crate::drive::Drive; mod fetch_platform_state_bytes; mod store_platform_state_bytes; const PLATFORM_STATE_KEY: &[u8; 11] = b"saved_state"; + +/// The small companion to [`PLATFORM_STATE_KEY`]: the fields of the platform +/// state that change on every block. The full record is rewritten only when the +/// masternode lists, validator sets or quorum sets change, so this one carries +/// the block info in between. Both are written in the block's transaction, so a +/// reader always sees a pair that committed together. +const PLATFORM_STATE_RECENT_KEY: &[u8; 18] = b"saved_state_recent"; + +impl Drive { + /// Stores the per-block part of the platform state in auxiliary storage. + pub fn store_platform_state_recent_bytes( + &self, + state_bytes: &[u8], + transaction: grovedb::TransactionArg, + ) -> Result<(), crate::error::Error> { + self.grove + .put_aux(PLATFORM_STATE_RECENT_KEY, state_bytes, None, transaction) + .unwrap() + .map_err(crate::error::Error::from) + } + + /// Fetches the per-block part of the platform state, if one was ever written. + pub fn fetch_platform_state_recent_bytes( + &self, + transaction: grovedb::TransactionArg, + ) -> Result>, crate::error::Error> { + self.grove + .get_aux(PLATFORM_STATE_RECENT_KEY, transaction) + .unwrap() + .map_err(crate::error::Error::from) + } +}