From a319483d2ec834edef48a5da84b83a1e3ee5cad1 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 31 Aug 2026 17:23:46 +0200 Subject: [PATCH] perf(drive-abci): stop rewriting the whole platform state every block The saved platform state is 1.28 MB on mainnet, almost all of it masternode lists, validator sets and quorum sets, and it was serialized and written to GroveDB aux storage on every block. Serialization also cloned the entire state first, so a block paid two full copies of some 4,000 masternodes. Serialization now builds the saving form from a borrowed state, and a test asserts the bytes are identical to the owned path. The heavy fields carry a dirty flag set by the accessors that can change them, and while replaying history the full record is rewritten only when it is set, with a small companion record holding the per-block fields written every block; both land in the block's transaction, and a database without the companion record reads exactly as before. Once at the tip the full record is written every block again, so an up-to-date node always has a complete record on disk. The two Core-driven update paths now decide read-only whether anything actually moved before taking a mutable borrow, because Core reports the same masternodes and quorums on most blocks and the borrow alone would force the rewrite. --- .../block_end/update_state_cache/v0/mod.rs | 5 + .../update_state_masternode_list/v0/mod.rs | 17 +++ .../update_quorum_info/v0/mod.rs | 109 +++++++++++------- .../storage/fetch_platform_state/v0/mod.rs | 61 +++++++--- .../storage/store_platform_state/v0/mod.rs | 59 ++++++++-- .../platform_state/accessors.rs | 16 +++ .../src/platform_types/platform_state/mod.rs | 63 +++++++++- .../platform_state_for_saving/v0/mod.rs | 2 + .../platform_state_for_saving/v1/mod.rs | 61 ++++++++++ .../platform_types/platform_state/recent.rs | 86 ++++++++++++++ packages/rs-drive-abci/src/utils/replay.rs | 4 +- .../rs-drive/src/drive/platform_state/mod.rs | 33 ++++++ 12 files changed, 447 insertions(+), 69 deletions(-) create mode 100644 packages/rs-drive-abci/src/platform_types/platform_state/recent.rs 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) + } +}