Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ where
let block_time = block_info.block_time_ms();
let block_height = block_info.height();

// Checkpoints are restore points for a running node. Replaying history,
// ten minutes of chain time is a handful of blocks, so this fires dozens
// of times a second and all but the last `keep_n` are deleted again
// immediately — each one a RocksDB checkpoint over the whole database
// plus a copy of the platform state. The node writes its first real
// checkpoint once it reaches the tip.
if crate::utils::is_historical_block(block_time) {
return Ok(None);
}

let most_recent_checkpoint_interval_time =
block_time - block_time % checkpoint_interval_milliseconds;

Expand Down Expand Up @@ -95,6 +105,13 @@ mod tests {
use dpp::version::PlatformVersion;
use std::collections::BTreeMap;

fn now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock is before the unix epoch")
.as_millis() as u64
}

fn make_block_execution_context(height: u64, block_time_ms: u64) -> BlockExecutionContext {
let platform_version = PlatformVersion::latest();
let platform_state =
Expand Down Expand Up @@ -158,7 +175,10 @@ mod tests {
return;
}

let block_execution_context = make_block_execution_context(1, 1_000_000);
// A block the network has just produced: checkpoints are restore points
// for a running node, so the age of the block decides whether one is worth
// taking, and a fixed fixture timestamp would read as ancient history.
let block_execution_context = make_block_execution_context(1, now_ms());
let result = platform
.should_checkpoint_v0(&block_execution_context, platform_version)
.expect("expected Ok");
Expand All @@ -167,6 +187,38 @@ mod tests {
assert!(result.is_some(), "first block should trigger checkpoint");
}

/// Replaying history, ten minutes of chain time is a handful of blocks, so a
/// checkpoint would be taken dozens of times a second and all but the last
/// few deleted again immediately. A node catching up takes none.
#[test]
fn test_historical_block_does_not_checkpoint() {
let platform_version = PlatformVersion::latest();
if platform_version
.drive_abci
.methods
.block_end
.should_checkpoint
.is_none()
{
return;
}

let platform = TestPlatformBuilder::new()
.build_with_mock_rpc()
.set_genesis_state();

let block_execution_context =
make_block_execution_context(1, now_ms() - 24 * 60 * 60 * 1000);
let result = platform
.should_checkpoint_v0(&block_execution_context, platform_version)
.expect("expected Ok");

assert!(
result.is_none(),
"a day-old block is being replayed, not followed"
);
}

#[test]
fn test_checkpoint_interval_zero_returns_none() {
let platform_version = PlatformVersion::latest();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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::<QuorumHash>(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::<QuorumHash>(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::<QuorumHash>(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
Expand Down Expand Up @@ -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

Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -12,23 +15,53 @@ impl<C> Platform<C> {
transaction: TransactionArg,
platform_version: &PlatformVersion,
) -> Result<Option<PlatformState>, 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))
}
}
Loading
Loading