Skip to content

Commit

Permalink
feat(debug): add chunk endorsement ratio to last_blocks debug page (n…
Browse files Browse the repository at this point in the history
…ear#11443)

Adds a new metric in the `last_blocks` debug page: `endorsement(stake)`.
This indicates what percentage of the 'validators stake' has endorsed a
given chunk in a block.

The addition works only if the stateless validation feature is enabled.
  • Loading branch information
Trisfald authored Jun 4, 2024
1 parent f7679a7 commit 44cb356
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 19 deletions.
2 changes: 2 additions & 0 deletions chain/client-primitives/src/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ pub struct DebugChunkStatus {
pub processing_time_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub congestion_info: Option<CongestionInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub endorsement_ratio: Option<f64>,
}

#[derive(serde::Serialize, serde::Deserialize, Debug)]
Expand Down
120 changes: 102 additions & 18 deletions chain/client/src/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::client_actor::ClientActorInner;
use near_async::messaging::Handler;
use near_async::time::{Clock, Instant};
use near_chain::crypto_hash_timer::CryptoHashTimer;
use near_chain::{near_chain_primitives, Chain, ChainStoreAccess};
use near_chain::{near_chain_primitives, Block, Chain, ChainStoreAccess};
use near_client_primitives::debug::{
ApprovalAtHeightStatus, BlockProduction, ChunkCollection, DebugBlockStatusData, DebugStatus,
DebugStatusResponse, MissedHeightInfo, ProductionAtHeight, ValidatorStatus,
Expand All @@ -19,6 +19,7 @@ use near_epoch_manager::EpochManagerAdapter;
use near_o11y::log_assert;
use near_performance_metrics_macros::perf;
use near_primitives::state_sync::get_num_state_parts;
use near_primitives::stateless_validation::ChunkEndorsement;
use near_primitives::types::{AccountId, BlockHeight, NumShards, ShardId, ValidatorInfoIdentifier};
use near_primitives::{
hash::CryptoHash,
Expand Down Expand Up @@ -443,28 +444,39 @@ impl ClientActorInner {
.get_block_producer(block_header.epoch_id(), block_header.height())
.ok();

let chunk_endorsements = self.compute_chunk_endorsements_ratio(&block);

let chunks = match &block {
Some(block) => block
.chunks()
.iter()
.map(|chunk| DebugChunkStatus {
shard_id: chunk.shard_id(),
chunk_hash: chunk.chunk_hash(),
chunk_producer: self
.client
.epoch_manager
.get_chunk_producer(
block_header.epoch_id(),
block_header.height(),
chunk.shard_id(),
.map(|chunk| {
let endorsement_ratio = chunk_endorsements
.as_ref()
.map(|chunks| chunks.get(&chunk.chunk_hash()))
.flatten()
.copied();

DebugChunkStatus {
shard_id: chunk.shard_id(),
chunk_hash: chunk.chunk_hash(),
chunk_producer: self
.client
.epoch_manager
.get_chunk_producer(
block_header.epoch_id(),
block_header.height(),
chunk.shard_id(),
)
.ok(),
gas_used: chunk.prev_gas_used(),
processing_time_ms: CryptoHashTimer::get_timer_value(
chunk.chunk_hash().0,
)
.ok(),
gas_used: chunk.prev_gas_used(),
processing_time_ms: CryptoHashTimer::get_timer_value(
chunk.chunk_hash().0,
)
.map(|s| s.whole_milliseconds() as u64),
congestion_info: chunk.congestion_info(),
.map(|s| s.whole_milliseconds() as u64),
congestion_info: chunk.congestion_info(),
endorsement_ratio,
}
})
.collect(),
None => vec![],
Expand Down Expand Up @@ -626,7 +638,79 @@ impl ClientActorInner {
.get_banned_chunk_producers(),
})
}

/// Computes the ratio of stake endorsed to all chunks in `block`.
/// The logic is based on `Chain::validate_chunk_endorsements_in_block`.
fn compute_chunk_endorsements_ratio(
&self,
block: &Option<Block>,
) -> Option<HashMap<ChunkHash, f64>> {
let Some(block) = block else {
return None;
};
let mut chunk_endorsements = HashMap::new();
if block.chunks().len() != block.chunk_endorsements().len() {
return None;
}
// Get the epoch id.
let Ok(epoch_id) =
self.client.epoch_manager.get_epoch_id_from_prev_block(block.header().prev_hash())
else {
return None;
};
// Iterate all shards and compute the endorsed stake from the endorsement signatures.
for (chunk_header, signatures) in block.chunks().iter().zip(block.chunk_endorsements()) {
// Validation checks.
if chunk_header.height_included() != block.header().height() {
chunk_endorsements.insert(chunk_header.chunk_hash(), 0.0);
continue;
}
let Ok(chunk_validator_assignments) =
self.client.epoch_manager.get_chunk_validator_assignments(
&epoch_id,
chunk_header.shard_id(),
chunk_header.height_created(),
)
else {
chunk_endorsements.insert(chunk_header.chunk_hash(), f64::NAN);
continue;
};
let ordered_chunk_validators = chunk_validator_assignments.ordered_chunk_validators();
if ordered_chunk_validators.len() != signatures.len() {
chunk_endorsements.insert(chunk_header.chunk_hash(), f64::NAN);
continue;
}
// Compute total stake and endorsed stake.
let mut endorsed_chunk_validators = HashSet::new();
for (account_id, signature) in ordered_chunk_validators.iter().zip(signatures) {
let Some(signature) = signature else { continue };
let Ok((validator, _)) = self.client.epoch_manager.get_validator_by_account_id(
&epoch_id,
block.header().prev_hash(),
account_id,
) else {
continue;
};
if !ChunkEndorsement::validate_signature(
chunk_header.chunk_hash(),
signature,
validator.public_key(),
) {
continue;
}
endorsed_chunk_validators.insert(account_id);
}
let endorsement_stats =
chunk_validator_assignments.compute_endorsement_stats(&endorsed_chunk_validators);
chunk_endorsements.insert(
chunk_header.chunk_hash(),
endorsement_stats.endorsed_stake as f64 / endorsement_stats.total_stake as f64,
);
}
Some(chunk_endorsements)
}
}

fn new_peer_info_view(chain: &Chain, connected_peer_info: &ConnectedPeerInfo) -> PeerInfoView {
let full_peer_info = &connected_peer_info.full_peer_info;
let now = Instant::now();
Expand Down
10 changes: 9 additions & 1 deletion chain/jsonrpc/res/last_blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ function ellipsify(str, maxLen) {
return str;
}

function toPercentage(number, fractionDigits) {
if (isNaN(number)) {
return number;
}
return (number * 100).toFixed(fractionDigits) + '%';
}

// Makes an element that when clicked, expands or ellipsifies the hash and creator.
function HashElement({ hashValue, creator, expandAll, knownProducers }) {
let [expanded, setExpanded] = React.useState(false);
Expand Down Expand Up @@ -132,7 +139,7 @@ function BlocksTable({ rows, knownProducers, expandAll, hideMissingHeights }) {
<th>Block Delay (s)</th>
<th>Gas price ratio</th>
{[...Array(numShards).keys()].map(i =>
<th key={i} colSpan="3">Shard {i} (hash/gas(Tgas)/time(ms))</th>)}
<th key={i} colSpan="4">Shard {i} (hash/gas(Tgas)/time(ms)/endorsement(stake))</th>)}
</tr>;

// One xarrow element per arrow (from block to block).
Expand Down Expand Up @@ -166,6 +173,7 @@ function BlocksTable({ rows, knownProducers, expandAll, hideMissingHeights }) {
</td>
<td>{(chunk.gas_used / (1024 * 1024 * 1024 * 1024)).toFixed(1)}</td>
<td>{chunk.processing_time_ms}</td>
<td>{toPercentage(chunk.endorsement_ratio, 1)}</td>
</React.Fragment>);
});

Expand Down

0 comments on commit 44cb356

Please sign in to comment.