From 37d3d63a23e82b59b4ceea52599b7848612bcc7e Mon Sep 17 00:00:00 2001 From: oliviasaa Date: Thu, 20 Nov 2025 09:59:35 +0000 Subject: [PATCH 1/8] feat(consensus): Introduce Scoring metric store (#9178) ```mermaid --- config: gitGraph: {parallelCommits: true, mainBranchName: 'Feature', rotateCommitLabel: true} --- gitGraph commit id: "Initial commit" commit id: "Old Scorer" tag: "PR8530" checkout Feature branch new-types commit id: "add ChangeEpochV4" checkout Feature branch misbehavior-reports commit id: "add MisbehaviorReports" checkout misbehavior-reports branch scoring-metrics commit id: "add ScoringMetrics" checkout scoring-metrics branch scorer commit id: "add Scorer" checkout scorer branch scoring-metrics-store commit id: "add ScoringMetricsStore" checkout scorer merge scoring-metrics-store tag: "THIS PR" ``` Introduces the `ScoringMetricStore`, which holds the validator scoring metrics to be updated by consensus - [x] Basic tests (linting, compilation, formatting, unit/integration tests) - [ ] Patch-specific tests (correctness, functionality coverage) - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have checked that new and existing unit tests pass locally with my changes - [x] Protocol: Introduce Scoring metric store - [ ] Nodes (Validators and Full nodes): - [ ] Indexer: - [ ] JSON-RPC: - [ ] GraphQL: - [ ] CLI: - [ ] Rust SDK: - [ ] REST API: remove unused code --- Cargo.lock | 1 + consensus/core/src/authority_node.rs | 29 +- consensus/core/src/commit_syncer.rs | 2 +- consensus/core/src/context.rs | 26 +- consensus/core/src/dag_state.rs | 38 +- consensus/core/src/lib.rs | 3 +- consensus/core/src/metrics.rs | 109 +-- .../{scorer.rs => scoring_metrics_store.rs} | 808 +++++++----------- consensus/core/src/subscriber.rs | 16 +- consensus/core/src/synchronizer.rs | 2 +- consensus/simtests/Cargo.toml | 1 + consensus/simtests/src/node.rs | 6 + crates/iota-common/src/lib.rs | 2 + .../authority/authority_per_epoch_store.rs | 2 +- .../consensus_manager/mysticeti_manager.rs | 1 + 15 files changed, 428 insertions(+), 618 deletions(-) rename consensus/core/src/{scorer.rs => scoring_metrics_store.rs} (82%) diff --git a/Cargo.lock b/Cargo.lock index c07d7ba3fce..47bc59f9b93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2702,6 +2702,7 @@ dependencies = [ "arc-swap", "consensus-config", "consensus-core", + "iota-common", "iota-config", "iota-macros", "iota-metrics", diff --git a/consensus/core/src/authority_node.rs b/consensus/core/src/authority_node.rs index 59b31e40ac2..6ce3f1d4287 100644 --- a/consensus/core/src/authority_node.rs +++ b/consensus/core/src/authority_node.rs @@ -5,6 +5,7 @@ use std::{sync::Arc, time::Instant}; use consensus_config::{AuthorityIndex, Committee, NetworkKeyPair, Parameters, ProtocolKeyPair}; +use iota_common::scoring_metrics::VersionedScoringMetrics; use iota_protocol_config::{ConsensusNetwork, ProtocolConfig}; use itertools::Itertools; use parking_lot::RwLock; @@ -29,7 +30,7 @@ use crate::{ metrics::initialise_metrics, network::{NetworkClient as _, NetworkManager, tonic_network::TonicManager}, round_prober::{RoundProber, RoundProberHandle}, - scorer::Scorer, + scoring_metrics_store::MysticetiScoringMetricsStore, storage::rocksdb_store::RocksDBStore, subscriber::Subscriber, synchronizer::{Synchronizer, SynchronizerHandle}, @@ -59,6 +60,7 @@ impl ConsensusAuthority { transaction_verifier: Arc, commit_consumer: CommitConsumer, registry: Registry, + current_local_metrics_count: Arc, // A counter that keeps track of how many times the authority node has been booted while // the binary or the component that is calling the `ConsensusAuthority` has been // running. It's mostly useful to make decisions on whether amnesia recovery should @@ -80,6 +82,7 @@ impl ConsensusAuthority { transaction_verifier, commit_consumer, registry, + current_local_metrics_count, boot_counter, ) .await; @@ -165,6 +168,7 @@ where transaction_verifier: Arc, commit_consumer: CommitConsumer, registry: Registry, + current_local_metrics_count: Arc, boot_counter: u64, ) -> Self { assert!( @@ -185,7 +189,13 @@ where ); info!("Consensus parameters: {:?}", parameters); info!("Consensus committee: {:?}", committee); - let scorer = Arc::new(Scorer::new(committee.size())); + + let scoring_metrics_store = Arc::new(MysticetiScoringMetricsStore::new( + committee.size(), + current_local_metrics_count, + &protocol_config, + )); + let context = Arc::new(Context::new( epoch_start_timestamp_ms, own_index, @@ -193,7 +203,7 @@ where parameters, protocol_config, initialise_metrics(registry), - scorer, + scoring_metrics_store, clock, )); let start_time = Instant::now(); @@ -467,6 +477,11 @@ mod tests { let (sender, _receiver) = unbounded_channel("consensus_output"); let commit_consumer = CommitConsumer::new(sender, 0); + let protocol_config = ProtocolConfig::get_for_max_version_UNSAFE(); + let current_local_metrics_count = Arc::new(VersionedScoringMetrics::new( + committee.size(), + &protocol_config, + )); let authority = ConsensusAuthority::start( network_type, @@ -474,13 +489,14 @@ mod tests { own_index, committee, parameters, - ProtocolConfig::get_for_max_version_UNSAFE(), + protocol_config, protocol_keypair, network_keypair, Arc::new(Clock::default()), Arc::new(txn_verifier), commit_consumer, registry, + current_local_metrics_count, 0, ) .await; @@ -867,6 +883,10 @@ mod tests { let (sender, receiver) = unbounded_channel("consensus_output"); let commit_consumer = CommitConsumer::new(sender, 0); + let current_local_metrics_count = Arc::new(VersionedScoringMetrics::new( + committee.size(), + &protocol_config, + )); let authority = ConsensusAuthority::start( network_type, @@ -881,6 +901,7 @@ mod tests { Arc::new(txn_verifier), commit_consumer, registry, + current_local_metrics_count, boot_counter, ) .await; diff --git a/consensus/core/src/commit_syncer.rs b/consensus/core/src/commit_syncer.rs index 957c1618137..1f82f690a88 100644 --- a/consensus/core/src/commit_syncer.rs +++ b/consensus/core/src/commit_syncer.rs @@ -512,7 +512,7 @@ impl CommitSyncer { .clone(); inner .context - .scorer + .scoring_metrics_store .update_scoring_metrics_on_block_receival( authority, hostname.as_str(), diff --git a/consensus/core/src/context.rs b/consensus/core/src/context.rs index 4e5c95a5758..c7c87157380 100644 --- a/consensus/core/src/context.rs +++ b/consensus/core/src/context.rs @@ -14,7 +14,9 @@ use tokio::time::Instant; #[cfg(test)] use crate::metrics::test_metrics; -use crate::{block::BlockTimestampMs, metrics::Metrics, scorer::Scorer}; +use crate::{ + block::BlockTimestampMs, metrics::Metrics, scoring_metrics_store::MysticetiScoringMetricsStore, +}; /// Context contains per-epoch configuration and metrics shared by all /// components of this authority. #[derive(Clone)] @@ -31,7 +33,8 @@ pub(crate) struct Context { pub protocol_config: ProtocolConfig, /// Metrics of this authority. pub metrics: Arc, - pub(crate) scorer: Arc, + /// Store for scoring metrics collected by this authority. + pub(crate) scoring_metrics_store: Arc, /// Access to local clock pub clock: Arc, } @@ -44,7 +47,8 @@ impl Context { parameters: Parameters, protocol_config: ProtocolConfig, metrics: Arc, - scorer: Arc, + scoring_metrics_store: Arc, + clock: Arc, ) -> Self { Self { @@ -54,7 +58,7 @@ impl Context { parameters, protocol_config, metrics, - scorer, + scoring_metrics_store, clock, } } @@ -64,13 +68,21 @@ impl Context { pub(crate) fn new_for_test( committee_size: usize, ) -> (Self, Vec<(NetworkKeyPair, ProtocolKeyPair)>) { + use iota_common::scoring_metrics::{ScoringMetricsV1, VersionedScoringMetrics}; + let (committee, keypairs) = consensus_config::local_committee_and_keys(0, vec![1; committee_size]); let metrics = test_metrics(); let temp_dir = TempDir::new().unwrap(); let clock = Arc::new(Clock::default()); - let scorer = Arc::new(Scorer::new_dummy_for_tests(committee_size)); - + let current_local_metrics_count = Arc::new(VersionedScoringMetrics::V1( + ScoringMetricsV1::new(committee_size), + )); + let scoring_metrics_store = Arc::new(MysticetiScoringMetricsStore::new( + committee_size, + current_local_metrics_count, + &ProtocolConfig::get_for_max_version_UNSAFE(), + )); let context = Context::new( 0, AuthorityIndex::new_for_test(0), @@ -81,7 +93,7 @@ impl Context { }, ProtocolConfig::get_for_max_version_UNSAFE(), metrics, - scorer, + scoring_metrics_store, clock, ); (context, keypairs) diff --git a/consensus/core/src/dag_state.rs b/consensus/core/src/dag_state.rs index 9e77b558db0..f6aa00130ba 100644 --- a/consensus/core/src/dag_state.rs +++ b/consensus/core/src/dag_state.rs @@ -249,13 +249,16 @@ impl DagState { // Initialize scoring metrics according to the metrics in store and the blocks // that were loaded to cache. let recovered_scoring_metrics = state.store.scan_scoring_metrics().expect("Database error"); - state.context.scorer.initialize_scoring_metrics( - recovered_scoring_metrics, - &state.recent_refs_by_authority, - state.threshold_clock_round(), - &state.evicted_rounds, - state.context.clone(), - ); + state + .context + .scoring_metrics_store + .initialize_scoring_metrics( + recovered_scoring_metrics, + &state.recent_refs_by_authority, + state.threshold_clock_round(), + &state.evicted_rounds, + state.context.clone(), + ); if state.gc_enabled() { if let Some(last_commit) = last_commit { @@ -1057,15 +1060,18 @@ impl DagState { for (authority_index, authority) in self.context.committee.authorities() { let last_eviction_round = self.evicted_rounds[authority_index]; let current_eviction_round = self.calculate_authority_eviction_round(authority_index); - let metrics_to_write_from_authority = self.context.scorer.update_score( - authority_index, - authority.hostname.as_str(), - &self.recent_refs_by_authority[authority_index], - current_eviction_round, - last_eviction_round, - threshold_clock_round, - &self.context.metrics.node_metrics, - ); + let metrics_to_write_from_authority = self + .context + .scoring_metrics_store + .update_scoring_metrics_on_eviction( + authority_index, + authority.hostname.as_str(), + &self.recent_refs_by_authority[authority_index], + current_eviction_round, + last_eviction_round, + threshold_clock_round, + &self.context.metrics.node_metrics, + ); if let Some(metrics_to_write_from_authority) = metrics_to_write_from_authority { metrics_to_write.push((authority_index, metrics_to_write_from_authority)); } diff --git a/consensus/core/src/lib.rs b/consensus/core/src/lib.rs index 88ee9750637..25f2c2b720b 100644 --- a/consensus/core/src/lib.rs +++ b/consensus/core/src/lib.rs @@ -30,7 +30,6 @@ mod network; #[cfg(msim)] pub mod network; -mod scorer; mod stake_aggregator; mod storage; mod subscriber; @@ -55,6 +54,8 @@ mod test_dag_builder; #[cfg(test)] mod test_dag_parser; +pub mod scoring_metrics_store; + /// Exported consensus API. pub use authority_node::ConsensusAuthority; pub use block::{BlockAPI, BlockRef, Round}; diff --git a/consensus/core/src/metrics.rs b/consensus/core/src/metrics.rs index d2e6cebf9a5..d8307f8403e 100644 --- a/consensus/core/src/metrics.rs +++ b/consensus/core/src/metrics.rs @@ -81,6 +81,7 @@ pub(crate) struct Metrics { pub(crate) node_metrics: NodeMetrics, pub(crate) network_metrics: NetworkMetrics, } + pub(crate) struct NodeMetrics { pub(crate) block_commit_latency: Histogram, pub(crate) proposed_blocks: IntCounterVec, @@ -133,7 +134,10 @@ pub(crate) struct NodeMetrics { pub(crate) uncached_missing_proposals_by_authority: IntCounterVec, pub(crate) equivocations_in_cache_by_authority: IntGaugeVec, pub(crate) missing_proposals_in_cache_by_authority: IntGaugeVec, + #[allow(dead_code)] pub(crate) score_by_authority: IntGaugeVec, + #[allow(dead_code)] + pub(crate) invalid_misbehavior_reports_by_authority: IntCounterVec, pub(crate) rejected_blocks: IntCounterVec, pub(crate) rejected_future_blocks: IntCounterVec, pub(crate) subscribed_blocks: IntCounterVec, @@ -522,6 +526,12 @@ impl NodeMetrics { &["authority"], registry, ).unwrap(), + invalid_misbehavior_reports_by_authority: register_int_counter_vec_with_registry!( + "invalid_misbehavior_reports_by_authority", + "Number of invalid misbehavior reports received from each authority", + &["authority"], + registry, + ).unwrap(), rejected_blocks: register_int_counter_vec_with_registry!( "rejected_blocks", "Number of blocks rejected before verifications", @@ -893,105 +903,6 @@ pub(crate) fn initialise_metrics(registry: Registry) -> Arc { }) } -// Given the set of blocks issued by an authority in rounds in the inclusive -// range [start, end], this function calculates and returns the number of -// equivocations and missing blocks in that range . The function should receive -// the vector with the rounds of such blocks and the range start and end points. -fn calculate_scoring_metrics_for_range( - mut block_rounds: Vec, - start: u32, - end: u32, -) -> (u64, u64) { - // Filter out rounds that are not in the range [start, end]. - block_rounds.retain(|&round| round >= start && round <= end); - let number_of_blocks = block_rounds.len(); - block_rounds.dedup(); - let unique_block_rounds = block_rounds.len(); - // We use saturating_sub to avoid unexpected underflows, but the subtractions - // below should never result in negative values by construction: - // 1) unique_block_rounds <= number_of_blocks - // 2) end - start + 1 >= unique_block_rounds - let number_of_equivocations = number_of_blocks.saturating_sub(unique_block_rounds) as u64; - let number_of_missing_blocks = - (end + 1).saturating_sub(start + unique_block_rounds as u32) as u64; - - (number_of_equivocations, number_of_missing_blocks) -} - -fn should_update_provable_metrics(error: &ConsensusError, source: &str) -> bool { - if source == "handle_send_block" - && (is_from_signed_block_verification(error) - || matches!( - error, - ConsensusError::BlockRejected { .. } //| ConsensusError::MalformedAncestorBlock { .. } - )) - { - return true; - } - false -} - -fn should_update_unprovable_metrics(error: &ConsensusError, source: &str) -> bool { - if source == "handle_send_block" { - return is_from_unsigned_block_verification(error) - || matches!( - error, - ConsensusError::MalformedBlock { .. } | ConsensusError::UnexpectedAuthority { .. } - ); - } else if source == "fetch_once" { - return is_from_commit_syncer(error); - } else if source == "process_fetched_blocks" { - return is_from_unsigned_block_verification(error) - || is_from_signed_block_verification(error) - || matches!(error, ConsensusError::MalformedBlock { .. }); - } - false -} - -fn is_from_unsigned_block_verification(err: &ConsensusError) -> bool { - matches!( - err, - ConsensusError::WrongEpoch { .. } - | ConsensusError::UnexpectedGenesisBlock - | ConsensusError::InvalidAuthorityIndex { .. } - | ConsensusError::SerializationFailure { .. } - | ConsensusError::MalformedSignature { .. } - | ConsensusError::SignatureVerificationFailure { .. } - ) -} - -fn is_from_signed_block_verification(err: &ConsensusError) -> bool { - matches!( - err, - ConsensusError::TooManyAncestors { .. } - | ConsensusError::InsufficientParentStakes { .. } - | ConsensusError::InvalidAuthorityIndex { .. } - | ConsensusError::InvalidAncestorPosition { .. } - | ConsensusError::InvalidAncestorRound { .. } - | ConsensusError::InvalidGenesisAncestor { .. } - | ConsensusError::DuplicatedAncestorsAuthority { .. } - | ConsensusError::TransactionTooLarge { .. } - | ConsensusError::TooManyTransactions { .. } - | ConsensusError::TooManyTransactionBytes { .. } - | ConsensusError::InvalidTransaction { .. } - ) -} - -fn is_from_commit_syncer(err: &ConsensusError) -> bool { - matches!( - err, - ConsensusError::MalformedCommit { .. } - | ConsensusError::UnexpectedStartCommit { .. } - | ConsensusError::UnexpectedCommitSequence { .. } - | ConsensusError::NoCommitReceived { .. } - | ConsensusError::MalformedBlock { .. } - | ConsensusError::NotEnoughCommitVotes { .. } - | ConsensusError::UnexpectedNumberOfBlocksFetched { .. } - | ConsensusError::UnexpectedBlockForCommit { .. } - ) || is_from_unsigned_block_verification(err) - || is_from_signed_block_verification(err) -} - #[cfg(test)] pub(crate) fn test_metrics() -> Arc { initialise_metrics(Registry::new()) diff --git a/consensus/core/src/scorer.rs b/consensus/core/src/scoring_metrics_store.rs similarity index 82% rename from consensus/core/src/scorer.rs rename to consensus/core/src/scoring_metrics_store.rs index 36a4020ec7f..dff1e24e5eb 100644 --- a/consensus/core/src/scorer.rs +++ b/consensus/core/src/scoring_metrics_store.rs @@ -3,176 +3,48 @@ use std::{ collections::BTreeSet, - sync::{ - Arc, - atomic::{AtomicU64, Ordering}, - }, + sync::{Arc, atomic::Ordering}, }; use consensus_config::AuthorityIndex; +use iota_common::scoring_metrics::{ScoringMetricsV1, VersionedScoringMetrics}; +use iota_protocol_config::ProtocolConfig; use itertools::izip; use crate::{ BlockRef, context::Context, error::ConsensusError, metrics::NodeMetrics, storage::StorageScoringMetrics, }; - -/// The Scorer holds the scoring metrics for all authorities in the committee, -/// which is updated according to the blocks received -/// and the evictions that happen in storage. It also holds the partial scores -/// for each authority, which are then added to EndOfPublishV2 and used to -/// calculate a final score. -pub struct Scorer { - scoring_metrics: ValidatorScoringMetrics, - partial_scores: PartialScores, +/// Struct that holds the scoring metrics for all authorities in the committee, +/// both cached and uncached. It also holds a shared reference to the current +/// local metrics count used by Scorer. +pub(crate) struct MysticetiScoringMetricsStore { + pub current_local_metrics_count: Arc, + pub cached_metrics: VersionedScoringMetrics, + pub uncached_metrics: VersionedScoringMetrics, } -impl Scorer { - pub fn new(committee_size: usize) -> Self { - Self { - scoring_metrics: ValidatorScoringMetrics::new(committee_size), - partial_scores: PartialScores::new(committee_size), - } - } - - #[allow(dead_code)] - pub fn get_provable_partial_scores(&self) -> &Vec { - &self.partial_scores.provable - } - - #[allow(dead_code)] - pub fn get_unprovable_partial_scores(&self) -> &Vec { - &self.partial_scores.unprovable - } - - pub(crate) fn update_score( - &self, - authority_index: AuthorityIndex, - hostname: &str, - recent_refs: &BTreeSet, - eviction_round: u32, - last_eviction_round: u32, - threshold_clock_round: u32, - node_metrics: &NodeMetrics, - ) -> Option { - // threshold_clock_round should be always at least 1. - // Analogously, authority_index should be a valid index. - if threshold_clock_round == 0 - || authority_index.value() >= self.scoring_metrics.cached.len() - { - return None; - } - - // Get the blocks rounds that were not evicted. - let cached_block_rounds = recent_refs - .iter() - .map(|block| block.round) - .filter(|&round| round > eviction_round && round < threshold_clock_round) - .collect::>(); - - // Update metrics according to the blocks from rounds still in cache. - let (cached_equivocations, missing_blocks_in_cached_rounds) = - calculate_scoring_metrics_for_range( - cached_block_rounds, - eviction_round + 1, - threshold_clock_round.saturating_sub(1), - ); - - self.scoring_metrics - .update_missing_blocks_and_equivocations( - missing_blocks_in_cached_rounds, - cached_equivocations, - hostname, - authority_index, - MetricType::Cached, - node_metrics, - ); - - // If no eviction happened, we do not update the metrics on storage. - if eviction_round == last_eviction_round { - return None; - } - - // Get the evicted blocks rounds. - let evicted_block_rounds = recent_refs - .iter() - .map(|block| block.round) - .filter(|&round| round <= eviction_round) - .collect::>(); - - // Update metrics according to the blocks from evicted rounds. - let (evicted_equivocations, missing_blocks_in_evicted_rounds) = - calculate_scoring_metrics_for_range( - evicted_block_rounds, - last_eviction_round + 1, - eviction_round, - ); - - self.scoring_metrics - .update_missing_blocks_and_equivocations( - missing_blocks_in_evicted_rounds, - evicted_equivocations, - hostname, - authority_index, - MetricType::Uncached, - node_metrics, - ); - - // Update score - self.update_authority_score(authority_index, hostname, node_metrics); - - Some(StorageScoringMetrics { - faulty_blocks_provable: self.scoring_metrics.uncached[authority_index] - .faulty_blocks_provable - .load(Ordering::Relaxed), - faulty_blocks_unprovable: self.scoring_metrics.uncached[authority_index] - .faulty_blocks_unprovable - .load(Ordering::Relaxed), - equivocations: self.scoring_metrics.uncached[authority_index] - .equivocations - .load(Ordering::Relaxed), - missing_proposals: self.scoring_metrics.uncached[authority_index] - .missing_proposals - .load(Ordering::Relaxed), - }) - } - - pub(crate) fn update_scoring_metrics_on_block_receival( - &self, - authority_index: AuthorityIndex, - hostname: &str, - error: ConsensusError, - source: &str, - node_metrics: &NodeMetrics, - ) { - // authority_index will be always a valid index. However, this method will - // panic if authority_index >= committee_size. We run this check only to avoid - // this panic. - if authority_index.value() >= self.scoring_metrics.cached.len() { - return; - } - - if should_update_provable_metrics(&error, source) { - self.scoring_metrics.uncached[authority_index] - .faulty_blocks_provable - .fetch_add(1, Ordering::Relaxed); - node_metrics - .faulty_blocks_provable_by_authority - .with_label_values(&[hostname, source, error.name()]) - .inc(); - } else if should_update_unprovable_metrics(&error, source) { - self.scoring_metrics.uncached[authority_index] - .faulty_blocks_unprovable - .fetch_add(1, Ordering::Relaxed); - node_metrics - .faulty_blocks_unprovable_by_authority - .with_label_values(&[hostname, source, error.name()]) - .inc(); - } else { - // No scoring metrics to update. +impl MysticetiScoringMetricsStore { + pub(crate) fn new( + committee_size: usize, + current_local_metrics_count: Arc, + protocol_config: &ProtocolConfig, + ) -> Self { + match protocol_config.scorer_version_as_option() { + None | Some(1) => Self { + current_local_metrics_count, + cached_metrics: VersionedScoringMetrics::V1(ScoringMetricsV1::new(committee_size)), + + uncached_metrics: VersionedScoringMetrics::V1(ScoringMetricsV1::new( + committee_size, + )), + }, + _ => panic!("Unsupported scorer version"), } } + // Initializes the scoring metrics store according to the + // recovered_scoring_metrics and blocks_in_cache_by_authority. pub(crate) fn initialize_scoring_metrics( &self, mut recovered_scoring_metrics: Vec<(AuthorityIndex, StorageScoringMetrics)>, @@ -191,7 +63,6 @@ impl Scorer { // component for every authority. A perfectly functioning validator, for // example, will never have its metrics updated, so no metric will ever be // stored. For this reason, we manually "fill" this vector. - if recovered_scoring_metrics.len() < context.committee.size() { for i in 0..context.committee.size() { if !recovered_scoring_metrics @@ -200,9 +71,9 @@ impl Scorer { { // We add a component with zeroed metrics for the authority with index i. // This will ensure that every authority has its metrics initialized. - // Note that this is correct, as if an authority does not have any - // recovered metrics, it means that it never misbehaved in a way that - // was detected by the node. + // They are initialized as zero because if an authority does not have any + // recovered metrics, it means that it never misbehaved in a way that was + // detected by the node. recovered_scoring_metrics.insert( i, ( @@ -232,22 +103,21 @@ impl Scorer { equivocations, missing_proposals, } = metrics; - self.scoring_metrics.initialize_faulty_blocks_metrics( + self.initialize_faulty_blocks_metrics( faulty_blocks_provable, faulty_blocks_unprovable, hostname, authority_index, &context.metrics.node_metrics, ); - self.scoring_metrics - .update_missing_blocks_and_equivocations( - missing_proposals, - equivocations, - hostname, - authority_index, - MetricType::Uncached, - &context.metrics.node_metrics, - ); + self.update_missing_blocks_and_equivocations( + missing_proposals, + equivocations, + hostname, + authority_index, + MetricType::Uncached, + &context.metrics.node_metrics, + ); // Initialize the cached scoring metrics according to blocks_in_cache. let block_rounds_in_cache = blocks_in_cache @@ -260,96 +130,84 @@ impl Scorer { eviction_round + 1, threshold_clock_round - 1, ); - self.scoring_metrics - .update_missing_blocks_and_equivocations( - missing_blocks_in_cached_rounds, - cached_equivocations, - hostname, - authority_index, - MetricType::Cached, - &context.metrics.node_metrics, - ); - // Initialize score - self.update_authority_score(authority_index, hostname, &context.metrics.node_metrics); + self.update_missing_blocks_and_equivocations( + missing_blocks_in_cached_rounds, + cached_equivocations, + hostname, + authority_index, + MetricType::Cached, + &context.metrics.node_metrics, + ); } } - // Auxiliary function used to update scores. The `authority` parameter should be - // a valid index, otherwise the function will panic. This check is not - // performed here, as it is assumed that the caller has already checked it. - fn update_authority_score( + // Updates the scoring metrics according to the received block's + // authority and error encountered during its processing. + pub(crate) fn update_scoring_metrics_on_block_receival( &self, - authority: AuthorityIndex, + authority_index: AuthorityIndex, hostname: &str, + error: ConsensusError, + source: &str, node_metrics: &NodeMetrics, ) { - let (faulty_blocks_provable, faulty_blocks_unprovable, equivocations, missing_proposals) = ( - self.scoring_metrics.uncached[authority] - .faulty_blocks_provable - .load(Ordering::Relaxed), - self.scoring_metrics.uncached[authority] - .faulty_blocks_unprovable - .load(Ordering::Relaxed), - self.scoring_metrics.uncached[authority] - .equivocations - .load(Ordering::Relaxed), - self.scoring_metrics.uncached[authority] - .missing_proposals - .load(Ordering::Relaxed), - ); + // authority_index will be always a valid index. However, this method will + // panic if authority_index >= committee_size. We run this check only to avoid + // this panic. + if authority_index.value() >= self.cached_metrics.faulty_blocks_provable().len() { + return; + } - // We provisionally use the formula below to calculate the score, but changes - // to this function are already expected. The hardcoded parameters (which are - // also still provisional) represent: - // - No tolerance to any provably faulty misbehavior. If any is detected, the - // score will be 0. - // - If no provably faulty blocks are detected, 50% of the score is guaranteed. - // - If no unprovably faulty blocks are detected, an additional 12.5% of the - // score is guaranteed. - // - If no missing proposals are detected, an additional 37.5% of the score is - // guaranteed. - // - The maximum achievable score is u32::MAX. - - if faulty_blocks_provable > 0 || equivocations > 0 { - self.partial_scores.unprovable[authority].store(0, Ordering::Relaxed); + if should_update_provable_metrics(&error, source) { + self.uncached_metrics + .increment_faulty_blocks_provable(authority_index.value(), 1); node_metrics - .score_by_authority - .with_label_values(&[hostname]) - .set(0i64); - } else { - let score = (2 << 31) - 1 - + (3 * (2 << 29) / (missing_proposals.saturating_add(1)) - + (2 << 29) / (faulty_blocks_unprovable.saturating_add(1))); - self.partial_scores.unprovable[authority].store(score, Ordering::Relaxed); + .faulty_blocks_provable_by_authority + .with_label_values(&[hostname, source, error.name()]) + .inc(); + } else if should_update_unprovable_metrics(&error, source) { + self.uncached_metrics + .increment_faulty_blocks_unprovable(authority_index.value(), 1); node_metrics - .score_by_authority - .with_label_values(&[hostname]) - .set(score as i64); + .faulty_blocks_unprovable_by_authority + .with_label_values(&[hostname, source, error.name()]) + .inc(); + } else { + // No scoring metrics need to be updated. } } -} -pub(crate) struct ValidatorScoringMetrics { - pub(crate) uncached: Vec, - pub(crate) cached: Vec, -} - -impl ValidatorScoringMetrics { - pub(crate) fn new(committee_size: usize) -> Self { - let uncached = (0..committee_size) - .map(|_| UncachedScoringMetrics::new()) - .collect(); - let cached = (0..committee_size) - .map(|_| CachedScoringMetrics::new()) - .collect(); - Self { uncached, cached } + // Auxiliary function to initialize scoring metrics relative to faulty blocks. + // The `authority` parameter should be a valid index, otherwise the function + // will panic. This check is not performed here, as it is assumed that the + // caller has already checked it. + pub(crate) fn initialize_faulty_blocks_metrics( + &self, + faulty_blocks_provable: u64, + faulty_blocks_unprovable: u64, + hostname: &str, + authority_index: AuthorityIndex, + node_metrics: &NodeMetrics, + ) { + node_metrics + .faulty_blocks_provable_by_authority + .with_label_values(&[hostname, "loaded from storage", "loaded from storage"]) + .inc_by(faulty_blocks_provable); + node_metrics + .faulty_blocks_unprovable_by_authority + .with_label_values(&[hostname, "loaded from storage", "loaded from storage"]) + .inc_by(faulty_blocks_unprovable); + self.uncached_metrics + .store_faulty_blocks_provable(authority_index.value(), faulty_blocks_provable); + self.uncached_metrics + .store_faulty_blocks_unprovable(authority_index.value(), faulty_blocks_unprovable); } // Auxiliary function to update scoring metrics relative to missing blocks // and equivocations. The `authority` parameter should be a valid index, // otherwise the function will panic. This check is not performed here, as // it is assumed that the caller has already checked it. - fn update_missing_blocks_and_equivocations( + pub(crate) fn update_missing_blocks_and_equivocations( &self, missing_blocks: u64, equivocations: u64, @@ -360,12 +218,10 @@ impl ValidatorScoringMetrics { ) { match metric_type { MetricType::Cached => { - self.cached[authority] - .equivocations - .store(equivocations, Ordering::Relaxed); - self.cached[authority] - .missing_proposals - .store(missing_blocks, Ordering::Relaxed); + self.cached_metrics + .store_equivocations(authority.value(), equivocations); + self.cached_metrics + .store_missing_proposals(authority.value(), missing_blocks); node_metrics .equivocations_in_cache_by_authority .with_label_values(&[hostname]) @@ -377,12 +233,10 @@ impl ValidatorScoringMetrics { } MetricType::Uncached => { - self.uncached[authority] - .equivocations - .fetch_add(equivocations, Ordering::Relaxed); - self.uncached[authority] - .missing_proposals - .fetch_add(missing_blocks, Ordering::Relaxed); + self.uncached_metrics + .increment_equivocations(authority.value(), equivocations); + self.uncached_metrics + .increment_missing_proposals(authority.value(), missing_blocks); node_metrics .uncached_equivocations_by_authority .with_label_values(&[hostname]) @@ -395,102 +249,115 @@ impl ValidatorScoringMetrics { } } - // Auxiliary function to initialize scoring metrics relative to faulty blocks. - // The `authority` parameter should be a valid index, otherwise the function - // will panic. This check is not performed here, as it is assumed that the - // caller has already checked it. - fn initialize_faulty_blocks_metrics( + // Updates the authority's scoring metrics according to the recent changes in + // the DAG state, i.e., recent evictions and additions to cache. It also + // updates the current local metrics count used by Scorer. It returns metrics + // changes that should be updated in disk storage. + pub(crate) fn update_scoring_metrics_on_eviction( &self, - faulty_blocks_provable: u64, - faulty_blocks_unprovable: u64, - hostname: &str, authority_index: AuthorityIndex, + hostname: &str, + recent_refs: &BTreeSet, + eviction_round: u32, + last_eviction_round: u32, + threshold_clock_round: u32, node_metrics: &NodeMetrics, - ) { - node_metrics - .faulty_blocks_provable_by_authority - .with_label_values(&[hostname, "loaded from storage", "loaded from storage"]) - .inc_by(faulty_blocks_provable); - node_metrics - .faulty_blocks_unprovable_by_authority - .with_label_values(&[hostname, "loaded from storage", "loaded from storage"]) - .inc_by(faulty_blocks_unprovable); - self.uncached[authority_index] - .faulty_blocks_provable - .store(faulty_blocks_provable, Ordering::Relaxed); - self.uncached[authority_index] - .faulty_blocks_unprovable - .store(faulty_blocks_unprovable, Ordering::Relaxed); - } -} + ) -> Option { + // threshold_clock_round should be always at least 1. + // Analogously, authority_index should be a valid index. + if threshold_clock_round == 0 + || authority_index.value() >= self.uncached_metrics.faulty_blocks_provable().len() + { + return None; + } -enum MetricType { - Cached, - Uncached, -} -// pub struct PartialScore(pub AtomicU64); -pub type PartialScore = AtomicU64; - -#[derive(Debug)] -pub(crate) struct UncachedScoringMetrics { - // Counts the number of times that a faulty block signed by the validator was already verified - // in the epoch. - pub(crate) faulty_blocks_provable: AtomicU64, - // Counts the number of times that a faulty block not signed by the validator was already - // verified in the epoch. - pub(crate) faulty_blocks_unprovable: AtomicU64, - // Counts the number of equivocations that were already evicted from cache in the epoch. - pub(crate) equivocations: AtomicU64, - // Counts the number of blocks that the validator failed to propose, or that the node did not - // receive, from the rounds already evicted from cache in the epoch. - pub(crate) missing_proposals: AtomicU64, -} + // Get the blocks rounds that were not evicted. + let cached_block_rounds = recent_refs + .iter() + .map(|block| block.round) + .filter(|&round| round > eviction_round && round < threshold_clock_round) + .collect::>(); -impl UncachedScoringMetrics { - pub(crate) fn new() -> Self { - Self { - faulty_blocks_provable: AtomicU64::new(0), - faulty_blocks_unprovable: AtomicU64::new(0), - equivocations: AtomicU64::new(0), - missing_proposals: AtomicU64::new(0), - } - } -} + // Update metrics according to the blocks from rounds still in cache. + let (cached_equivocations, missing_blocks_in_cached_rounds) = + calculate_scoring_metrics_for_range( + cached_block_rounds, + eviction_round + 1, + threshold_clock_round.saturating_sub(1), + ); -pub(crate) struct CachedScoringMetrics { - // Counts the number of equivocations in cache, below the threshold clock round. - pub(crate) equivocations: AtomicU64, - // Counts the number of blocks that the validator failed to propose, or that the node did not - // receive yet, from the rounds stored in cache and below the threshold clock round. - pub(crate) missing_proposals: AtomicU64, -} + self.update_missing_blocks_and_equivocations( + missing_blocks_in_cached_rounds, + cached_equivocations, + hostname, + authority_index, + MetricType::Cached, + node_metrics, + ); -impl CachedScoringMetrics { - pub(crate) fn new() -> Self { - Self { - equivocations: AtomicU64::new(0), - missing_proposals: AtomicU64::new(0), + // If no eviction happened, we do not update the metrics on storage. + if eviction_round == last_eviction_round { + return None; } - } -} -pub struct PartialScores { - pub provable: Vec, - pub unprovable: Vec, -} + // Get the evicted blocks rounds. + let evicted_block_rounds = recent_refs + .iter() + .map(|block| block.round) + .filter(|&round| round <= eviction_round) + .collect::>(); -impl PartialScores { - pub fn new(committee_size: usize) -> Self { - let provable = (0..committee_size) - .map(|_| AtomicU64::new(u64::MAX)) - .collect(); - let unprovable = (0..committee_size) - .map(|_| AtomicU64::new(u64::MAX)) - .collect(); - Self { - provable, - unprovable, - } + // Update metrics according to the blocks from evicted rounds. + let (evicted_equivocations, missing_blocks_in_evicted_rounds) = + calculate_scoring_metrics_for_range( + evicted_block_rounds, + last_eviction_round + 1, + eviction_round, + ); + + self.update_missing_blocks_and_equivocations( + missing_blocks_in_evicted_rounds, + evicted_equivocations, + hostname, + authority_index, + MetricType::Uncached, + node_metrics, + ); + + // Update current local metrics count. + self.update_current_local_metrics_count(authority_index); + + Some(StorageScoringMetrics { + faulty_blocks_provable: self.uncached_metrics.faulty_blocks_provable()[authority_index] + .load(Ordering::Relaxed), + faulty_blocks_unprovable: self.uncached_metrics.faulty_blocks_unprovable() + [authority_index] + .load(Ordering::Relaxed), + equivocations: self.uncached_metrics.equivocations()[authority_index] + .load(Ordering::Relaxed), + missing_proposals: self.uncached_metrics.missing_proposals()[authority_index] + .load(Ordering::Relaxed), + }) + } + + pub(crate) fn update_current_local_metrics_count(&self, authority_index: AuthorityIndex) { + let faulty_blocks_provable = + self.uncached_metrics.faulty_blocks_provable()[authority_index].load(Ordering::Relaxed); + let faulty_blocks_unprovable = self.uncached_metrics.faulty_blocks_unprovable() + [authority_index] + .load(Ordering::Relaxed); + let equivocations = + self.uncached_metrics.equivocations()[authority_index].load(Ordering::Relaxed); + let missing_proposals = + self.uncached_metrics.missing_proposals()[authority_index].load(Ordering::Relaxed); + self.current_local_metrics_count + .store_faulty_blocks_provable(authority_index.value(), faulty_blocks_provable); + self.current_local_metrics_count + .store_faulty_blocks_unprovable(authority_index.value(), faulty_blocks_unprovable); + self.current_local_metrics_count + .store_equivocations(authority_index.value(), equivocations); + self.current_local_metrics_count + .store_missing_proposals(authority_index.value(), missing_proposals); } } @@ -594,20 +461,14 @@ fn is_from_commit_syncer(err: &ConsensusError) -> bool { || is_from_signed_block_verification(err) } -#[cfg(test)] -impl Scorer { - pub(crate) fn new_dummy_for_tests(committee_size: usize) -> Self { - Self::new(committee_size) - } +pub(crate) enum MetricType { + Cached, + Uncached, } #[cfg(test)] mod tests { - use std::{ - collections::BTreeSet, - sync::{Arc, atomic::Ordering}, - vec, - }; + use std::{collections::BTreeSet, sync::Arc, vec}; use consensus_config::{AuthorityIndex, NetworkKeyPair, ProtocolKeyPair}; use parking_lot::RwLock; @@ -625,7 +486,7 @@ mod tests { context::Context, dag_state::DagState, error::ConsensusError, - scorer::ValidatorScoringMetrics, + scoring_metrics_store::MysticetiScoringMetricsStore, storage::{StorageScoringMetrics, mem_store::MemStore}, synchronizer::Synchronizer, test_dag_builder::DagBuilder, @@ -682,47 +543,29 @@ mod tests { (keys, context, core_dispatcher, authority_service) } - impl ValidatorScoringMetrics { + impl MysticetiScoringMetricsStore { pub(crate) fn uncached_missing_proposals_by_authority(&self) -> Vec { - self.uncached - .iter() - .map(|metrics| metrics.missing_proposals.load(Ordering::Relaxed)) - .collect() + self.uncached_metrics.load_missing_proposals() } pub(crate) fn equivocations_in_cache_by_authority(&self) -> Vec { - self.cached - .iter() - .map(|metrics| metrics.equivocations.load(Ordering::Relaxed)) - .collect() + self.cached_metrics.load_equivocations() } pub(crate) fn missing_proposals_in_cache_by_authority(&self) -> Vec { - self.cached - .iter() - .map(|metrics| metrics.missing_proposals.load(Ordering::Relaxed)) - .collect() + self.cached_metrics.load_missing_proposals() } pub(crate) fn uncached_equivocations_by_authority(&self) -> Vec { - self.uncached - .iter() - .map(|metrics| metrics.equivocations.load(Ordering::Relaxed)) - .collect() + self.uncached_metrics.load_equivocations() } pub(crate) fn faulty_blocks_provable_by_authority(&self) -> Vec { - self.uncached - .iter() - .map(|metrics| metrics.faulty_blocks_provable.load(Ordering::Relaxed)) - .collect() + self.uncached_metrics.load_faulty_blocks_provable() } pub(crate) fn faulty_blocks_unprovable_by_authority(&self) -> Vec { - self.uncached - .iter() - .map(|metrics| metrics.faulty_blocks_unprovable.load(Ordering::Relaxed)) - .collect() + self.uncached_metrics.load_faulty_blocks_unprovable() } } @@ -812,6 +655,7 @@ mod tests { } metrics } + fn get_faulty_blocks_unprovable(context: &Arc, source: &str, error: &str) -> Vec { let mut metrics = Vec::new(); for authority in context.committee.authorities() { @@ -830,9 +674,9 @@ mod tests { } #[tokio::test] - async fn test_update_score_edge_cases() { + async fn test_update_scoring_metrics_on_eviction_edge_cases() { let context = Context::new_for_test(4); - let scorer = context.0.scorer; + let scoring_metrics_store = context.0.scoring_metrics_store; let authority_index = AuthorityIndex::new_for_test(0); let hostname = "test_host"; let recent_refs_by_authority = BTreeSet::new(); @@ -849,7 +693,7 @@ mod tests { let last_evicted_round = 5; let eviction_round = 5; let threshold_clock_round = 5; - let stored_metrics = scorer.update_score( + let stored_metrics = scoring_metrics_store.update_scoring_metrics_on_eviction( authority_index, hostname, &recent_refs_by_authority, @@ -865,7 +709,7 @@ mod tests { let last_evicted_round = 0; let eviction_round = 0; let threshold_clock_round = 0; - let stored_metrics = scorer.update_score( + let stored_metrics = scoring_metrics_store.update_scoring_metrics_on_eviction( authority_index, hostname, &recent_refs_by_authority, @@ -882,7 +726,7 @@ mod tests { let last_evicted_round = 0; let eviction_round = 3; let threshold_clock_round = 2; - let stored_metrics = scorer.update_score( + let stored_metrics = scoring_metrics_store.update_scoring_metrics_on_eviction( authority_index, hostname, &recent_refs_by_authority, @@ -908,7 +752,7 @@ mod tests { let last_evicted_round = 1; let eviction_round = 0; let threshold_clock_round = 2; - let stored_metrics = scorer.update_score( + let stored_metrics = scoring_metrics_store.update_scoring_metrics_on_eviction( authority_index, hostname, &recent_refs_by_authority, @@ -933,7 +777,7 @@ mod tests { let last_evicted_round = 2; let eviction_round = 0; let threshold_clock_round = 1; - let stored_metrics = scorer.update_score( + let stored_metrics = scoring_metrics_store.update_scoring_metrics_on_eviction( authority_index, hostname, &recent_refs_by_authority, @@ -958,7 +802,7 @@ mod tests { let last_evicted_round = 1; let eviction_round = 2; let threshold_clock_round = 0; - let stored_metrics = scorer.update_score( + let stored_metrics = scoring_metrics_store.update_scoring_metrics_on_eviction( authority_index, hostname, &recent_refs_by_authority, @@ -972,7 +816,7 @@ mod tests { let last_evicted_round = 2; let eviction_round = 1; let threshold_clock_round = 0; - let stored_metrics = scorer.update_score( + let stored_metrics = scoring_metrics_store.update_scoring_metrics_on_eviction( authority_index, hostname, &recent_refs_by_authority, @@ -992,7 +836,7 @@ mod tests { let last_evicted_round = 1; let eviction_round = 2; let threshold_clock_round = 3; - let stored_metrics = scorer.update_score( + let stored_metrics = scoring_metrics_store.update_scoring_metrics_on_eviction( out_of_bounds_authority_index, hostname, &recent_refs_by_authority, @@ -1028,7 +872,7 @@ mod tests { .authorities() .map(|a| a.1.hostname.as_str()) .collect(); - let scoring_metrics = &context.scorer.scoring_metrics; + let scoring_metrics = &context.scoring_metrics_store; let node_metrics = &context.metrics.node_metrics; let store = Arc::new(MemStore::new()); let mut dag_state = DagState::new(context.clone(), store.clone()); @@ -1148,12 +992,8 @@ mod tests { ); // Clear and check all metrics - scoring_metrics.uncached[0] - .missing_proposals - .store(0, Ordering::Relaxed); - scoring_metrics.cached[0] - .missing_proposals - .store(0, Ordering::Relaxed); + scoring_metrics.uncached_metrics.reset(); + scoring_metrics.cached_metrics.reset(); node_metrics .uncached_missing_proposals_by_authority .with_label_values(&[hostnames[0]]) @@ -1268,12 +1108,8 @@ mod tests { } // Clear and check all metrics - scoring_metrics.uncached[0] - .missing_proposals - .store(0, Ordering::Relaxed); - scoring_metrics.cached[1] - .equivocations - .store(0, Ordering::Relaxed); + scoring_metrics.uncached_metrics.reset(); + scoring_metrics.cached_metrics.reset(); node_metrics .uncached_missing_proposals_by_authority .with_label_values(&[hostnames[0]]) @@ -1368,7 +1204,6 @@ mod tests { ); } - #[ignore] #[tokio::test] async fn test_metrics_flush_and_recovery() { telemetry_subscribers::init_for_testing(); @@ -1386,6 +1221,9 @@ mod tests { context .protocol_config .set_consensus_linearize_subdag_v2_for_testing(false); + context + .protocol_config + .set_consensus_median_timestamp_with_checkpoint_enforcement_for_testing(false); let context = Arc::new(context); let hostnames: Vec<&str> = context @@ -1393,7 +1231,7 @@ mod tests { .authorities() .map(|a| a.1.hostname.as_str()) .collect(); - let scoring_metrics = &context.scorer.scoring_metrics; + let scoring_metrics = &context.scoring_metrics_store; let node_metrics = &context.metrics.node_metrics; let store = Arc::new(MemStore::new()); @@ -1515,12 +1353,8 @@ mod tests { ); // Clear and check all metrics - scoring_metrics.uncached[0] - .missing_proposals - .store(0, Ordering::Relaxed); - scoring_metrics.cached[0] - .missing_proposals - .store(0, Ordering::Relaxed); + scoring_metrics.uncached_metrics.reset(); + scoring_metrics.cached_metrics.reset(); node_metrics .uncached_missing_proposals_by_authority .with_label_values(&[hostnames[0]]) @@ -1633,15 +1467,9 @@ mod tests { } // Clear and check all metrics - scoring_metrics.uncached[0] - .missing_proposals - .store(0, Ordering::Relaxed); - scoring_metrics.cached[1] - .equivocations - .store(0, Ordering::Relaxed); - scoring_metrics.cached[0] - .missing_proposals - .store(0, Ordering::Relaxed); + scoring_metrics.uncached_metrics.reset(); + scoring_metrics.cached_metrics.reset(); + scoring_metrics.cached_metrics.reset(); node_metrics .uncached_missing_proposals_by_authority .with_label_values(&[hostnames[0]]) @@ -1745,7 +1573,7 @@ mod tests { // Initialize context and authority service given a committee_size let committee_size = 4; let (_, context, _, _) = new_authority_service_for_metrics_tests(committee_size); - let scoring_metrics = &context.scorer.scoring_metrics; + let scoring_metrics = &context.scoring_metrics_store; let source = "handle_send_block"; // Create a set of errors to test let ignored_error = ConsensusError::Shutdown; @@ -1758,13 +1586,15 @@ mod tests { // Update metrics for each authority with an error that should be ignored. // Metrics should not be updated for this error. for authority in context.committee.authorities() { - context.scorer.update_scoring_metrics_on_block_receival( - authority.0, - authority.1.hostname.as_str(), - ignored_error.clone(), - source, - &context.metrics.node_metrics, - ); + context + .scoring_metrics_store + .update_scoring_metrics_on_block_receival( + authority.0, + authority.1.hostname.as_str(), + ignored_error.clone(), + source, + &context.metrics.node_metrics, + ); } assert_eq!( [ @@ -1792,13 +1622,15 @@ mod tests { // Update metrics for each authority with a parsing error. // Only unprovable metrics should be updated for this error. for authority in context.committee.authorities() { - context.scorer.update_scoring_metrics_on_block_receival( - authority.0, - authority.1.hostname.as_str(), - parsing_error.clone(), - source, - &context.metrics.node_metrics, - ); + context + .scoring_metrics_store + .update_scoring_metrics_on_block_receival( + authority.0, + authority.1.hostname.as_str(), + parsing_error.clone(), + source, + &context.metrics.node_metrics, + ); } assert_eq!( [ @@ -1826,13 +1658,15 @@ mod tests { // Update metrics for each authority with a signed block verification error. // Only provable metrics should be updated for this error. for authority in context.committee.authorities() { - context.scorer.update_scoring_metrics_on_block_receival( - authority.0, - authority.1.hostname.as_str(), - block_verification_error.clone(), - source, - &context.metrics.node_metrics, - ); + context + .scoring_metrics_store + .update_scoring_metrics_on_block_receival( + authority.0, + authority.1.hostname.as_str(), + block_verification_error.clone(), + source, + &context.metrics.node_metrics, + ); } assert_eq!( [ @@ -1863,7 +1697,7 @@ mod tests { // Initialize context and authority service given a committee_size let committee_size = 4; let (_, context, _, _) = new_authority_service_for_metrics_tests(committee_size); - let scoring_metrics = &context.scorer.scoring_metrics; + let scoring_metrics = &context.scoring_metrics_store; let source = "fetch_once"; // Create a set of errors to test let ignored_error = ConsensusError::Shutdown; @@ -1873,13 +1707,15 @@ mod tests { // Update metrics for each authority with an error that should be ignored. // Metrics should not be updated for this error. for authority in context.committee.authorities() { - context.scorer.update_scoring_metrics_on_block_receival( - authority.0, - authority.1.hostname.as_str(), - ignored_error.clone(), - source, - &context.metrics.node_metrics, - ); + context + .scoring_metrics_store + .update_scoring_metrics_on_block_receival( + authority.0, + authority.1.hostname.as_str(), + ignored_error.clone(), + source, + &context.metrics.node_metrics, + ); } assert_eq!( [ @@ -1907,13 +1743,15 @@ mod tests { // Update metrics for each authority with a parsing error. // Only unprovable metrics should be updated for this error. for authority in context.committee.authorities() { - context.scorer.update_scoring_metrics_on_block_receival( - authority.0, - authority.1.hostname.as_str(), - parsing_error.clone(), - source, - &context.metrics.node_metrics, - ); + context + .scoring_metrics_store + .update_scoring_metrics_on_block_receival( + authority.0, + authority.1.hostname.as_str(), + parsing_error.clone(), + source, + &context.metrics.node_metrics, + ); } assert_eq!( [ @@ -1943,13 +1781,15 @@ mod tests { // necessarily from the peer. Thus, it is not provable that the peer actually // sent this block. Only unprovable metrics should be updated for this error. for authority in context.committee.authorities() { - context.scorer.update_scoring_metrics_on_block_receival( - authority.0, - authority.1.hostname.as_str(), - block_verification_error.clone(), - source, - &context.metrics.node_metrics, - ); + context + .scoring_metrics_store + .update_scoring_metrics_on_block_receival( + authority.0, + authority.1.hostname.as_str(), + block_verification_error.clone(), + source, + &context.metrics.node_metrics, + ); } assert_eq!( [ @@ -1980,7 +1820,7 @@ mod tests { // Initialize context and authority service given a committee_size let committee_size = 4; let (_, context, _, _) = new_authority_service_for_metrics_tests(committee_size); - let scoring_metrics = &context.scorer.scoring_metrics; + let scoring_metrics = &context.scoring_metrics_store; let source = "process_fetched_blocks"; // Create a set of errors to test let ignored_error = ConsensusError::Shutdown; @@ -1990,13 +1830,15 @@ mod tests { // Update metrics for each authority with an error that should be ignored. // Metrics should not be updated for this error. for authority in context.committee.authorities() { - context.scorer.update_scoring_metrics_on_block_receival( - authority.0, - authority.1.hostname.as_str(), - ignored_error.clone(), - source, - &context.metrics.node_metrics, - ); + context + .scoring_metrics_store + .update_scoring_metrics_on_block_receival( + authority.0, + authority.1.hostname.as_str(), + ignored_error.clone(), + source, + &context.metrics.node_metrics, + ); } assert_eq!( [ @@ -2024,13 +1866,15 @@ mod tests { // Update metrics for each authority with a parsing error. // Only unprovable metrics should be updated for this error. for authority in context.committee.authorities() { - context.scorer.update_scoring_metrics_on_block_receival( - authority.0, - authority.1.hostname.as_str(), - parsing_error.clone(), - source, - &context.metrics.node_metrics, - ); + context + .scoring_metrics_store + .update_scoring_metrics_on_block_receival( + authority.0, + authority.1.hostname.as_str(), + parsing_error.clone(), + source, + &context.metrics.node_metrics, + ); } assert_eq!( [ @@ -2060,13 +1904,15 @@ mod tests { // necessarily from the peer. Thus, it is not provable that the peer actually // sent this block. Only unprovable metrics should be updated for this error. for authority in context.committee.authorities() { - context.scorer.update_scoring_metrics_on_block_receival( - authority.0, - authority.1.hostname.as_str(), - block_verification_error.clone(), - source, - &context.metrics.node_metrics, - ); + context + .scoring_metrics_store + .update_scoring_metrics_on_block_receival( + authority.0, + authority.1.hostname.as_str(), + block_verification_error.clone(), + source, + &context.metrics.node_metrics, + ); } assert_eq!( [ diff --git a/consensus/core/src/subscriber.rs b/consensus/core/src/subscriber.rs index 3c3e6e57ae1..40ca54ce9ee 100644 --- a/consensus/core/src/subscriber.rs +++ b/consensus/core/src/subscriber.rs @@ -230,13 +230,15 @@ impl Subscriber { .handle_send_block(peer, block.clone()) .await; if let Err(e) = result { - context.scorer.update_scoring_metrics_on_block_receival( - peer, - peer_hostname, - e.clone(), - "handle_send_block", - &context.metrics.node_metrics, - ); + context + .scoring_metrics_store + .update_scoring_metrics_on_block_receival( + peer, + peer_hostname, + e.clone(), + "handle_send_block", + &context.metrics.node_metrics, + ); match e { ConsensusError::BlockRejected { block_ref, reason } => { debug!( diff --git a/consensus/core/src/synchronizer.rs b/consensus/core/src/synchronizer.rs index b25b2a192cc..a61d557c87b 100644 --- a/consensus/core/src/synchronizer.rs +++ b/consensus/core/src/synchronizer.rs @@ -566,7 +566,7 @@ impl Synchronizer, /// Component including the local view about the other authorities' - /// misbehaviour metrics, and received reports. + /// misbehavior metrics, and received reports. pub(crate) scorer: Arc, } diff --git a/crates/iota-core/src/consensus_manager/mysticeti_manager.rs b/crates/iota-core/src/consensus_manager/mysticeti_manager.rs index 821ef86ea91..6cb23c88517 100644 --- a/crates/iota-core/src/consensus_manager/mysticeti_manager.rs +++ b/crates/iota-core/src/consensus_manager/mysticeti_manager.rs @@ -201,6 +201,7 @@ impl ConsensusManagerTrait for MysticetiManager { Arc::new(tx_validator.clone()), consumer, registry.clone(), + epoch_store.scorer.current_local_metrics_count.clone(), *boot_counter, ) .await; From 5f354fe84c39b17e90288a8cb488095bf7fe9a2c Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 1 Dec 2025 17:55:42 +0000 Subject: [PATCH 2/8] feat(iota-core): Add report validation logic (#9362) We build on top of the other PRs of this feature branch to enable the misbehavior reports validation logic. - [x] Basic tests (linting, compilation, formatting, unit/integration tests) - [ ] Patch-specific tests (correctness, functionality coverage) - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have checked that new and existing unit tests pass locally with my changes - [x] Protocol: Enables misbehavior report validation - [ ] Nodes (Validators and Full nodes): - [ ] Indexer: - [ ] JSON-RPC: - [ ] GraphQL: - [ ] CLI: - [ ] Rust SDK: - [ ] REST API: --------- Co-authored-by: Olivia --- .../authority/authority_per_epoch_store.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/iota-core/src/authority/authority_per_epoch_store.rs b/crates/iota-core/src/authority/authority_per_epoch_store.rs index 45d16da3a30..b1e63afdf68 100644 --- a/crates/iota-core/src/authority/authority_per_epoch_store.rs +++ b/crates/iota-core/src/authority/authority_per_epoch_store.rs @@ -3934,10 +3934,25 @@ impl AuthorityPerEpochStore { panic!("process_consensus_transaction called with end-of-publish transaction"); } SequencedConsensusTransactionKind::External(ConsensusTransaction { - kind: ConsensusTransactionKind::MisbehaviorReport(_authority, _report, _), + kind: ConsensusTransactionKind::MisbehaviorReport(authority, report, _), .. }) => { - // Validate report + let authority_index = self + .committee + .authority_index(authority) + .expect("authority in committee"); + // Check validity of the report and update scores depending on the result. We + // already have consensus on inclusion of this report in the DAG. + if !report.verify(self.committee.num_members()) { + self.scorer.update_invalid_reports_count(authority_index); + warn!( + "Received invalid misbehavior report from {:?}", + authority.concise() + ); + } else { + // Here we update all counts related to the information in the reports. + self.scorer.update_received_reports(authority_index, report); + } Ok(ConsensusCertificateResult::ConsensusMessage) } SequencedConsensusTransactionKind::External(ConsensusTransaction { From 22d653c063f0ea3a52797d497433f03cb61ed16c Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 1 Dec 2025 17:59:11 +0000 Subject: [PATCH 3/8] feat(iota-core, iota-types): Add scoring function (#9361) We introduce the scoring formulas for the validator scoring mechanism - [x] Basic tests (linting, compilation, formatting, unit/integration tests) - [ ] Patch-specific tests (correctness, functionality coverage) - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have checked that new and existing unit tests pass locally with my changes - [x] Protocol: Introduces new scoring formulas for the validator score mechanism - [ ] Nodes (Validators and Full nodes): - [ ] Indexer: - [ ] JSON-RPC: - [ ] GraphQL: - [ ] CLI: - [ ] Rust SDK: - [ ] REST API: --------- Co-authored-by: Olivia --- crates/iota-core/src/authority/scorer.rs | 597 +++++++++++++++++++- crates/iota-core/src/checkpoints/mod.rs | 8 + crates/iota-types/src/messages_consensus.rs | 72 ++- crates/iota-types/src/scoring_metrics.rs | 13 +- 4 files changed, 643 insertions(+), 47 deletions(-) diff --git a/crates/iota-core/src/authority/scorer.rs b/crates/iota-core/src/authority/scorer.rs index 2861deab2b1..a6a37d7fa93 100644 --- a/crates/iota-core/src/authority/scorer.rs +++ b/crates/iota-core/src/authority/scorer.rs @@ -8,20 +8,34 @@ use std::sync::{ use iota_protocol_config::ProtocolConfig; use iota_types::{ - messages_consensus::VersionedMisbehaviorReport, scoring_metrics::VersionedScoringMetrics, + messages_consensus::{MisbehaviorsV1, VersionedMisbehaviorReport}, + scoring_metrics::VersionedScoringMetrics, }; -#[allow(unused)] -const MAX_SCORE: u64 = u64::MAX; - -#[expect(dead_code)] +/// Holds all information related to scoring of authorities in the committee. pub struct Scorer { + // The current metrics counts collected by the authority, i.e., the local view of the node + // about the behaviour of the rest of the committee, according to the blocks received. pub(crate) current_local_metrics_count: Arc, + // The metrics counts received from other authorities, i.e., the information contained in the + // MisbehaviourReports received by the authority. If an authority has not sent a report, its + // entry in this vector will be all zeroed. received_metrics: Vec, - metrics_missing_from: Vec, + // Indicates whether an authority did not send any misbehavior reports in the epoch. We use + // this to differentiate an authority that did not send a report from another one who sent + // zeroed reports. + has_not_sent_report: Vec, + // The current scores of the authorities, updated after each received report. This score is + // calculated based on the information in the received reports and the validity of the reports + // themselves. pub(crate) current_scores: Scores, + // The count of invalid reports received from each authority. Validity here must be checked in + // a deterministic way, since this information will not be propagated again to the rest of the + // committee. invalid_reports_count: Vec, + // The voting power of each authority in the committee. voting_power: Vec, + // The version of the scorer being used with its parameters. version: ScorerVersion, } @@ -30,77 +44,391 @@ impl Scorer { let committee_size = voting_power.len(); match protocol_config.scorer_version_as_option() { None | Some(1) => { + // Local metrics count are always initialized as zero. let current_local_metrics_count = Arc::new(VersionedScoringMetrics::new( committee_size, protocol_config, )); - let (received_metrics, metrics_missing_from, current_scores, invalid_reports_count) = + let max_score = 2_u64.pow(16); + let (received_metrics, has_not_sent_report, current_scores, invalid_reports_count) = (0..committee_size) .map(|_| { ( + // Received metrics initialized to zero. VersionedScoringMetrics::new(committee_size, protocol_config), + // Initially, none of the authorities had sent any valid report. AtomicBool::new(true), - AtomicU64::new(0), + // Current scores initialized to max score. + AtomicU64::new(max_score), + // Invalid reports count initialized to zero. AtomicU64::new(0), ) }) .collect(); + let parameters = ParametersV1 { + allowances: MisbehaviorsV1 { + faulty_blocks_provable: 1, + faulty_blocks_unprovable: 2, + missing_proposals: 48_000, // roughly 3% of consensus rounds in an epoch + equivocations: 0, + }, + maximums: MisbehaviorsV1 { + faulty_blocks_provable: 5, + faulty_blocks_unprovable: 10, + missing_proposals: 160_000, // roughly 10% of consensus rounds in an epoch + equivocations: 1, + }, + weights: MisbehaviorsV1 { + faulty_blocks_provable: SCALE_FACTOR * 30 / 100, + faulty_blocks_unprovable: SCALE_FACTOR * 10 / 100, + missing_proposals: SCALE_FACTOR * 35 / 100, + equivocations: 1, + }, + }; + // Assert that the allowance for major misbehaviors is 0, + // maximum is 1 and weight is 1. This is because major misbehaviors should + // reduce the score to 0 is there are any occurrences. + // Only equivocation is considered a major misbehavior in this version. + assert!( + parameters + .allowances + .iter_major_misbehaviors() + .all(|&a| a == 0) + && parameters + .maximums + .iter_major_misbehaviors() + .all(|&m| m == 1) + && parameters + .weights + .iter_major_misbehaviors() + .all(|&w| w == 1) + ); Self { current_local_metrics_count, received_metrics, - metrics_missing_from, + has_not_sent_report, current_scores, invalid_reports_count, voting_power, - version: ScorerVersion::V1, + version: ScorerVersion::V1(parameters), } } _ => panic!("Unsupported scorer version"), } } + fn get_parameters_v1(&self) -> ParametersV1 { + match &self.version { + ScorerVersion::V1(params) => params.clone(), + } + } + pub(crate) fn update_invalid_reports_count(&self, authority: u32) { self.invalid_reports_count[authority as usize].fetch_add(1, Ordering::Relaxed); } pub(crate) fn update_scores(&self) { match self.version { - ScorerVersion::V1 => self.update_scores_v1(), + ScorerVersion::V1(_) => self.update_scores_v1(), }; } - #[expect(dead_code)] - pub(crate) fn update_received_reports_and_score( + pub(crate) fn update_received_reports( &self, authority: u32, report: &VersionedMisbehaviorReport, ) { + // Update the received metrics for the authority, and mark that we have received + // metrics from them. Then, update the scores accordingly. self.received_metrics[authority as usize].update_from_report(report); - self.metrics_missing_from[authority as usize].store(false, Ordering::Relaxed); - self.update_scores(); + self.has_not_sent_report[authority as usize].store(false, Ordering::Relaxed); } +} +// Methods for ScorerVersion::V1 +impl Scorer { fn update_scores_v1(&self) { - // Placeholder + // Vector with the highest received reports from each authority and their voting + // power. Authorities that did not send reports are filtered out. + let highest_received_reports_from_authority = self + .received_metrics + .iter() + .zip(self.voting_power.iter()) + .zip(self.has_not_sent_report.iter()) + .filter(|((_, _), is_missing)| !is_missing.load(Ordering::Relaxed)) + .map(|((metrics, voting_power), _)| (metrics.to_report(), *voting_power)) + .collect::>(); + // Ensure that we have at least one report to calculate the scores, otherwise we + // do nothing. + if highest_received_reports_from_authority.is_empty() { + } else { + let median_report = calculate_median_report(&highest_received_reports_from_authority); + let scores = calculate_scores_v1(median_report, self.get_parameters_v1()); + for (i, &score) in scores.iter().enumerate() { + self.current_scores[i].store(score, Ordering::Relaxed); + } + } + } +} + +/// Given a vector of pairs (VersionedMisbehaviorReport, VotingPower), calculate +/// the medians for all metrics in VersionedMisbehaviorReport and authorities: +/// +/// - Assume we have N authorities in the committee, but n<=N reports R_1, R_2, +/// ..., R_n from authorities with voting powers VP_1, VP_2, ..., VP_n. +/// - For each metric M in VersionedMisbehaviorReport, we'll have n vectors of +/// metric values: M_1, M_2, ..., M_n, where M_i is the vector of metric +/// values for report R_i. +/// - Each M_i is a vector of length N, where the j-th value corresponds to the +/// metric value for authority j. +/// +/// Example: If we have 4 authorities in the committee, and we receive 3 +/// reports: +/// - Report R_1 from authority A_1 with voting power VP_1 = 1: +/// - Metric M1: [0, 0, 0, 0] (values for authorities A_1, A_2, A_3, A_4) +/// - Metric M2: [0, 0, 0, 0] (values for authorities A_1, A_2, A_3, A_4) +/// - Report R_2 from authority A_2 with voting power VP_2 = 1: +/// - Metric M1: [1, 1, 1, 1] (values for authorities A_1, A_2, A_3, A_4) +/// - Metric M2: [2, 2, 2, 2] (values for authorities A_1, A_2, A_3, A_4) +/// - Report R_3 from authority A_3 with voting power VP_3 = 1: +/// - Metric M1: [2, 2, 2, 2] (values for authorities A_1, A_2, A_3, A_4) +/// - Metric M2: [1, 1, 1, 1] (values for authorities A_1, A_2, A_3, A_4) +/// +/// For Metric M1, we have that the median metric vector is [1, 1, 1, 1]. +/// +/// This method returns a vector of MedianMetricVec, one per metric in +/// VersionedMisbehaviorReport +fn calculate_median_report( + reports_and_voting_power: &[(VersionedMisbehaviorReport, VotingPower)], +) -> MisbehaviorsV1 { + // Calls to this method should ensure that we have at least one report to + // process. + assert!(!reports_and_voting_power.is_empty()); + + let number_of_metrics = reports_and_voting_power[0].0.iterate_over_metrics().len(); + + // In the case of the example in the method documentation, + // reports_and_voting_power_per_metric should be + // vec![ + // vec![([0, 0, 0, 0],VP_1),([1, 1, 1, 1],VP_2),([2, 2, 2, 2],VP_3)], + // vec![([0, 0, 0, 0],VP_1),([2, 2, 2, 2],VP_2),([1, 1, 1, 1],VP_3)] + // ] + let mut reports_and_voting_power_per_metric: Vec> = + vec![vec![]; number_of_metrics]; + for (versioned_report, voting_power) in reports_and_voting_power.iter() { + for (i, metric) in versioned_report.iterate_over_metrics().enumerate() { + reports_and_voting_power_per_metric[i].push((metric.clone(), *voting_power)); + } } + + // Calculate and return the weighted median for each metric + let median_report = reports_and_voting_power_per_metric + .iter_mut() + .map(|vec| calculate_weighted_median(vec.as_mut_slice())) + .collect::>(); + median_report +} + +// Given a vector of pairs (MetricVec, VotingPower), calculate the weighted +// median of each entry of MetricVec and returns a MedianMetricVec. Each entry +// of reports corresponds to a single authority i who sent the report. MetricVec +// always corresponds to a single metric, and each of its entries corresponds to +// the number of misbehaviors that i claims to have detected from each authority +// in the committee. +fn calculate_weighted_median(reports: &mut [(MetricVec, VotingPower)]) -> MedianMetricVec { + // Calls to this method should ensure that we have at least one pair (MetricVec, + // VotingPower) to process. + assert!(!reports.is_empty()); + + // We calculate the weighted median relative to the voting power of the + // authorities who actually sent a report. + let voting_power_used = reports.iter().map(|(_, vp)| *vp).sum::(); + // The caller should also guarantee that the MetricVec in all reports have the + // same length (committee_size). This is naturally guaranteed when these data + // come from MisbehaviorReports, since they would been considered invalid + // otherwise. + let committee_size = reports[0].0.len(); + let mut median_per_validator_being_scored = Vec::new(); + + for validator_being_scored in 0..committee_size { + let mut accumulated_voting_power = 0; + reports.sort_by_key(|(reported_counts, _)| reported_counts[validator_being_scored]); + for (reported_counts, voting_power) in reports.iter() { + accumulated_voting_power += *voting_power; + if accumulated_voting_power * 2 >= voting_power_used { + median_per_validator_being_scored.push(reported_counts[validator_being_scored]); + break; + } + } + } + + median_per_validator_being_scored +} + +// Scorer version. Currently, only V1 is implemented, relative to both +// protocol_config.scorer_version = None or Some(1). +enum ScorerVersion { + V1(ParametersV1), } -pub(crate) enum ScorerVersion { - V1, +// Parameters for ScorerVersion::V1 +#[derive(Clone)] +struct ParametersV1 { + // Allowed misbehaviors without any punishment + allowances: MisbehaviorsV1, + // Number of misbehaviors that lead to zero score + maximums: MisbehaviorsV1, + // Weights for each metric. The sum of minor misbehavior weights + baseline_score = + // scale_factor. Major misbehavior weights are either 0 or 1. + weights: MisbehaviorsV1, } +// Aliases for better readability. pub(crate) type Scores = Vec; pub(crate) type Score = AtomicU64; +type VotingPower = u64; +type MedianMetricVec = Vec; +type MetricVec = Vec; + +// Given the median reports for all metrics, calculate the final scores. A score +// is an integer between 0 and max_score. For each metrics, we have an allowance +// (allowed misbehaviors without any punishment) and a maximum (number of +// misbehaviors that lead to zero score). Based on those values, we calculate a +// score per metric, and then combine them into a final score. Each individual +// score for minor misbeahviors (non-equivocation) is also an integer between 0 +// and max_score, and the weights used for the combination are such that +// sum(weights) + baseline_score = scale_factor. Thus, we need +// max_score*scale_factor < 2^64 to avoid overflows. +// Major misbehaviors (equivocations) are treated differently, as they +// multiplicatively impact the final score. Their value is either 0 or 1. +fn calculate_scores_v1( + median_reports: MisbehaviorsV1, + parameters: ParametersV1, +) -> Vec { + let baseline_score = SCALE_FACTOR - parameters.weights.iter_minor_misbehaviors().sum::(); + + let median_minor_reports_and_parameters = median_reports + .iter_minor_misbehaviors() + .zip(parameters.allowances.iter_minor_misbehaviors()) + .zip(parameters.maximums.iter_minor_misbehaviors()); + + // Calculate individual metric scores + let minor_metric_scores = median_minor_reports_and_parameters + .map( + |((median_report_for_a_single_metric, metric_allowance), metric_maximum)| { + median_report_single_metric_to_score( + median_report_for_a_single_metric, + *metric_allowance, + *metric_maximum, + MAX_SCORE, + ) + }, + ) + .collect::>>(); + + let median_major_reports_and_parameters = median_reports + .iter_major_misbehaviors() + .zip(parameters.allowances.iter_major_misbehaviors()) + .zip(parameters.maximums.iter_major_misbehaviors()); + + // Calculate individual metric scores + let major_metric_scores = median_major_reports_and_parameters + .map( + |((median_report_for_a_single_metric, metric_allowance), metric_maximum)| { + median_report_single_metric_to_score( + median_report_for_a_single_metric, + *metric_allowance, + *metric_maximum, + 1, + ) + }, + ) + .collect::>>(); + + metrics_scores_to_final_scores( + minor_metric_scores, + major_metric_scores, + parameters.weights, + baseline_score, + SCALE_FACTOR, + MAX_SCORE, + ) +} + +fn metrics_scores_to_final_scores( + minor_metric_scores: Vec>, + major_metric_scores: Vec>, + weights: MisbehaviorsV1, + baseline_score: u64, + scale_factor: u64, + max_score: u64, +) -> Vec { + // Initialise the final scores with the baseline score whose value is between 0 + // and max_score * scale_factor. + let committee_size = minor_metric_scores.first().unwrap().len(); + let mut final_scores = vec![baseline_score * max_score; committee_size]; + // First, calculate the weights sum of minor misbehavior scores vector. The + // values in final_scores will still be between 0 and max_score * scale_factor + minor_metric_scores + .iter() + .zip(weights.iter_minor_misbehaviors()) + .for_each(|(scores, weight)| { + for (i, &score) in scores.iter().enumerate() { + final_scores[i] += score * weight; + } + }); + // Then, multiply by each major misbehavior score which is a value of either 0 + // or 1. + major_metric_scores.iter().for_each(|scores| { + for (i, &score) in scores.iter().enumerate() { + final_scores[i] *= score; + } + }); + // Finally, divide by the scale factor and scale to max_score + for score in final_scores.iter_mut() { + *score /= scale_factor; + } + final_scores +} + +// Calculate the metric scores for a single metric's median report vector. It +// returns a vector of values between 0 and the max score for that metric. +fn median_report_single_metric_to_score( + median_report_for_metric: &MedianMetricVec, + metric_allowance: u64, + metric_max: u64, + max_metric_score: u64, +) -> Vec { + median_report_for_metric + .iter() + .map(|&report| metric_to_score(report, metric_allowance, metric_max, max_metric_score)) + .collect() +} + +fn metric_to_score(value: u64, allowance: u64, max: u64, max_score: u64) -> u64 { + if value <= allowance { + max_score + } else if value >= max { + 0 + } else { + // TODO: add overflow checks + (max - value) * max_score / (max - allowance) + } +} +// NOTE: the tests below are going to be finalized in a different PR #[cfg(test)] mod tests { use std::sync::atomic::Ordering; use iota_protocol_config::{ConsensusChoice, ProtocolConfig}; + use iota_types::messages_consensus::{MisbehaviorsV1, VersionedMisbehaviorReport}; - use super::*; + use crate::authority::authority_per_epoch_store::scorer::{ + ParametersV1, Scorer, calculate_median_report, calculate_scores_v1, + }; fn mock_protocol_config(consensus_choice: ConsensusChoice) -> ProtocolConfig { let mut config = ProtocolConfig::get_for_max_version_UNSAFE(); @@ -108,6 +436,16 @@ mod tests { config } + impl Scorer { + fn set_reports_for_tests( + &self, + reports_and_authorities: &[(VersionedMisbehaviorReport, u32)], + ) { + for (report, authority) in reports_and_authorities.iter() { + self.update_received_reports(*authority, report); + } + } + } #[test] fn test_scorer_initialization() { let voting_power = vec![10, 20, 30]; @@ -118,8 +456,8 @@ mod tests { assert_eq!(scorer.current_scores.len(), committee_size); assert_eq!(scorer.invalid_reports_count.len(), committee_size); - - // Add more + assert_eq!(scorer.received_metrics.len(), committee_size); + assert_eq!(scorer.has_not_sent_report.len(), committee_size); } #[test] @@ -143,24 +481,227 @@ mod tests { // After update assert_eq!( - scorer.invalid_reports_count[authority_index as usize].load(Ordering::Relaxed), + scorer.invalid_reports_count[0_usize].load(Ordering::Relaxed), + 0 + ); + assert_eq!( + scorer.invalid_reports_count[1_usize].load(Ordering::Relaxed), + 0 + ); + assert_eq!( + scorer.invalid_reports_count[2_usize].load(Ordering::Relaxed), + 1 + ); + + let authority_index = 1; + // Call the method twice + scorer.update_invalid_reports_count(authority_index); + scorer.update_invalid_reports_count(authority_index); + + // After update + assert_eq!( + scorer.invalid_reports_count[0_usize].load(Ordering::Relaxed), + 0 + ); + assert_eq!( + scorer.invalid_reports_count[1_usize].load(Ordering::Relaxed), + 2 + ); + assert_eq!( + scorer.invalid_reports_count[2_usize].load(Ordering::Relaxed), 1 ); } #[test] fn test_update_scores() { - let voting_power = vec![10, 20, 30]; - + let voting_power = vec![2, 5, 20]; let protocol_config = mock_protocol_config(ConsensusChoice::Mysticeti); - let scorer = Scorer::new(voting_power, &protocol_config); - // Before calling update_scores, all scores should be 0 + // Before calling update_scores, all scores should be MAX_SCORE for score in scorer.current_scores.iter() { - assert_eq!(score.load(Ordering::Relaxed), 0); + assert_eq!(score.load(Ordering::Relaxed), MAX_SCORE,); } - // Add logic + // Set some reports for testing + let reports_and_authorities = vec![ + ( + VersionedMisbehaviorReport::V1(MisbehaviorsV1 { + faulty_blocks_provable: vec![5, 0, 0], + faulty_blocks_unprovable: vec![0, 0, 0], + missing_proposals: vec![0, 0, 0], + equivocations: vec![0, 0, 0], + }), + 0_u32, + ), + ( + VersionedMisbehaviorReport::V1(MisbehaviorsV1 { + faulty_blocks_provable: vec![0, 10, 0], + faulty_blocks_unprovable: vec![0, 0, 0], + missing_proposals: vec![0, 0, 0], + equivocations: vec![0, 0, 0], + }), + 1_u32, + ), + ( + VersionedMisbehaviorReport::V1(MisbehaviorsV1 { + faulty_blocks_provable: vec![0, 0, 15], + faulty_blocks_unprovable: vec![0, 0, 0], + missing_proposals: vec![0, 0, 0], + equivocations: vec![5, 0, 0], + }), + 2_u32, + ), + ]; + + scorer.set_reports_for_tests(&reports_and_authorities); + + // Call the method + scorer.update_scores(); + + let expected_score = vec![0, 65536, 45876]; + // After calling update_scores, scores should be updated + let actual_score = scorer + .current_scores + .iter() + .map(|value| value.load(Ordering::Relaxed)) + .collect::>(); + assert_eq!(actual_score, expected_score); + } + + #[test] + fn test_calculate_median_report() { + let reports_and_voting_power = vec![( + VersionedMisbehaviorReport::V1(MisbehaviorsV1 { + faulty_blocks_provable: vec![7, 8, 9], + faulty_blocks_unprovable: vec![10, 11, 12], + missing_proposals: vec![4, 5, 6], + equivocations: vec![1, 2, 3], + }), + 10_u64, + )]; + let median_report = calculate_median_report(&reports_and_voting_power); + + assert_eq!( + median_report, + MisbehaviorsV1 { + faulty_blocks_provable: vec![7, 8, 9], + faulty_blocks_unprovable: vec![10, 11, 12], + missing_proposals: vec![4, 5, 6], + equivocations: vec![1, 2, 3] + } + ); + + let reports_and_voting_power = vec![ + ( + VersionedMisbehaviorReport::V1(MisbehaviorsV1 { + faulty_blocks_provable: vec![7, 8, 9], + faulty_blocks_unprovable: vec![10, 11, 12], + missing_proposals: vec![4, 5, 6], + equivocations: vec![1, 2, 3], + }), + 20_u64, + ), + ( + VersionedMisbehaviorReport::V1(MisbehaviorsV1 { + faulty_blocks_provable: vec![70, 80, 90], + faulty_blocks_unprovable: vec![100, 110, 120], + missing_proposals: vec![40, 50, 60], + equivocations: vec![10, 20, 30], + }), + 10_u64, + ), + ]; + + let median_report = calculate_median_report(&reports_and_voting_power); + + assert_eq!( + median_report, + MisbehaviorsV1 { + faulty_blocks_provable: vec![7, 8, 9], + faulty_blocks_unprovable: vec![10, 11, 12], + missing_proposals: vec![4, 5, 6], + equivocations: vec![1, 2, 3] + } + ); + + let reports_and_voting_power = vec![ + ( + VersionedMisbehaviorReport::V1(MisbehaviorsV1 { + faulty_blocks_provable: vec![1, 8, 9], + faulty_blocks_unprovable: vec![10, 15, 12], + missing_proposals: vec![4, 5, 6], + equivocations: vec![1, 20, 3], + }), + 10_u64, + ), + ( + VersionedMisbehaviorReport::V1(MisbehaviorsV1 { + faulty_blocks_provable: vec![7, 8, 9], + faulty_blocks_unprovable: vec![10, 11, 12], + missing_proposals: vec![4, 5, 6], + equivocations: vec![1, 2, 0], + }), + 10_u64, + ), + ( + VersionedMisbehaviorReport::V1(MisbehaviorsV1 { + faulty_blocks_provable: vec![6, 8, 9], + faulty_blocks_unprovable: vec![10, 11, 12], + missing_proposals: vec![4, 22, 6], + equivocations: vec![1, 2, 30], + }), + 10_u64, + ), + ]; + + let median_report = calculate_median_report(&reports_and_voting_power); + + assert_eq!( + median_report, + MisbehaviorsV1 { + faulty_blocks_provable: vec![6, 8, 9], + faulty_blocks_unprovable: vec![10, 11, 12], + missing_proposals: vec![4, 5, 6], + equivocations: vec![1, 2, 3] + } + ); + } + + #[test] + fn test_calculate_scores_v1() { + let parameters = ParametersV1 { + allowances: MisbehaviorsV1 { + faulty_blocks_provable: 1, + faulty_blocks_unprovable: 2, + missing_proposals: 1000, + equivocations: 0, + }, + maximums: MisbehaviorsV1 { + faulty_blocks_provable: 5, + faulty_blocks_unprovable: 10, + missing_proposals: 5000, + equivocations: 1, + }, + weights: MisbehaviorsV1 { + faulty_blocks_provable: SCALE_FACTOR * 30 / 100, + faulty_blocks_unprovable: SCALE_FACTOR * 10 / 100, + missing_proposals: SCALE_FACTOR * 35 / 100, + equivocations: 1, + }, + }; + + let median_reports = MisbehaviorsV1 { + faulty_blocks_provable: vec![6, 7, 8], + faulty_blocks_unprovable: vec![9, 10, 11], + missing_proposals: vec![3, 4, 5], + equivocations: vec![0, 1, 2], + }; + + let scores = calculate_scores_v1(median_reports, parameters); + + // Check that scores are calculated correctly + assert_eq!(scores, vec![40142, 0, 0]); } } diff --git a/crates/iota-core/src/checkpoints/mod.rs b/crates/iota-core/src/checkpoints/mod.rs index 5b9ba29b5f2..dae5776ab6c 100644 --- a/crates/iota-core/src/checkpoints/mod.rs +++ b/crates/iota-core/src/checkpoints/mod.rs @@ -1615,6 +1615,14 @@ impl CheckpointBuilder { } } + // We update the validator scores based on the information contained in the + // Scorer. We choose this point in time to do so because we must guarantee that + // scores are up to date right before the epoch changes. It also provides a good + // update periodicity: updating scores each time a report is received could be + // too frequent and not needed, since scores are not used during the epoch + // (except for monitoring purposes, which does not need to be 100% exact) + self.epoch_store.scorer.update_scores(); + let (mut effects, mut signatures): (Vec<_>, Vec<_>) = transactions.into_iter().unzip(); let epoch_rolling_gas_cost_summary = self.get_epoch_total_gas_cost(last_checkpoint.as_ref().map(|(_, c)| c), &effects); diff --git a/crates/iota-types/src/messages_consensus.rs b/crates/iota-types/src/messages_consensus.rs index 6d03eb13a49..fcb75bed960 100644 --- a/crates/iota-types/src/messages_consensus.rs +++ b/crates/iota-types/src/messages_consensus.rs @@ -285,7 +285,7 @@ impl ConsensusTransactionKind { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum VersionedMisbehaviorReport { - V1(MisbehaviorReportV1), + V1(MisbehaviorsV1>), } impl VersionedMisbehaviorReport { @@ -294,23 +294,31 @@ impl VersionedMisbehaviorReport { VersionedMisbehaviorReport::V1(report) => report.verify(committee_size), } } + + /// Returns an iterator over references to some of the fields in the report. + pub fn iterate_over_metrics(&self) -> std::vec::IntoIter<&Vec> { + match self { + VersionedMisbehaviorReport::V1(report) => report.iter(), + } + } } -// MisbehaviorReportV1 contains lists of faulty blocks, equivocation and missing -// proposal counts for each authority. This first version does not include any -// type of proof. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MisbehaviorReportV1 { - pub faulty_blocks_provable: Vec, - pub faulty_blocks_unprovable: Vec, - pub equivocations: Vec, - pub missing_proposals: Vec, +// MisbehaviorsV1 contains lists of all metrics used in v1 of misbehavior +// reports, with a value for each metric. The metrics (misbeheaviors) include, +// faulty blocks, equivocation and missing proposal counts for each authority. +// This first version does not include any type of proof. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct MisbehaviorsV1 { + pub faulty_blocks_provable: T, + pub faulty_blocks_unprovable: T, + pub missing_proposals: T, + pub equivocations: T, } -impl MisbehaviorReportV1 { +impl MisbehaviorsV1> { pub fn verify(&self, committee_size: usize) -> bool { // This version of reports are valid as long as they contain the counts for all - // authorities. Future versions may contain proofs that need verification. + // authorities. Future versions may contain proofs that need verification. // However, since the validity of a proof is deeply coupled with the protocol // version and the consensus mechanism being used, we cannot verify it here. In // the future, reports should be unwrapped (or translated) to a type verifiable @@ -326,6 +334,44 @@ impl MisbehaviorReportV1 { true } } +impl MisbehaviorsV1 { + pub fn iter(&self) -> std::vec::IntoIter<&T> { + vec![ + &self.faulty_blocks_provable, + &self.faulty_blocks_unprovable, + &self.missing_proposals, + &self.equivocations, + ] + .into_iter() + } + // Returns an iterator over references to major misbehavior fields in the + // report. Major misbehaviors carry a higher penalty in the scoring system. + pub fn iter_major_misbehaviors(&self) -> std::vec::IntoIter<&T> { + vec![&self.equivocations].into_iter() + } + // Returns an iterator over references to minor misbehavior fields in the + // report. Minor misbehaviors carry a lower penalty in the scoring system. + pub fn iter_minor_misbehaviors(&self) -> std::vec::IntoIter<&T> { + vec![ + &self.faulty_blocks_provable, + &self.faulty_blocks_unprovable, + &self.missing_proposals, + ] + .into_iter() + } +} + +impl FromIterator for MisbehaviorsV1 { + fn from_iter>(iter: I) -> Self { + let mut iterator = iter.into_iter(); + Self { + faulty_blocks_provable: iterator.next().expect("Not enough elements in iterator"), + faulty_blocks_unprovable: iterator.next().expect("Not enough elements in iterator"), + missing_proposals: iterator.next().expect("Not enough elements in iterator"), + equivocations: iterator.next().expect("Not enough elements in iterator"), + } + } +} #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum VersionedDkgMessage { @@ -519,7 +565,7 @@ impl ConsensusTransaction { pub fn new_misbehavior_report_v1( authority: AuthorityName, - report: &MisbehaviorReportV1, + report: &MisbehaviorsV1>, round: CommitRound, ) -> Self { let serialized_report = diff --git a/crates/iota-types/src/scoring_metrics.rs b/crates/iota-types/src/scoring_metrics.rs index e9a34bc91c2..c52d33e5422 100644 --- a/crates/iota-types/src/scoring_metrics.rs +++ b/crates/iota-types/src/scoring_metrics.rs @@ -5,7 +5,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use iota_protocol_config::ProtocolConfig; -use crate::messages_consensus::{MisbehaviorReportV1, VersionedMisbehaviorReport}; +use crate::messages_consensus::{MisbehaviorsV1, VersionedMisbehaviorReport}; // This struct represents the scoring metrics collected by all authorities. They // are stored locally by each authority and then converted to a misbehavior @@ -21,6 +21,7 @@ pub enum VersionedScoringMetrics { // Basic getters, setters and increments for the metrics. impl VersionedScoringMetrics { pub fn new(committee_size: usize, protocol_config: &ProtocolConfig) -> Self { + // Any version of ScoringMetrics created here must be initialized to zero. match protocol_config.scorer_version_as_option() { None | Some(1) => VersionedScoringMetrics::V1(ScoringMetricsV1::new(committee_size)), _ => panic!("Unsupported scorer version"), @@ -256,11 +257,11 @@ impl VersionedScoringMetrics { .iter() .map(|metric| metric.load(Ordering::Relaxed)) .collect(); - VersionedMisbehaviorReport::V1(MisbehaviorReportV1 { + VersionedMisbehaviorReport::V1(MisbehaviorsV1 { faulty_blocks_provable, faulty_blocks_unprovable, - equivocations, missing_proposals, + equivocations, }) } } @@ -270,8 +271,8 @@ impl VersionedScoringMetrics { pub struct ScoringMetricsV1 { faulty_blocks_provable: Vec, faulty_blocks_unprovable: Vec, - equivocations: Vec, missing_proposals: Vec, + equivocations: Vec, } impl ScoringMetricsV1 { @@ -281,11 +282,11 @@ impl ScoringMetricsV1 { faulty_blocks_provable: (0..committee_size).map(|_| AtomicU64::new(0)).collect(), // Blocks considered faulty before passing the signature check. faulty_blocks_unprovable: (0..committee_size).map(|_| AtomicU64::new(0)).collect(), + // Number or rounds that the authority did not propose any block + missing_proposals: (0..committee_size).map(|_| AtomicU64::new(0)).collect(), // Number of additional blocks issued by a validator within rounds where another block // was already produced by them. equivocations: (0..committee_size).map(|_| AtomicU64::new(0)).collect(), - // Number or rounds that the authority did not propose any block - missing_proposals: (0..committee_size).map(|_| AtomicU64::new(0)).collect(), } } } From 99c886f1b7827b64a5eca547eabccdb9a3dc30ac Mon Sep 17 00:00:00 2001 From: oliviasaa Date: Mon, 1 Dec 2025 14:35:59 +0000 Subject: [PATCH 4/8] feat(iota-core, iota-types): Enable misbehavior report creation (#9363) # Description of change This PR enables the creation of the MisbehaviorReport consensus message, as their submission to consensus in order to disseminate it with the rest of the committee. ## Links to any relevant issues Be sure to reference any related issues by adding `fixes #(issue)`. ## How the change has been tested - [x] Basic tests (linting, compilation, formatting, unit/integration tests) - [ ] Patch-specific tests (correctness, functionality coverage) - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have checked that new and existing unit tests pass locally with my changes ### Release Notes Check each box that your changes affect. If none of the boxes relate to your changes, release notes aren't required. For each box you select, include information after the relevant heading that describes the impact of your changes that a user might notice and any actions they must take to implement updates. - [x] Protocol: Enables misbehavior report creation and dissemination. - [ ] Nodes (Validators and Full nodes): - [ ] Indexer: - [ ] JSON-RPC: - [ ] GraphQL: - [ ] CLI: - [ ] Rust SDK: - [ ] REST API: --------- Co-authored-by: Andrew --- .../src/checkpoints/checkpoint_output.rs | 14 +++++++ crates/iota-core/src/checkpoints/mod.rs | 3 +- crates/iota-types/src/messages_consensus.rs | 40 +++++++++++-------- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/crates/iota-core/src/checkpoints/checkpoint_output.rs b/crates/iota-core/src/checkpoints/checkpoint_output.rs index fbfb4d4aab8..6abb72bc27a 100644 --- a/crates/iota-core/src/checkpoints/checkpoint_output.rs +++ b/crates/iota-core/src/checkpoints/checkpoint_output.rs @@ -123,6 +123,20 @@ impl CheckpointOutput .set(checkpoint_seq as i64); } + // We also send misbehavior reports to consensus at this point. Misbehavior + // reports containing proofs of misbehaviour can be send whenever the + // misbehavior is detected, but we choose to send the ones that include only + // unprovable counts at this point, due to periodicity reasons and to ensure a + // (approximate) synchronization with the score updates. + let misbehavior_report = epoch_store.scorer.current_local_metrics_count.to_report(); + let transaction = ConsensusTransaction::new_misbehavior_report( + epoch_store.name, + &misbehavior_report, + checkpoint_seq, + ); + self.sender + .submit_to_consensus(&vec![transaction], epoch_store)?; + if checkpoint_timestamp >= self.next_reconfiguration_timestamp_ms { // close_epoch is ok if called multiple times self.sender.close_epoch(epoch_store); diff --git a/crates/iota-core/src/checkpoints/mod.rs b/crates/iota-core/src/checkpoints/mod.rs index dae5776ab6c..eeae0532c4f 100644 --- a/crates/iota-core/src/checkpoints/mod.rs +++ b/crates/iota-core/src/checkpoints/mod.rs @@ -1381,7 +1381,8 @@ impl CheckpointBuilder { batch.write()?; - // Send all checkpoint sigs to consensus. + // Send all checkpoint sigs to consensus. The messages including + // MisbehaviorReports are also sent in this step. for (summary, contents) in &new_checkpoints { self.output .checkpoint_created(summary, contents, &self.epoch_store, &self.store) diff --git a/crates/iota-types/src/messages_consensus.rs b/crates/iota-types/src/messages_consensus.rs index fcb75bed960..a48d0807cd8 100644 --- a/crates/iota-types/src/messages_consensus.rs +++ b/crates/iota-types/src/messages_consensus.rs @@ -20,8 +20,7 @@ use serde::{Deserialize, Serialize}; use crate::{ base_types::{ - AuthorityName, CommitRound, ConciseableName, ObjectID, ObjectRef, SequenceNumber, - TransactionDigest, + AuthorityName, ConciseableName, ObjectID, ObjectRef, SequenceNumber, TransactionDigest, }, crypto::{AuthoritySignature, DefaultHash}, digests::{ConsensusCommitDigest, Digest}, @@ -91,10 +90,10 @@ pub enum ConsensusTransactionKey { NewJWKFetched(Box<(AuthorityName, JwkId, JWK)>), RandomnessDkgMessage(AuthorityName), RandomnessDkgConfirmation(AuthorityName), - // If a validator submits more than one report for the same round, we update the - // scoring metrics using the maximum reported metric, so CommitRound is sufficient to - // identify a report from a single AuthorityName. - MisbehaviorReport(AuthorityName, CommitRound), + // If a validator submits more than one report for the same checkpoint, we update the + // scoring metrics using the maximum reported metric, so the checkpoint sequence number is + // sufficient to identify a report from a single AuthorityName. + MisbehaviorReport(AuthorityName, u64 /* checkpoint_seq */), // New entries should be added at the end to preserve serialization compatibility. DO NOT // CHANGE THE ORDER OF EXISTING ENTRIES! } @@ -107,8 +106,13 @@ impl Debug for ConsensusTransactionKey { write!(f, "CheckpointSignature({:?}, {:?})", name.concise(), seq) } Self::EndOfPublish(name) => write!(f, "EndOfPublish({:?})", name.concise()), - Self::MisbehaviorReport(name, round) => { - write!(f, "MisbehaviorReport({:?},{:?})", name.concise(), round) + Self::MisbehaviorReport(name, checkpoint_seq) => { + write!( + f, + "MisbehaviorReport({:?},{:?})", + name.concise(), + checkpoint_seq + ) } Self::CapabilityNotification(name, generation) => write!( f, @@ -268,7 +272,11 @@ pub enum ConsensusTransactionKind { // of `RandomnessDkgMessages` have been received locally, to complete the key generation // process. Contents are a serialized `fastcrypto_tbls::dkg::Confirmation`. RandomnessDkgConfirmation(AuthorityName, Vec), - MisbehaviorReport(AuthorityName, VersionedMisbehaviorReport, CommitRound), + MisbehaviorReport( + AuthorityName, + VersionedMisbehaviorReport, + u64, // checkpoint_seq + ), // New entries should be added at the end to preserve serialization compatibility. DO NOT // CHANGE THE ORDER OF EXISTING ENTRIES! } @@ -563,10 +571,10 @@ impl ConsensusTransaction { } } - pub fn new_misbehavior_report_v1( + pub fn new_misbehavior_report( authority: AuthorityName, - report: &MisbehaviorsV1>, - round: CommitRound, + report: &VersionedMisbehaviorReport, + checkpoint_seq: u64, ) -> Self { let serialized_report = bcs::to_bytes(report).expect("report serialization should not fail"); @@ -577,8 +585,8 @@ impl ConsensusTransaction { tracking_id, kind: ConsensusTransactionKind::MisbehaviorReport( authority, - VersionedMisbehaviorReport::V1(report.clone()), - round, + report.clone(), + checkpoint_seq, ), } } @@ -603,8 +611,8 @@ impl ConsensusTransaction { ConsensusTransactionKind::EndOfPublish(authority) => { ConsensusTransactionKey::EndOfPublish(*authority) } - ConsensusTransactionKind::MisbehaviorReport(authority, _, round) => { - ConsensusTransactionKey::MisbehaviorReport(*authority, *round) + ConsensusTransactionKind::MisbehaviorReport(authority, _, checkpoint_seq) => { + ConsensusTransactionKey::MisbehaviorReport(*authority, *checkpoint_seq) } ConsensusTransactionKind::CapabilityNotificationV1(cap) => { ConsensusTransactionKey::CapabilityNotification(cap.authority, cap.generation) From 270008771441408a988c4849b9ae4fd8b9b37a5b Mon Sep 17 00:00:00 2001 From: oliviasaa Date: Mon, 1 Dec 2025 14:47:28 +0000 Subject: [PATCH 5/8] feat(iota-core, iota-types): Add advance epoch v4 (#9424) Enables the creation of a new version of advance epoch transaction and new advance epoch function in iota-framework to modify rewards based on scores. The existing validator reporting system remains whereby rewards can be slashed entirely when a validator is reported. Now we additionally modulate the rewards with the score provided as an argument to the new version of the advance epoch function. - [x] Basic tests (linting, compilation, formatting, unit/integration tests) - [ ] Patch-specific tests (correctness, functionality coverage) - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have checked that new and existing unit tests pass locally with my changes - [x] Protocol: Enables the creation of a new version of advance epoch transaction and new advance epoch function in iota-framework to modify rewards based on scores. - [ ] Nodes (Validators and Full nodes): - [x] Indexer: Enables a new `ChangeEpochV4` system transaction which may appear in checkpoints for networks where `score_based_rewards` protocol config is enabled. Consequently **a dependency bump is necessary for `iota_data_ingestion_core` library consumers (custom indexers) to prevent outages**. - [ ] JSON-RPC: - [ ] GraphQL: - [ ] CLI: - [ ] Rust SDK: - [ ] REST API: --------- Co-authored-by: Andrew --- crates/iota-core/src/authority.rs | 47 ++++++---- crates/iota-core/src/authority/scorer.rs | 9 +- .../checkpoints/checkpoint_executor/tests.rs | 3 +- crates/iota-core/src/checkpoints/mod.rs | 10 +++ .../tests/reconfiguration_tests.rs | 5 +- .../iota-system/sources/iota_system.move | 4 + .../sources/iota_system_state_inner.move | 2 + .../iota-system/sources/validator_set.move | 29 +++++-- .../tests/governance_test_utils.move | 52 +++++++++++ .../tests/rewards_distribution_tests.move | 87 +++++++++++++++++++ .../tests/validator_set_tests.move | 20 +++++ crates/iota-protocol-config/src/lib.rs | 7 +- .../iota-types/src/iota_system_state/mod.rs | 1 + crates/iota-types/src/transaction.rs | 28 ++++++ .../iota-adapter/src/execution_engine.rs | 36 +++++++- 15 files changed, 306 insertions(+), 34 deletions(-) diff --git a/crates/iota-core/src/authority.rs b/crates/iota-core/src/authority.rs index aee55f00dc5..b36c5a4984a 100644 --- a/crates/iota-core/src/authority.rs +++ b/crates/iota-core/src/authority.rs @@ -4773,6 +4773,7 @@ impl AuthorityState { gas_cost_summary: &GasCostSummary, checkpoint: CheckpointSequenceNumber, epoch_start_timestamp_ms: CheckpointTimestamp, + scores: Vec, ) -> anyhow::Result<( IotaSystemState, Option, @@ -4827,9 +4828,8 @@ impl AuthorityState { bail!("missing system packages: cannot form ChangeEpochTx"); }; - // Use ChangeEpochV3 when the feature flag is enabled and ChangeEpochV2 - // requirements are met - + // Use ChangeEpochV3 or ChangeEpochV4 when the feature flags are enabled and + // ChangeEpochV2 requirements are met if config.select_committee_from_eligible_validators() { // Get the list of eligible validators that support the target protocol version let active_validators = epoch_store.epoch_start_state().get_active_validators(); @@ -4871,18 +4871,35 @@ impl AuthorityState { } } - txns.push(EndOfEpochTransactionKind::new_change_epoch_v3( - next_epoch, - next_epoch_protocol_version, - gas_cost_summary.storage_cost, - gas_cost_summary.computation_cost, - gas_cost_summary.computation_cost_burned, - gas_cost_summary.storage_rebate, - gas_cost_summary.non_refundable_storage_fee, - epoch_start_timestamp_ms, - next_epoch_system_package_bytes, - eligible_active_validators, - )); + // Use ChangeEpochV4 when the feature flag is enabled + if config.score_based_rewards() { + txns.push(EndOfEpochTransactionKind::new_change_epoch_v4( + next_epoch, + next_epoch_protocol_version, + gas_cost_summary.storage_cost, + gas_cost_summary.computation_cost, + gas_cost_summary.computation_cost_burned, + gas_cost_summary.storage_rebate, + gas_cost_summary.non_refundable_storage_fee, + epoch_start_timestamp_ms, + next_epoch_system_package_bytes, + eligible_active_validators, + scores, + )); + } else { + txns.push(EndOfEpochTransactionKind::new_change_epoch_v3( + next_epoch, + next_epoch_protocol_version, + gas_cost_summary.storage_cost, + gas_cost_summary.computation_cost, + gas_cost_summary.computation_cost_burned, + gas_cost_summary.storage_rebate, + gas_cost_summary.non_refundable_storage_fee, + epoch_start_timestamp_ms, + next_epoch_system_package_bytes, + eligible_active_validators, + )); + } } else if config.protocol_defined_base_fee() && config.max_committee_members_count_as_option().is_some() { diff --git a/crates/iota-core/src/authority/scorer.rs b/crates/iota-core/src/authority/scorer.rs index a6a37d7fa93..3d0775dcab8 100644 --- a/crates/iota-core/src/authority/scorer.rs +++ b/crates/iota-core/src/authority/scorer.rs @@ -12,6 +12,9 @@ use iota_types::{ scoring_metrics::VersionedScoringMetrics, }; +const MAX_SCORE: u64 = 2_u64.pow(16); // Note: must be consistent with MAX_SCORE in validator_set.move in iota-framework. +const SCALE_FACTOR: u64 = 2_u64.pow(16); + /// Holds all information related to scoring of authorities in the committee. pub struct Scorer { // The current metrics counts collected by the authority, i.e., the local view of the node @@ -49,8 +52,6 @@ impl Scorer { committee_size, protocol_config, )); - - let max_score = 2_u64.pow(16); let (received_metrics, has_not_sent_report, current_scores, invalid_reports_count) = (0..committee_size) .map(|_| { @@ -60,7 +61,7 @@ impl Scorer { // Initially, none of the authorities had sent any valid report. AtomicBool::new(true), // Current scores initialized to max score. - AtomicU64::new(max_score), + AtomicU64::new(MAX_SCORE), // Invalid reports count initialized to zero. AtomicU64::new(0), ) @@ -427,7 +428,7 @@ mod tests { use iota_types::messages_consensus::{MisbehaviorsV1, VersionedMisbehaviorReport}; use crate::authority::authority_per_epoch_store::scorer::{ - ParametersV1, Scorer, calculate_median_report, calculate_scores_v1, + MAX_SCORE, ParametersV1, SCALE_FACTOR, Scorer, calculate_median_report, calculate_scores_v1, }; fn mock_protocol_config(consensus_choice: ConsensusChoice) -> ProtocolConfig { diff --git a/crates/iota-core/src/checkpoints/checkpoint_executor/tests.rs b/crates/iota-core/src/checkpoints/checkpoint_executor/tests.rs index 6963f6bb26f..1a05cd4f4bf 100644 --- a/crates/iota-core/src/checkpoints/checkpoint_executor/tests.rs +++ b/crates/iota-core/src/checkpoints/checkpoint_executor/tests.rs @@ -467,7 +467,8 @@ async fn sync_end_of_epoch_checkpoint( &authority_state.epoch_store_for_testing().clone(), &GasCostSummary::new(0, 0, 0, 0, 0), *checkpoint.sequence_number(), - 0, // epoch_start_timestamp_ms + 0, // epoch_start_timestamp_ms + vec![], // scores ) .await .expect("Failed to create and execute advance epoch tx"); diff --git a/crates/iota-core/src/checkpoints/mod.rs b/crates/iota-core/src/checkpoints/mod.rs index eeae0532c4f..39cf2d7aaaa 100644 --- a/crates/iota-core/src/checkpoints/mod.rs +++ b/crates/iota-core/src/checkpoints/mod.rs @@ -1623,6 +1623,13 @@ impl CheckpointBuilder { // too frequent and not needed, since scores are not used during the epoch // (except for monitoring purposes, which does not need to be 100% exact) self.epoch_store.scorer.update_scores(); + let scores: Vec = self + .epoch_store + .scorer + .current_scores + .iter() + .map(|x| x.load(std::sync::atomic::Ordering::Relaxed)) + .collect(); let (mut effects, mut signatures): (Vec<_>, Vec<_>) = transactions.into_iter().unzip(); let epoch_rolling_gas_cost_summary = @@ -1636,6 +1643,7 @@ impl CheckpointBuilder { &mut effects, &mut signatures, sequence_number, + scores, ) .await?; @@ -1781,6 +1789,7 @@ impl CheckpointBuilder { checkpoint_effects: &mut Vec, signatures: &mut Vec>, checkpoint: CheckpointSequenceNumber, + scores: Vec, ) -> anyhow::Result<(IotaSystemState, Option)> { let (system_state, system_epoch_info_event, effects) = self .state @@ -1789,6 +1798,7 @@ impl CheckpointBuilder { epoch_total_gas_cost, checkpoint, epoch_start_timestamp_ms, + scores, ) .await?; checkpoint_effects.push(effects); diff --git a/crates/iota-e2e-tests/tests/reconfiguration_tests.rs b/crates/iota-e2e-tests/tests/reconfiguration_tests.rs index 108a5470f48..18f61fe62ed 100644 --- a/crates/iota-e2e-tests/tests/reconfiguration_tests.rs +++ b/crates/iota-e2e-tests/tests/reconfiguration_tests.rs @@ -62,8 +62,9 @@ async fn advance_epoch_tx_test() { .create_and_execute_advance_epoch_tx( &state.epoch_store_for_testing(), &GasCostSummary::new(0, 0, 0, 0, 0), - 0, // checkpoint - 0, // epoch_start_timestamp_ms + 0, // checkpoint + 0, // epoch_start_timestamp_ms + vec![], // scores ) .await .unwrap(); diff --git a/crates/iota-framework/packages/iota-system/sources/iota_system.move b/crates/iota-framework/packages/iota-system/sources/iota_system.move index e7c4e2c144b..cbfe6e6688a 100644 --- a/crates/iota-framework/packages/iota-system/sources/iota_system.move +++ b/crates/iota-framework/packages/iota-system/sources/iota_system.move @@ -532,6 +532,7 @@ fun advance_epoch( epoch_start_timestamp_ms: u64, // Timestamp of the epoch start max_committee_members_count: u64, eligible_active_validators: vector, + scores : vector, ctx: &mut TxContext, ): Balance { let self = load_system_state_mut(wrapper); @@ -550,6 +551,7 @@ fun advance_epoch( epoch_start_timestamp_ms, max_committee_members_count, eligible_active_validators, + scores, ctx, ); @@ -765,6 +767,7 @@ public(package) fun advance_epoch_for_testing( epoch_start_timestamp_ms: u64, max_committee_members_count: u64, eligible_active_validators: vector, + scores : vector, ctx: &mut TxContext, ): Balance { let storage_charge = balance::create_for_testing(storage_charge); @@ -783,6 +786,7 @@ public(package) fun advance_epoch_for_testing( epoch_start_timestamp_ms, max_committee_members_count, eligible_active_validators, + scores, ctx, ); storage_rebate diff --git a/crates/iota-framework/packages/iota-system/sources/iota_system_state_inner.move b/crates/iota-framework/packages/iota-system/sources/iota_system_state_inner.move index 939ad72d6bc..3c783632746 100644 --- a/crates/iota-framework/packages/iota-system/sources/iota_system_state_inner.move +++ b/crates/iota-framework/packages/iota-system/sources/iota_system_state_inner.move @@ -735,6 +735,7 @@ public(package) fun advance_epoch( epoch_start_timestamp_ms: u64, // Timestamp of the epoch start max_committee_members_count: u64, eligible_active_validators: vector, + scores: vector, ctx: &mut TxContext, ): Balance { self.epoch_start_timestamp_ms = epoch_start_timestamp_ms; @@ -792,6 +793,7 @@ public(package) fun advance_epoch( self.parameters.validator_low_stake_grace_period, max_committee_members_count, eligible_active_validators, + scores, ctx, ); diff --git a/crates/iota-framework/packages/iota-system/sources/validator_set.move b/crates/iota-framework/packages/iota-system/sources/validator_set.move index d8eb6d89759..6653d985438 100644 --- a/crates/iota-framework/packages/iota-system/sources/validator_set.move +++ b/crates/iota-framework/packages/iota-system/sources/validator_set.move @@ -140,6 +140,7 @@ const ACTIVE_OR_PENDING_VALIDATOR: u8 = 2; const ANY_VALIDATOR: u8 = 3; const BASIS_POINT_DENOMINATOR: u128 = 10000; +const MAX_SCORE: u128 = 65536; // Note: must be consistent with max score used in iota-core. const MIN_STAKING_THRESHOLD: u64 = 1_000_000_000; // 1 IOTA // Errors @@ -161,6 +162,7 @@ const EValidatorSetEmpty: u64 = 13; const ENotACommitteeValidator: u64 = 14; const EInvalidStakeAmount: u64 = 15; const EInvalidEligibleValidatorIndex: u64 = 16; +const EInvalidRewardAdjustmentData: u64 = 19; const EInvalidCap: u64 = 101; @@ -457,12 +459,13 @@ public(package) fun advance_epoch( low_stake_grace_period: u64, committee_size: u64, eligible_active_validators: vector, + scores: vector, ctx: &mut TxContext, ) { let new_epoch = ctx.epoch() + 1; let total_voting_power = voting_power::total_voting_power(); - // Compute the reward distribution without taking into account the tallying rule slashing. + // Compute the reward distribution without taking into account the scores or reporting. let unadjusted_staking_reward_amounts = compute_unadjusted_reward_distribution( &self.active_validators, &self.committee_members, @@ -482,6 +485,7 @@ public(package) fun advance_epoch( unadjusted_staking_reward_amounts, get_validator_indices_set(&self.active_validators, &slashed_validators), reward_slashing_rate, + scores, ); // Distribute the rewards before adjusting stake so that we immediately start compounding @@ -506,6 +510,7 @@ public(package) fun advance_epoch( &adjusted_staking_reward_amounts, validator_report_records, &slashed_validators, + scores, ); // Collect committee validator addresses before modifying the `active_validators`. @@ -1326,15 +1331,23 @@ fun compute_adjusted_reward_distribution( unadjusted_staking_reward_amounts: vector, slashed_validator_indices_set: VecSet, reward_slashing_rate: u64, + scores: vector, ): vector { let mut adjusted_staking_reward_amounts = vector[]; // Loop through each validator and adjust rewards as necessary let length = committee_members.length(); + assert!(unadjusted_staking_reward_amounts.length() == scores.length(), EInvalidRewardAdjustmentData); + assert!(length == unadjusted_staking_reward_amounts.length(), EInvalidRewardAdjustmentData); + let mut i = 0; while (i < length) { let unadjusted_staking_reward_amount = unadjusted_staking_reward_amounts[i]; + // Calculate staking reward amount adjusted for the validator's score + let score_adjusted_staking_reward_amount = scores[i] as u128 * (unadjusted_staking_reward_amount as u128) + / MAX_SCORE; + // Check if the validator is slashed let adjusted_staking_reward_amount = if ( slashed_validator_indices_set.contains(&committee_members[i]) @@ -1342,15 +1355,14 @@ fun compute_adjusted_reward_distribution( // Use the slashing rate to compute the amount of staking rewards slashed from this punished validator. // Use u128 to avoid multiplication overflow. let staking_reward_adjustment_u128 = - ((unadjusted_staking_reward_amount as u128) * (reward_slashing_rate as u128)) / BASIS_POINT_DENOMINATOR; - unadjusted_staking_reward_amount - (staking_reward_adjustment_u128 as u64) + (score_adjusted_staking_reward_amount * (reward_slashing_rate as u128)) / BASIS_POINT_DENOMINATOR; + score_adjusted_staking_reward_amount - staking_reward_adjustment_u128 } else { // Otherwise, unadjusted staking reward amount is assigned to the unslashed validators - unadjusted_staking_reward_amount + score_adjusted_staking_reward_amount }; - adjusted_staking_reward_amounts.push_back(adjusted_staking_reward_amount); - + adjusted_staking_reward_amounts.push_back(adjusted_staking_reward_amount as u64); // Move to the next validator i = i + 1; }; @@ -1408,6 +1420,7 @@ fun emit_validator_epoch_events( pool_staking_reward_amounts: &vector, report_records: &VecMap>, slashed_validators: &vector
, + scores: vector, ) { assert!(committee_members.length() == pool_staking_reward_amounts.length()); let mut i = 0; @@ -1418,9 +1431,9 @@ fun emit_validator_epoch_events( } else { vector[] }; - let tallying_rule_global_score = if (slashed_validators.contains(&validator_address)) 0 - else 1; let mut committee_member_index = committee_members.find_index!(|c| c == i); + let tallying_rule_global_score = if (slashed_validators.contains(&validator_address) || !committee_member_index.is_some()) 0 + else scores[committee_member_index.extract()]; let pool_staking_reward = if (committee_member_index.is_some()) { // prepare event for a committee validator pool_staking_reward_amounts[committee_member_index.extract()] diff --git a/crates/iota-framework/packages/iota-system/tests/governance_test_utils.move b/crates/iota-framework/packages/iota-system/tests/governance_test_utils.move index 5dc13f7268a..01124259f34 100644 --- a/crates/iota-framework/packages/iota-system/tests/governance_test_utils.move +++ b/crates/iota-framework/packages/iota-system/tests/governance_test_utils.move @@ -190,6 +190,11 @@ public fun advance_epoch_with_reward_amounts_return_rebate_and_max_committee_mem |i| i, ); + let scores = vector::tabulate!( + system_state.committee_validator_addresses().length(), + |_| 65536u64, + ); + let storage_rebate = system_state.advance_epoch_for_testing( new_epoch, 1, @@ -203,6 +208,7 @@ public fun advance_epoch_with_reward_amounts_return_rebate_and_max_committee_mem 0, max_committee_members_count, eligible_active_validators, + scores, ctx, ); test_scenario::return_shared(system_state); @@ -285,6 +291,11 @@ public fun advance_epoch_with_reward_amounts_and_slashing_rates( |i| i, ); + let scores = vector::tabulate!( + system_state.committee_validator_addresses().length(), + |_| 65536u64, + ); + // Use the same value as the default value of max_active_validators. let max_committee_members_count = 150; @@ -301,6 +312,47 @@ public fun advance_epoch_with_reward_amounts_and_slashing_rates( 0, max_committee_members_count, eligible_active_validators, + scores, + ctx, + ); + test_utils::destroy(storage_rebate); + test_scenario::return_shared(system_state); + scenario.next_epoch(@0x0); +} + +public fun advance_epoch_with_subsidy_and_scores( + validator_subsidy: u64, + scores: vector, + scenario: &mut Scenario, +) { + scenario.next_tx(@0x0); + let new_epoch = scenario.ctx().epoch() + 1; + let mut system_state = scenario.take_shared(); + + let ctx = scenario.ctx(); + + let eligible_active_validators = vector::tabulate!( + system_state.validators().active_validators_inner().length(), + |i| i, + ); + + // Use the same value as the default value of max_active_validators. + let max_committee_members_count = 150; + + let storage_rebate = system_state.advance_epoch_for_testing( + new_epoch, + 1, + validator_subsidy * NANOS_PER_IOTA, + 0, + 0, + 0, + 0, + 0, + 10000, // 100% slashing + 0, + max_committee_members_count, + eligible_active_validators, + scores, ctx, ); test_utils::destroy(storage_rebate); diff --git a/crates/iota-framework/packages/iota-system/tests/rewards_distribution_tests.move b/crates/iota-framework/packages/iota-system/tests/rewards_distribution_tests.move index dcaef7d4cdb..d79ff2e140c 100644 --- a/crates/iota-framework/packages/iota-system/tests/rewards_distribution_tests.move +++ b/crates/iota-framework/packages/iota-system/tests/rewards_distribution_tests.move @@ -16,6 +16,7 @@ use iota_system::governance_test_utils::{ advance_epoch_with_reward_amounts_return_rebate, advance_epoch_with_reward_amounts_and_slashing_rates, advance_epoch_with_amounts, + advance_epoch_with_subsidy_and_scores, assert_validator_total_stake_amounts, assert_validator_non_self_stake_amounts, assert_validator_self_stake_amounts, @@ -1458,6 +1459,92 @@ fun test_pool_tokens_minted() { scenario_val.end(); } +#[test] +fun test_rewards_with_scores() { + set_up_iota_system_state(); + let mut scenario_val = test_scenario::begin(VALIDATOR_ADDR_1); + let scenario = &mut scenario_val; + + // Need to advance epoch so validator's staking starts counting. + advance_epoch(scenario); + + // Set validator scores. + let system_state = scenario.take_shared(); + let max_score = 65_536u64; + let scores = vector[ + max_score / 4, // Validator 1 + max_score / 2, // Validator 2 + (max_score * 3) / 4, // Validator 3 + max_score, // Validator 4 + ]; + test_scenario::return_shared(system_state); + + // Advance epoch with 800 IOTA of subsidy and the above scores. + advance_epoch_with_subsidy_and_scores(800, scores, scenario); + + // Check that the rewards were distributed according to the scores. + // Each pool gets +200 IOTA. + assert_validator_self_stake_amounts( + validator_addrs(), + vector[ + (100 + 50) * NANOS_PER_IOTA, // 200 * 1/4 = 50 + (200 + 100) * NANOS_PER_IOTA, // 200 * 1/2 = 100 + (300 + 150) * NANOS_PER_IOTA, // 200 * 3/4 = 150 + (400 + 200) * NANOS_PER_IOTA, // 200 * 1 = 200 + ], + scenario, + ); + + scenario_val.end(); +} + +#[test] +fun test_rewards_with_scores_and_slashing() { + set_up_iota_system_state(); + let mut scenario_val = test_scenario::begin(VALIDATOR_ADDR_1); + let scenario = &mut scenario_val; + + // Need to advance epoch so validator's staking starts counting. + advance_epoch(scenario); + + // Set validator scores. + let system_state = scenario.take_shared(); + let max_score = 65_536u64; + let scores = vector[ + max_score / 4, // Validator 1 + max_score / 2, // Validator 2 + (max_score * 3) / 4, // Validator 3 + max_score, // Validator 4 + ]; + test_scenario::return_shared(system_state); + + // validators 2 and 4 reported by all others, so they get slashed. + report_validator(VALIDATOR_ADDR_1, VALIDATOR_ADDR_2, scenario); + report_validator(VALIDATOR_ADDR_3, VALIDATOR_ADDR_2, scenario); + report_validator(VALIDATOR_ADDR_4, VALIDATOR_ADDR_2, scenario); + report_validator(VALIDATOR_ADDR_1, VALIDATOR_ADDR_4, scenario); + report_validator(VALIDATOR_ADDR_2, VALIDATOR_ADDR_4, scenario); + report_validator(VALIDATOR_ADDR_3, VALIDATOR_ADDR_4, scenario); + + // Advance epoch with 800 IOTA of subsidy and the above scores. + advance_epoch_with_subsidy_and_scores(800, scores, scenario); + + // Check that the rewards were distributed according to the scores. + // Each pool gets +200 IOTA. + assert_validator_self_stake_amounts( + validator_addrs(), + vector[ + (100 + 50) * NANOS_PER_IOTA, // 200 * 1/4 = 50 + (200) * NANOS_PER_IOTA, // slashed + (300 + 150) * NANOS_PER_IOTA, // 200 * 3/4 = 150 + (400) * NANOS_PER_IOTA, // slashed + ], + scenario, + ); + + scenario_val.end(); +} + // This will set up the IOTA system state with the following validator stakes: // Validator 1 => 100 // Validator 2 => 200 diff --git a/crates/iota-framework/packages/iota-system/tests/validator_set_tests.move b/crates/iota-framework/packages/iota-system/tests/validator_set_tests.move index ecfddcf058c..0b4c7bc91d7 100644 --- a/crates/iota-framework/packages/iota-system/tests/validator_set_tests.move +++ b/crates/iota-framework/packages/iota-system/tests/validator_set_tests.move @@ -975,6 +975,11 @@ fun advance_epoch_with_dummy_rewards( |i| i, ); + let scores = vector::tabulate!( + validator_set.committee_validator_addresses().length(), + |_| 65536u64, + ); + validator_set.advance_epoch( &mut dummy_computation_charge, &mut vec_map::empty(), @@ -984,6 +989,7 @@ fun advance_epoch_with_dummy_rewards( 0, // low_stake_grace_period committee_size, eligible_validators, + scores, scenario.ctx(), ); @@ -999,6 +1005,13 @@ fun advance_epoch_with_eligible_validators( scenario.next_epoch(@0x0); let mut dummy_computation_charge = balance::zero(); + + + let scores = vector::tabulate!( + validator_set.committee_validator_addresses().length(), + |_| 65536u64, + ); + validator_set.advance_epoch( &mut dummy_computation_charge, &mut vec_map::empty(), @@ -1008,6 +1021,7 @@ fun advance_epoch_with_eligible_validators( 0, // low_stake_grace_period committee_size, eligible_validators, + scores, scenario.ctx(), ); @@ -1031,6 +1045,11 @@ fun advance_epoch_with_low_stake_params( |i| i, ); + let scores = vector::tabulate!( + validator_set.committee_validator_addresses().length(), + |_| 65536u64, + ); + validator_set.advance_epoch( &mut dummy_computation_charge, &mut vec_map::empty(), @@ -1040,6 +1059,7 @@ fun advance_epoch_with_low_stake_params( low_stake_grace_period, committee_size, eligible_validators, + scores, scenario.ctx(), ); diff --git a/crates/iota-protocol-config/src/lib.rs b/crates/iota-protocol-config/src/lib.rs index 871ea2e0305..6d1df41e64e 100644 --- a/crates/iota-protocol-config/src/lib.rs +++ b/crates/iota-protocol-config/src/lib.rs @@ -1462,7 +1462,12 @@ impl ProtocolConfig { } pub fn score_based_rewards(&self) -> bool { - self.feature_flags.score_based_rewards + let score_based_rewards = self.feature_flags.score_based_rewards; + assert!( + !score_based_rewards || self.scorer_version.is_some(), + "score_based_rewards requires scorer_version to be set" + ); + score_based_rewards } } diff --git a/crates/iota-types/src/iota_system_state/mod.rs b/crates/iota-types/src/iota_system_state/mod.rs index fe3971a6483..5265aa62071 100644 --- a/crates/iota-types/src/iota_system_state/mod.rs +++ b/crates/iota-types/src/iota_system_state/mod.rs @@ -450,6 +450,7 @@ pub struct AdvanceEpochParams { pub epoch_start_timestamp_ms: u64, pub max_committee_members_count: u64, pub eligible_active_validators: Vec, + pub scores: Vec, } #[cfg(msim)] diff --git a/crates/iota-types/src/transaction.rs b/crates/iota-types/src/transaction.rs index 8954217730d..0295b2020ee 100644 --- a/crates/iota-types/src/transaction.rs +++ b/crates/iota-types/src/transaction.rs @@ -494,6 +494,34 @@ impl EndOfEpochTransactionKind { }) } + pub fn new_change_epoch_v4( + next_epoch: EpochId, + protocol_version: ProtocolVersion, + storage_charge: u64, + computation_charge: u64, + computation_charge_burned: u64, + storage_rebate: u64, + non_refundable_storage_fee: u64, + epoch_start_timestamp_ms: u64, + system_packages: Vec<(SequenceNumber, Vec>, Vec)>, + eligible_active_validators: Vec, + scores: Vec, + ) -> Self { + Self::ChangeEpochV4(ChangeEpochV4 { + epoch: next_epoch, + protocol_version, + storage_charge, + computation_charge, + computation_charge_burned, + storage_rebate, + non_refundable_storage_fee, + epoch_start_timestamp_ms, + system_packages, + eligible_active_validators, + scores, + }) + } + pub fn new_authenticator_state_expire( min_epoch: u64, authenticator_obj_initial_shared_version: SequenceNumber, diff --git a/iota-execution/latest/iota-adapter/src/execution_engine.rs b/iota-execution/latest/iota-adapter/src/execution_engine.rs index 910b0803f23..847e9767e50 100644 --- a/iota-execution/latest/iota-adapter/src/execution_engine.rs +++ b/iota-execution/latest/iota-adapter/src/execution_engine.rs @@ -948,6 +948,31 @@ mod checked { construct_advance_epoch_pt_impl(builder, params, call_arg_vec) } + pub fn construct_advance_epoch_pt_v4( + builder: ProgrammableTransactionBuilder, + params: &AdvanceEpochParams, + ) -> Result { + // the first three arguments to the advance_epoch function, namely + // validator_subsidy, storage_charges and computation_charges, are + // common to both v1, v2, v3 and v4 and are added in + // `construct_advance_epoch_pt_impl`. The remaining arguments are added + // here. + let call_arg_vec = vec![ + CallArg::Pure(bcs::to_bytes(¶ms.computation_charge_burned).unwrap()), /* computation_charge_burned: u64 */ + CallArg::IOTA_SYSTEM_MUT, // wrapper: &mut IotaSystemState + CallArg::Pure(bcs::to_bytes(¶ms.epoch).unwrap()), // new_epoch: u64 + CallArg::Pure(bcs::to_bytes(¶ms.next_protocol_version.as_u64()).unwrap()), /* next_protocol_version: u64 */ + CallArg::Pure(bcs::to_bytes(¶ms.storage_rebate).unwrap()), // storage_rebate: u64 + CallArg::Pure(bcs::to_bytes(¶ms.non_refundable_storage_fee).unwrap()), /* non_refundable_storage_fee: u64 */ + CallArg::Pure(bcs::to_bytes(¶ms.reward_slashing_rate).unwrap()), /* reward_slashing_rate: u64 */ + CallArg::Pure(bcs::to_bytes(¶ms.epoch_start_timestamp_ms).unwrap()), /* epoch_start_timestamp_ms: u64 */ + CallArg::Pure(bcs::to_bytes(¶ms.max_committee_members_count).unwrap()), /* max_committee_members_count: u64 */ + CallArg::Pure(bcs::to_bytes(¶ms.eligible_active_validators).unwrap()), /* eligible_active_validators: Vec */ + CallArg::Pure(bcs::to_bytes(¶ms.scores).unwrap()), // scores: Vec + ]; + construct_advance_epoch_pt_impl(builder, params, call_arg_vec) + } + /// Advances the epoch by executing a `ProgrammableTransaction`. If the /// transaction fails, it switches to safe mode and retries the epoch /// advancement in a more controlled environment. The function also @@ -1045,6 +1070,7 @@ mod checked { // separate AdvanceEpochParams struct. max_committee_members_count: 0, eligible_active_validators: vec![], + scores: vec![], }; let advance_epoch_pt = construct_advance_epoch_pt_v1(builder, ¶ms)?; advance_epoch_impl( @@ -1087,9 +1113,10 @@ mod checked { reward_slashing_rate: protocol_config.reward_slashing_rate(), epoch_start_timestamp_ms: change_epoch_v2.epoch_start_timestamp_ms, max_committee_members_count: protocol_config.max_committee_members_count(), - // AdvanceEpochV2 does not use this field, but keeping them to avoid creating a + // AdvanceEpochV2 does not use these fields, but keeping them to avoid creating a // separate AdvanceEpochParams struct. eligible_active_validators: vec![], + scores: vec![], }; let advance_epoch_pt = construct_advance_epoch_pt_v2(builder, ¶ms)?; advance_epoch_impl( @@ -1133,6 +1160,9 @@ mod checked { epoch_start_timestamp_ms: change_epoch_v3.epoch_start_timestamp_ms, max_committee_members_count: protocol_config.max_committee_members_count(), eligible_active_validators: change_epoch_v3.eligible_active_validators, + // AdvanceEpochV3 does not use these fields, but keeping them to avoid creating a + // separate AdvanceEpochParams struct. + scores: vec![], }; let advance_epoch_pt = construct_advance_epoch_pt_v3(builder, ¶ms)?; advance_epoch_impl( @@ -1163,7 +1193,6 @@ mod checked { metrics: Arc, trace_builder_opt: &mut Option, ) -> Result<(), ExecutionError> { - // To do: pass scores let params = AdvanceEpochParams { epoch: change_epoch_v4.epoch, next_protocol_version: change_epoch_v4.protocol_version, @@ -1177,8 +1206,9 @@ mod checked { epoch_start_timestamp_ms: change_epoch_v4.epoch_start_timestamp_ms, max_committee_members_count: protocol_config.max_committee_members_count(), eligible_active_validators: change_epoch_v4.eligible_active_validators, + scores: change_epoch_v4.scores, }; - let advance_epoch_pt = construct_advance_epoch_pt_v3(builder, ¶ms)?; + let advance_epoch_pt = construct_advance_epoch_pt_v4(builder, ¶ms)?; advance_epoch_impl( advance_epoch_pt, params, From d5f5cc60ae7803d6df7dcf365f7d27927f48fe12 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 2 Dec 2025 11:22:16 +0000 Subject: [PATCH 6/8] compile move packages --- .../packages_compiled/iota-system | Bin 48175 -> 48295 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/crates/iota-framework/packages_compiled/iota-system b/crates/iota-framework/packages_compiled/iota-system index 38b39f5f2ed49e3b84d831c3ace201e7670936c3..2b4a14f4b37a97b860aa88473340467094e8d573 100644 GIT binary patch delta 1936 zcmYjSZ-`yR6~E`+nLGELGk5Ntx9|UZ+3ef3KEY(&tl8{Mn#6r;5~cVLW1`t0iAgY< zKTTb*mO|=Et%x6##Oa2{ra?ub6hz@6NIry65g{rfv?!9+PeL&Ci{wM3mU`~n*xk#! z=g#?^GiPSb`JH+1T=(Dn-hX$`2KcYryJ1E9idHA9ZwyB7P3#R9*1h24+3W)7%j+*r z#%JaRlk;=Evm1UVfm~~<-n)J3Rv4Bc1{jcqfIm`URAzvGiXw;#DoT`lx?A-z=q+?- zgD4ULgCG!2Y~5I>@El2X2zqyKolm7mJrCUZ9Us6z@5!B;VP%~VAq)5wnIy0e6g{1o zB=x#mb*k#_^M{W>-EDg0`LU>D!2TYzKraOpSvVwygi^De0#KuAk73`h(us3S&)rMo+dvu(8P-HjhB6_54h8`O9 zIoJg2f+U6HGlG^EOJ!CX5|7D)`JoWhK!<%48H%Y$*t@p-3km2xd2BOF+V?0V?$Eyd zJpOdwUsxX5zZ#0}G)r_J?O$C+o}DUA9$X5{$Uxhwhr+uIgH$*R{G}plglj+P2U#%|li@*azF9 zA{&c9cq8+kjxfT)h`8SXM$@*WwGsWn1nG&I5)F;U=Zvr|TQ8;&F<>}oXd)!4YALr6 zYB@wwhq0n??G{VXvcg8Pm8VL%vBSeK;I<#$35VQY4==#mZtBSJ{7L!0i=Cy-Bm}yS zb#8!Y2`o~iD_F$Jhi$v{0B-Eq`Oe*ko}(tE6=`cN5P720x+kjC>B3X2cZh67#9W2YUus zoXEw=TpXtoHPExq1J$Tn^s~*BEZbW`#bsKNd$Qhp>*!iwqWtrhe}n7p)nmJvIzJpc z1kK*#zp4W(&pdIM<<%$dVrJbwcLBzG-^~pL8^4}@t#y_1O=}iuA7T1qxVPmhJU*42tGUZ5$H>6-b&-|x~r12z59N>AZm$fI`V~;ZK*{AhDe`1+dP`Rp`d^327EmKQdN{!9@N`8wa znk@06Q~4f?B#{3u?1Oo2)3}5UFSR8a;6if4rbYV@HK<7`Um$h&t!r%ioM)vf^Z!qb zw)h0dOQYqGPUJrrgu=1`CE`nvUx6xgsMPz#A4h!nyE}HOIe0ZtA4aE>=wH>n;f2~d zyjF|!548mqpBxg&LhsE}#h{i11Q0EXKt*1^C|ou7+39ccl4=H)dEBVkrB`Y+*d}3b f;R~8iJg)q9zh(*KHS;=@ynL?Vlj$|il`B62_wx(` delta 1757 zcmYjRU2GLq5Z>9{vuF3r*`ITJfA-#%(o*0;QVK;%mqxI?(xQn*qWrahlpm!O6)=G) z0cr>dF$M(>AcBG>1fGZ_tHuyRzz2OWA;y>x^#y%VBSsS;L0@q80=74M=giERGv~~F zGjskQ^P@ZF#dSS!L=X2&YP})(>!q{J?uD*l=g|B=K>XW%8RTaR?#%ZuZf_2+ZBI}4 z>}4QyQ*Sx#?wtwUwj(TIp#sZdT(|`i>Eq~v>v1=9NP3`jFftigGDooVRNsijxkYC! zOJo1TZh?4aVL4seKNjhqmVcUlxS|W7q_3}>MD+H`twh(aItX0xwg9F508SIRBHLx?wS*(*R$qa__4f#xY7!ao0H8zmxg`P1q zhnx=%O$ATy8;Yk!RWw2et;NdZmSNb%z)V7JPq!T=vpX%|p}sV<3|t+q?jGAKybiV5 z0ps4UC{djP844p}^ye4~=tx4~-w9EFcefu1Yo>eWcf;mo}}r&L|tV)-NLZ(1x`T=p!385`TNc`$YA|S)lY) zqMrV9b!k^DNHt+Mqi(WmfApEr(+I$);aT!twOx!%)z(N7{*BGjbea z{r5;KFkRU4CN%4_Tb7J<@sf0e(Wj*A+|Q^Xxu6njvpbm#K=7K%OA(k99-lcow%~*651!upa~@Gh1xNlzwkCf}FlE zx&qc}d+RayK%d+?W$XaEd!;)59v?xLM5(V|@^NK^T;@e2@@@7p5D}qJLRIURv53`v zkQHGEm89M7W^?M?!hDiQmo4D2bkUVX!Dzc;*Tx-sZ1Md!j~NC<=E;IEWm%LdInKCQ zfR1bsfoN#6pBDiYURi`S75J_8dMpEm$rLHrCBLZd*-tFtTk6l+$mwm9wV5xqX!A${kCnOwR1M4AatQUv9DBdp)+ZY4Qoq zZv|H-yB~K9J4a`46?bQU1@gv9Ze z1ckt8>R9QaJ-dqCKLTbg=M;p;ao#jruqdkuAGk}^9P-4MMc>9^LM3N}E{O%7VL|uk zT3ab#NgWB{lw^!n)>~vGh2Np3$WSWTsL$xBW8_R!gGRD#qHV)(B+AgEW`QWk*O&0# z66Lc*3tzoJT0`xVx%#m@HetRbE7Z6~s0!F9BMgHmF^k!N2_;5ISN3Kv%%Lr4{Hmv^ z7>vlz+{P+T2$hf%pHs`Qj1>&2zhk}TXl39VIu-H6{2%L9`)fcWawn=}z`i3TcoP^5 z?CZe32kup|A^rSln+ezSjpL2YzgYaL`)=s|P#ShF#q-5qu`BPyn)`3G*x_4g`JJ3a z@U)+d^}xxWivf2fp@9JIJ(6K#rM;(OOj^)ZGiXdwaC5oYS_dbngJV=NsE;xOY5G&@ RDQ3*Np3;YY`u4d(@jow*{aFA2 From 6afef5af648422ef678d6c513022c52cd36b9b1d Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 2 Dec 2025 15:03:20 +0000 Subject: [PATCH 7/8] update protocol config and snapshots --- crates/iota-protocol-config/src/lib.rs | 13 +- ...ocol_config__test__Mainnet_version_18.snap | 303 +++++++++++++++++ ...ocol_config__test__Testnet_version_18.snap | 304 +++++++++++++++++ ...ota_protocol_config__test__version_16.snap | 2 +- ...ota_protocol_config__test__version_18.snap | 316 ++++++++++++++++++ 5 files changed, 932 insertions(+), 6 deletions(-) create mode 100644 crates/iota-protocol-config/src/snapshots/iota_protocol_config__test__Mainnet_version_18.snap create mode 100644 crates/iota-protocol-config/src/snapshots/iota_protocol_config__test__Testnet_version_18.snap create mode 100644 crates/iota-protocol-config/src/snapshots/iota_protocol_config__test__version_18.snap diff --git a/crates/iota-protocol-config/src/lib.rs b/crates/iota-protocol-config/src/lib.rs index 6d1df41e64e..18b1e0865f9 100644 --- a/crates/iota-protocol-config/src/lib.rs +++ b/crates/iota-protocol-config/src/lib.rs @@ -19,7 +19,7 @@ use tracing::{info, warn}; /// The minimum and maximum protocol versions supported by this build. const MIN_PROTOCOL_VERSION: u64 = 1; -pub const MAX_PROTOCOL_VERSION: u64 = 17; +pub const MAX_PROTOCOL_VERSION: u64 = 18; // Record history of protocol version allocations here: // @@ -92,6 +92,7 @@ pub const MAX_PROTOCOL_VERSION: u64 = 17; // Enable committing transactions only for traversed headers in // Starfish. // Version 17: Increase the committee size to 100 on all networks. +// Version 18: Enable score based rewards on devnet. #[derive(Copy, Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct ProtocolVersion(u64); @@ -2340,15 +2341,17 @@ impl ProtocolConfig { // Enable committing transactions only for traversed headers in Starfish cfg.feature_flags .consensus_commit_transactions_only_for_traversed_headers = true; - // Enables score based rewards on Devnet - if chain != Chain::Testnet && chain != Chain::Mainnet { - cfg.feature_flags.score_based_rewards = true; - } } 17 => { // Increase the committee size to 100 on all networks. cfg.max_committee_members_count = Some(100); } + 18 => { + // Enables score based rewards on Devnet + if chain != Chain::Testnet && chain != Chain::Mainnet { + cfg.feature_flags.score_based_rewards = true; + } + } // Use this template when making changes: // // // modify an existing constant. diff --git a/crates/iota-protocol-config/src/snapshots/iota_protocol_config__test__Mainnet_version_18.snap b/crates/iota-protocol-config/src/snapshots/iota_protocol_config__test__Mainnet_version_18.snap new file mode 100644 index 00000000000..e30b0944539 --- /dev/null +++ b/crates/iota-protocol-config/src/snapshots/iota_protocol_config__test__Mainnet_version_18.snap @@ -0,0 +1,303 @@ +--- +source: crates/iota-protocol-config/src/lib.rs +expression: "ProtocolConfig::get_for_version(cur, *chain_id)" +snapshot_kind: text +--- +version: 18 +feature_flags: + consensus_transaction_ordering: ByGasPrice + per_object_congestion_control_mode: TotalTxCount + zklogin_max_epoch_upper_bound_delta: 30 + relocate_event_module: true + protocol_defined_base_fee: true + disallow_new_modules_in_deps_only_packages: true + native_charging_v2: true + convert_type_argument_error: true + consensus_round_prober: true + consensus_distributed_vote_scoring_strategy: true + consensus_linearize_subdag_v2: true + variant_nodes: true + consensus_round_prober_probe_accepted_rounds: true + consensus_zstd_compression: true + congestion_control_min_free_execution_slot: true + consensus_batched_block_sync: true + congestion_control_gas_price_feedback_mechanism: true + validate_identifier_inputs: true + minimize_child_object_mutations: true + dependency_linkage_error: true + additional_multisig_checks: true + normalize_ptb_arguments: true + select_committee_from_eligible_validators: true + track_non_committee_eligible_validators: true + select_committee_supporting_next_epoch_version: true + consensus_commit_transactions_only_for_traversed_headers: true +max_tx_size_bytes: 131072 +max_input_objects: 2048 +max_size_written_objects: 5000000 +max_size_written_objects_system_tx: 50000000 +max_serialized_tx_effects_size_bytes: 524288 +max_serialized_tx_effects_size_bytes_system_tx: 8388608 +max_gas_payment_objects: 256 +max_modules_in_publish: 64 +max_package_dependencies: 32 +max_arguments: 512 +max_type_arguments: 16 +max_type_argument_depth: 16 +max_pure_argument_size: 16384 +max_programmable_tx_commands: 1024 +move_binary_format_version: 7 +min_move_binary_format_version: 6 +binary_module_handles: 100 +binary_struct_handles: 300 +binary_function_handles: 1500 +binary_function_instantiations: 750 +binary_signatures: 1000 +binary_constant_pool: 4000 +binary_identifiers: 10000 +binary_address_identifiers: 100 +binary_struct_defs: 200 +binary_struct_def_instantiations: 100 +binary_function_defs: 1000 +binary_field_handles: 500 +binary_field_instantiations: 250 +binary_friend_decls: 100 +max_move_object_size: 256000 +max_move_package_size: 102400 +max_publish_or_upgrade_per_ptb: 5 +max_tx_gas: 50000000000 +max_gas_price: 100000 +max_gas_computation_bucket: 5000000 +gas_rounding_step: 1000 +max_loop_depth: 5 +max_generic_instantiation_length: 32 +max_function_parameters: 128 +max_basic_blocks: 1024 +max_value_stack_size: 1024 +max_type_nodes: 256 +max_push_size: 10000 +max_struct_definitions: 200 +max_function_definitions: 1000 +max_fields_in_struct: 32 +max_dependency_depth: 100 +max_num_event_emit: 1024 +max_num_new_move_object_ids: 2048 +max_num_new_move_object_ids_system_tx: 32768 +max_num_deleted_move_object_ids: 2048 +max_num_deleted_move_object_ids_system_tx: 32768 +max_num_transferred_move_object_ids: 2048 +max_num_transferred_move_object_ids_system_tx: 32768 +max_event_emit_size: 256000 +max_event_emit_size_total: 65536000 +max_move_vector_len: 262144 +max_move_identifier_len: 128 +max_move_value_depth: 128 +max_back_edges_per_function: 10000 +max_back_edges_per_module: 10000 +max_verifier_meter_ticks_per_function: 16000000 +max_meter_ticks_per_module: 16000000 +max_meter_ticks_per_package: 16000000 +object_runtime_max_num_cached_objects: 1000 +object_runtime_max_num_cached_objects_system_tx: 16000 +object_runtime_max_num_store_entries: 1000 +object_runtime_max_num_store_entries_system_tx: 16000 +base_tx_cost_fixed: 1000 +package_publish_cost_fixed: 1000 +base_tx_cost_per_byte: 0 +package_publish_cost_per_byte: 80 +obj_access_cost_read_per_byte: 15 +obj_access_cost_mutate_per_byte: 40 +obj_access_cost_delete_per_byte: 40 +obj_access_cost_verify_per_byte: 200 +max_type_to_layout_nodes: 512 +max_ptb_value_size: 1048576 +gas_model_version: 2 +obj_data_cost_refundable: 100 +obj_metadata_cost_non_refundable: 50 +storage_rebate_rate: 10000 +reward_slashing_rate: 10000 +storage_gas_price: 76 +base_gas_price: 1000 +validator_target_reward: 767000000000000 +max_transactions_per_checkpoint: 10000 +max_checkpoint_size_bytes: 31457280 +buffer_stake_for_protocol_upgrade_bps: 5000 +address_from_bytes_cost_base: 52 +address_to_u256_cost_base: 52 +address_from_u256_cost_base: 52 +config_read_setting_impl_cost_base: 100 +config_read_setting_impl_cost_per_byte: 40 +dynamic_field_hash_type_and_key_cost_base: 100 +dynamic_field_hash_type_and_key_type_cost_per_byte: 2 +dynamic_field_hash_type_and_key_value_cost_per_byte: 2 +dynamic_field_hash_type_and_key_type_tag_cost_per_byte: 2 +dynamic_field_add_child_object_cost_base: 100 +dynamic_field_add_child_object_type_cost_per_byte: 10 +dynamic_field_add_child_object_value_cost_per_byte: 10 +dynamic_field_add_child_object_struct_tag_cost_per_byte: 10 +dynamic_field_borrow_child_object_cost_base: 100 +dynamic_field_borrow_child_object_child_ref_cost_per_byte: 10 +dynamic_field_borrow_child_object_type_cost_per_byte: 10 +dynamic_field_remove_child_object_cost_base: 100 +dynamic_field_remove_child_object_child_cost_per_byte: 2 +dynamic_field_remove_child_object_type_cost_per_byte: 2 +dynamic_field_has_child_object_cost_base: 100 +dynamic_field_has_child_object_with_ty_cost_base: 100 +dynamic_field_has_child_object_with_ty_type_cost_per_byte: 2 +dynamic_field_has_child_object_with_ty_type_tag_cost_per_byte: 2 +event_emit_cost_base: 52 +event_emit_value_size_derivation_cost_per_byte: 2 +event_emit_tag_size_derivation_cost_per_byte: 5 +event_emit_output_cost_per_byte: 10 +object_borrow_uid_cost_base: 52 +object_delete_impl_cost_base: 52 +object_record_new_uid_cost_base: 52 +transfer_transfer_internal_cost_base: 52 +transfer_freeze_object_cost_base: 52 +transfer_share_object_cost_base: 52 +transfer_receive_object_cost_base: 52 +tx_context_derive_id_cost_base: 52 +types_is_one_time_witness_cost_base: 52 +types_is_one_time_witness_type_tag_cost_per_byte: 2 +types_is_one_time_witness_type_cost_per_byte: 2 +validator_validate_metadata_cost_base: 20000 +validator_validate_metadata_data_cost_per_byte: 2 +crypto_invalid_arguments_cost: 100 +bls12381_bls12381_min_sig_verify_cost_base: 44064 +bls12381_bls12381_min_sig_verify_msg_cost_per_byte: 2 +bls12381_bls12381_min_sig_verify_msg_cost_per_block: 2 +bls12381_bls12381_min_pk_verify_cost_base: 49282 +bls12381_bls12381_min_pk_verify_msg_cost_per_byte: 2 +bls12381_bls12381_min_pk_verify_msg_cost_per_block: 2 +ecdsa_k1_ecrecover_keccak256_cost_base: 500 +ecdsa_k1_ecrecover_keccak256_msg_cost_per_byte: 2 +ecdsa_k1_ecrecover_keccak256_msg_cost_per_block: 2 +ecdsa_k1_ecrecover_sha256_cost_base: 500 +ecdsa_k1_ecrecover_sha256_msg_cost_per_byte: 2 +ecdsa_k1_ecrecover_sha256_msg_cost_per_block: 2 +ecdsa_k1_decompress_pubkey_cost_base: 52 +ecdsa_k1_secp256k1_verify_keccak256_cost_base: 1470 +ecdsa_k1_secp256k1_verify_keccak256_msg_cost_per_byte: 2 +ecdsa_k1_secp256k1_verify_keccak256_msg_cost_per_block: 2 +ecdsa_k1_secp256k1_verify_sha256_cost_base: 1470 +ecdsa_k1_secp256k1_verify_sha256_msg_cost_per_byte: 2 +ecdsa_k1_secp256k1_verify_sha256_msg_cost_per_block: 2 +ecdsa_r1_ecrecover_keccak256_cost_base: 1173 +ecdsa_r1_ecrecover_keccak256_msg_cost_per_byte: 2 +ecdsa_r1_ecrecover_keccak256_msg_cost_per_block: 2 +ecdsa_r1_ecrecover_sha256_cost_base: 1173 +ecdsa_r1_ecrecover_sha256_msg_cost_per_byte: 2 +ecdsa_r1_ecrecover_sha256_msg_cost_per_block: 2 +ecdsa_r1_secp256r1_verify_keccak256_cost_base: 4225 +ecdsa_r1_secp256r1_verify_keccak256_msg_cost_per_byte: 2 +ecdsa_r1_secp256r1_verify_keccak256_msg_cost_per_block: 2 +ecdsa_r1_secp256r1_verify_sha256_cost_base: 4225 +ecdsa_r1_secp256r1_verify_sha256_msg_cost_per_byte: 2 +ecdsa_r1_secp256r1_verify_sha256_msg_cost_per_block: 2 +ecvrf_ecvrf_verify_cost_base: 4848 +ecvrf_ecvrf_verify_alpha_string_cost_per_byte: 2 +ecvrf_ecvrf_verify_alpha_string_cost_per_block: 2 +ed25519_ed25519_verify_cost_base: 1802 +ed25519_ed25519_verify_msg_cost_per_byte: 2 +ed25519_ed25519_verify_msg_cost_per_block: 2 +groth16_prepare_verifying_key_bls12381_cost_base: 53838 +groth16_prepare_verifying_key_bn254_cost_base: 82010 +groth16_verify_groth16_proof_internal_bls12381_cost_base: 72090 +groth16_verify_groth16_proof_internal_bls12381_cost_per_public_input: 8213 +groth16_verify_groth16_proof_internal_bn254_cost_base: 115502 +groth16_verify_groth16_proof_internal_bn254_cost_per_public_input: 9484 +groth16_verify_groth16_proof_internal_public_input_cost_per_byte: 2 +hash_blake2b256_cost_base: 10 +hash_blake2b256_data_cost_per_byte: 2 +hash_blake2b256_data_cost_per_block: 2 +hash_keccak256_cost_base: 10 +hash_keccak256_data_cost_per_byte: 2 +hash_keccak256_data_cost_per_block: 2 +poseidon_bn254_cost_per_block: 388 +group_ops_bls12381_decode_scalar_cost: 7 +group_ops_bls12381_decode_g1_cost: 2848 +group_ops_bls12381_decode_g2_cost: 3770 +group_ops_bls12381_decode_gt_cost: 3068 +group_ops_bls12381_scalar_add_cost: 10 +group_ops_bls12381_g1_add_cost: 1556 +group_ops_bls12381_g2_add_cost: 3048 +group_ops_bls12381_gt_add_cost: 188 +group_ops_bls12381_scalar_sub_cost: 10 +group_ops_bls12381_g1_sub_cost: 1550 +group_ops_bls12381_g2_sub_cost: 3019 +group_ops_bls12381_gt_sub_cost: 497 +group_ops_bls12381_scalar_mul_cost: 11 +group_ops_bls12381_g1_mul_cost: 4842 +group_ops_bls12381_g2_mul_cost: 9108 +group_ops_bls12381_gt_mul_cost: 27490 +group_ops_bls12381_scalar_div_cost: 91 +group_ops_bls12381_g1_div_cost: 5091 +group_ops_bls12381_g2_div_cost: 9206 +group_ops_bls12381_gt_div_cost: 27804 +group_ops_bls12381_g1_hash_to_base_cost: 2962 +group_ops_bls12381_g2_hash_to_base_cost: 8688 +group_ops_bls12381_g1_hash_to_cost_per_byte: 2 +group_ops_bls12381_g2_hash_to_cost_per_byte: 2 +group_ops_bls12381_g1_msm_base_cost: 62648 +group_ops_bls12381_g2_msm_base_cost: 131192 +group_ops_bls12381_g1_msm_base_cost_per_input: 1333 +group_ops_bls12381_g2_msm_base_cost_per_input: 3216 +group_ops_bls12381_msm_max_len: 32 +group_ops_bls12381_pairing_cost: 26897 +group_ops_bls12381_g1_to_uncompressed_g1_cost: 2099 +group_ops_bls12381_uncompressed_g1_to_g1_cost: 677 +group_ops_bls12381_uncompressed_g1_sum_base_cost: 77 +group_ops_bls12381_uncompressed_g1_sum_cost_per_term: 26 +group_ops_bls12381_uncompressed_g1_sum_max_terms: 1200 +hmac_hmac_sha3_256_cost_base: 52 +hmac_hmac_sha3_256_input_cost_per_byte: 2 +hmac_hmac_sha3_256_input_cost_per_block: 2 +check_zklogin_id_cost_base: 200 +check_zklogin_issuer_cost_base: 200 +bcs_per_byte_serialized_cost: 2 +bcs_legacy_min_output_size_cost: 1 +bcs_failure_cost: 52 +hash_sha2_256_base_cost: 52 +hash_sha2_256_per_byte_cost: 2 +hash_sha2_256_legacy_min_input_len_cost: 1 +hash_sha3_256_base_cost: 52 +hash_sha3_256_per_byte_cost: 2 +hash_sha3_256_legacy_min_input_len_cost: 1 +type_name_get_base_cost: 52 +type_name_get_per_byte_cost: 2 +string_check_utf8_base_cost: 52 +string_check_utf8_per_byte_cost: 2 +string_is_char_boundary_base_cost: 52 +string_sub_string_base_cost: 52 +string_sub_string_per_byte_cost: 2 +string_index_of_base_cost: 52 +string_index_of_per_byte_pattern_cost: 2 +string_index_of_per_byte_searched_cost: 2 +vector_empty_base_cost: 52 +vector_length_base_cost: 52 +vector_push_back_base_cost: 52 +vector_push_back_legacy_per_abstract_memory_unit_cost: 2 +vector_borrow_base_cost: 52 +vector_pop_back_base_cost: 52 +vector_destroy_empty_base_cost: 52 +vector_swap_base_cost: 52 +debug_print_base_cost: 52 +debug_print_stack_trace_base_cost: 52 +execution_version: 1 +consensus_bad_nodes_stake_threshold: 20 +max_jwk_votes_per_validator_per_epoch: 240 +max_age_of_jwk_in_epochs: 1 +random_beacon_reduction_allowed_delta: 800 +random_beacon_reduction_lower_bound: 1000 +random_beacon_dkg_timeout_round: 3000 +random_beacon_min_round_interval_ms: 500 +random_beacon_dkg_version: 1 +consensus_max_transaction_size_bytes: 262144 +consensus_max_transactions_in_block_bytes: 524288 +consensus_max_num_transactions_in_block: 512 +max_deferral_rounds_for_congestion_control: 10 +min_checkpoint_interval_ms: 200 +checkpoint_summary_version_specific_data: 1 +max_soft_bundle_size: 5 +max_accumulated_txn_cost_per_object_in_mysticeti_commit: 10 +max_committee_members_count: 100 +consensus_gc_depth: 60 diff --git a/crates/iota-protocol-config/src/snapshots/iota_protocol_config__test__Testnet_version_18.snap b/crates/iota-protocol-config/src/snapshots/iota_protocol_config__test__Testnet_version_18.snap new file mode 100644 index 00000000000..eb5d413555a --- /dev/null +++ b/crates/iota-protocol-config/src/snapshots/iota_protocol_config__test__Testnet_version_18.snap @@ -0,0 +1,304 @@ +--- +source: crates/iota-protocol-config/src/lib.rs +expression: "ProtocolConfig::get_for_version(cur, *chain_id)" +snapshot_kind: text +--- +version: 18 +feature_flags: + consensus_transaction_ordering: ByGasPrice + per_object_congestion_control_mode: TotalTxCount + zklogin_max_epoch_upper_bound_delta: 30 + relocate_event_module: true + protocol_defined_base_fee: true + disallow_new_modules_in_deps_only_packages: true + native_charging_v2: true + convert_type_argument_error: true + consensus_round_prober: true + consensus_distributed_vote_scoring_strategy: true + consensus_linearize_subdag_v2: true + variant_nodes: true + consensus_round_prober_probe_accepted_rounds: true + consensus_zstd_compression: true + congestion_control_min_free_execution_slot: true + consensus_batched_block_sync: true + congestion_control_gas_price_feedback_mechanism: true + validate_identifier_inputs: true + minimize_child_object_mutations: true + dependency_linkage_error: true + additional_multisig_checks: true + normalize_ptb_arguments: true + select_committee_from_eligible_validators: true + track_non_committee_eligible_validators: true + select_committee_supporting_next_epoch_version: true + consensus_median_timestamp_with_checkpoint_enforcement: true + consensus_commit_transactions_only_for_traversed_headers: true +max_tx_size_bytes: 131072 +max_input_objects: 2048 +max_size_written_objects: 5000000 +max_size_written_objects_system_tx: 50000000 +max_serialized_tx_effects_size_bytes: 524288 +max_serialized_tx_effects_size_bytes_system_tx: 8388608 +max_gas_payment_objects: 256 +max_modules_in_publish: 64 +max_package_dependencies: 32 +max_arguments: 512 +max_type_arguments: 16 +max_type_argument_depth: 16 +max_pure_argument_size: 16384 +max_programmable_tx_commands: 1024 +move_binary_format_version: 7 +min_move_binary_format_version: 6 +binary_module_handles: 100 +binary_struct_handles: 300 +binary_function_handles: 1500 +binary_function_instantiations: 750 +binary_signatures: 1000 +binary_constant_pool: 4000 +binary_identifiers: 10000 +binary_address_identifiers: 100 +binary_struct_defs: 200 +binary_struct_def_instantiations: 100 +binary_function_defs: 1000 +binary_field_handles: 500 +binary_field_instantiations: 250 +binary_friend_decls: 100 +max_move_object_size: 256000 +max_move_package_size: 102400 +max_publish_or_upgrade_per_ptb: 5 +max_tx_gas: 50000000000 +max_gas_price: 100000 +max_gas_computation_bucket: 5000000 +gas_rounding_step: 1000 +max_loop_depth: 5 +max_generic_instantiation_length: 32 +max_function_parameters: 128 +max_basic_blocks: 1024 +max_value_stack_size: 1024 +max_type_nodes: 256 +max_push_size: 10000 +max_struct_definitions: 200 +max_function_definitions: 1000 +max_fields_in_struct: 32 +max_dependency_depth: 100 +max_num_event_emit: 1024 +max_num_new_move_object_ids: 2048 +max_num_new_move_object_ids_system_tx: 32768 +max_num_deleted_move_object_ids: 2048 +max_num_deleted_move_object_ids_system_tx: 32768 +max_num_transferred_move_object_ids: 2048 +max_num_transferred_move_object_ids_system_tx: 32768 +max_event_emit_size: 256000 +max_event_emit_size_total: 65536000 +max_move_vector_len: 262144 +max_move_identifier_len: 128 +max_move_value_depth: 128 +max_back_edges_per_function: 10000 +max_back_edges_per_module: 10000 +max_verifier_meter_ticks_per_function: 16000000 +max_meter_ticks_per_module: 16000000 +max_meter_ticks_per_package: 16000000 +object_runtime_max_num_cached_objects: 1000 +object_runtime_max_num_cached_objects_system_tx: 16000 +object_runtime_max_num_store_entries: 1000 +object_runtime_max_num_store_entries_system_tx: 16000 +base_tx_cost_fixed: 1000 +package_publish_cost_fixed: 1000 +base_tx_cost_per_byte: 0 +package_publish_cost_per_byte: 80 +obj_access_cost_read_per_byte: 15 +obj_access_cost_mutate_per_byte: 40 +obj_access_cost_delete_per_byte: 40 +obj_access_cost_verify_per_byte: 200 +max_type_to_layout_nodes: 512 +max_ptb_value_size: 1048576 +gas_model_version: 2 +obj_data_cost_refundable: 100 +obj_metadata_cost_non_refundable: 50 +storage_rebate_rate: 10000 +reward_slashing_rate: 10000 +storage_gas_price: 76 +base_gas_price: 1000 +validator_target_reward: 767000000000000 +max_transactions_per_checkpoint: 10000 +max_checkpoint_size_bytes: 31457280 +buffer_stake_for_protocol_upgrade_bps: 5000 +address_from_bytes_cost_base: 52 +address_to_u256_cost_base: 52 +address_from_u256_cost_base: 52 +config_read_setting_impl_cost_base: 100 +config_read_setting_impl_cost_per_byte: 40 +dynamic_field_hash_type_and_key_cost_base: 100 +dynamic_field_hash_type_and_key_type_cost_per_byte: 2 +dynamic_field_hash_type_and_key_value_cost_per_byte: 2 +dynamic_field_hash_type_and_key_type_tag_cost_per_byte: 2 +dynamic_field_add_child_object_cost_base: 100 +dynamic_field_add_child_object_type_cost_per_byte: 10 +dynamic_field_add_child_object_value_cost_per_byte: 10 +dynamic_field_add_child_object_struct_tag_cost_per_byte: 10 +dynamic_field_borrow_child_object_cost_base: 100 +dynamic_field_borrow_child_object_child_ref_cost_per_byte: 10 +dynamic_field_borrow_child_object_type_cost_per_byte: 10 +dynamic_field_remove_child_object_cost_base: 100 +dynamic_field_remove_child_object_child_cost_per_byte: 2 +dynamic_field_remove_child_object_type_cost_per_byte: 2 +dynamic_field_has_child_object_cost_base: 100 +dynamic_field_has_child_object_with_ty_cost_base: 100 +dynamic_field_has_child_object_with_ty_type_cost_per_byte: 2 +dynamic_field_has_child_object_with_ty_type_tag_cost_per_byte: 2 +event_emit_cost_base: 52 +event_emit_value_size_derivation_cost_per_byte: 2 +event_emit_tag_size_derivation_cost_per_byte: 5 +event_emit_output_cost_per_byte: 10 +object_borrow_uid_cost_base: 52 +object_delete_impl_cost_base: 52 +object_record_new_uid_cost_base: 52 +transfer_transfer_internal_cost_base: 52 +transfer_freeze_object_cost_base: 52 +transfer_share_object_cost_base: 52 +transfer_receive_object_cost_base: 52 +tx_context_derive_id_cost_base: 52 +types_is_one_time_witness_cost_base: 52 +types_is_one_time_witness_type_tag_cost_per_byte: 2 +types_is_one_time_witness_type_cost_per_byte: 2 +validator_validate_metadata_cost_base: 20000 +validator_validate_metadata_data_cost_per_byte: 2 +crypto_invalid_arguments_cost: 100 +bls12381_bls12381_min_sig_verify_cost_base: 44064 +bls12381_bls12381_min_sig_verify_msg_cost_per_byte: 2 +bls12381_bls12381_min_sig_verify_msg_cost_per_block: 2 +bls12381_bls12381_min_pk_verify_cost_base: 49282 +bls12381_bls12381_min_pk_verify_msg_cost_per_byte: 2 +bls12381_bls12381_min_pk_verify_msg_cost_per_block: 2 +ecdsa_k1_ecrecover_keccak256_cost_base: 500 +ecdsa_k1_ecrecover_keccak256_msg_cost_per_byte: 2 +ecdsa_k1_ecrecover_keccak256_msg_cost_per_block: 2 +ecdsa_k1_ecrecover_sha256_cost_base: 500 +ecdsa_k1_ecrecover_sha256_msg_cost_per_byte: 2 +ecdsa_k1_ecrecover_sha256_msg_cost_per_block: 2 +ecdsa_k1_decompress_pubkey_cost_base: 52 +ecdsa_k1_secp256k1_verify_keccak256_cost_base: 1470 +ecdsa_k1_secp256k1_verify_keccak256_msg_cost_per_byte: 2 +ecdsa_k1_secp256k1_verify_keccak256_msg_cost_per_block: 2 +ecdsa_k1_secp256k1_verify_sha256_cost_base: 1470 +ecdsa_k1_secp256k1_verify_sha256_msg_cost_per_byte: 2 +ecdsa_k1_secp256k1_verify_sha256_msg_cost_per_block: 2 +ecdsa_r1_ecrecover_keccak256_cost_base: 1173 +ecdsa_r1_ecrecover_keccak256_msg_cost_per_byte: 2 +ecdsa_r1_ecrecover_keccak256_msg_cost_per_block: 2 +ecdsa_r1_ecrecover_sha256_cost_base: 1173 +ecdsa_r1_ecrecover_sha256_msg_cost_per_byte: 2 +ecdsa_r1_ecrecover_sha256_msg_cost_per_block: 2 +ecdsa_r1_secp256r1_verify_keccak256_cost_base: 4225 +ecdsa_r1_secp256r1_verify_keccak256_msg_cost_per_byte: 2 +ecdsa_r1_secp256r1_verify_keccak256_msg_cost_per_block: 2 +ecdsa_r1_secp256r1_verify_sha256_cost_base: 4225 +ecdsa_r1_secp256r1_verify_sha256_msg_cost_per_byte: 2 +ecdsa_r1_secp256r1_verify_sha256_msg_cost_per_block: 2 +ecvrf_ecvrf_verify_cost_base: 4848 +ecvrf_ecvrf_verify_alpha_string_cost_per_byte: 2 +ecvrf_ecvrf_verify_alpha_string_cost_per_block: 2 +ed25519_ed25519_verify_cost_base: 1802 +ed25519_ed25519_verify_msg_cost_per_byte: 2 +ed25519_ed25519_verify_msg_cost_per_block: 2 +groth16_prepare_verifying_key_bls12381_cost_base: 53838 +groth16_prepare_verifying_key_bn254_cost_base: 82010 +groth16_verify_groth16_proof_internal_bls12381_cost_base: 72090 +groth16_verify_groth16_proof_internal_bls12381_cost_per_public_input: 8213 +groth16_verify_groth16_proof_internal_bn254_cost_base: 115502 +groth16_verify_groth16_proof_internal_bn254_cost_per_public_input: 9484 +groth16_verify_groth16_proof_internal_public_input_cost_per_byte: 2 +hash_blake2b256_cost_base: 10 +hash_blake2b256_data_cost_per_byte: 2 +hash_blake2b256_data_cost_per_block: 2 +hash_keccak256_cost_base: 10 +hash_keccak256_data_cost_per_byte: 2 +hash_keccak256_data_cost_per_block: 2 +poseidon_bn254_cost_per_block: 388 +group_ops_bls12381_decode_scalar_cost: 7 +group_ops_bls12381_decode_g1_cost: 2848 +group_ops_bls12381_decode_g2_cost: 3770 +group_ops_bls12381_decode_gt_cost: 3068 +group_ops_bls12381_scalar_add_cost: 10 +group_ops_bls12381_g1_add_cost: 1556 +group_ops_bls12381_g2_add_cost: 3048 +group_ops_bls12381_gt_add_cost: 188 +group_ops_bls12381_scalar_sub_cost: 10 +group_ops_bls12381_g1_sub_cost: 1550 +group_ops_bls12381_g2_sub_cost: 3019 +group_ops_bls12381_gt_sub_cost: 497 +group_ops_bls12381_scalar_mul_cost: 11 +group_ops_bls12381_g1_mul_cost: 4842 +group_ops_bls12381_g2_mul_cost: 9108 +group_ops_bls12381_gt_mul_cost: 27490 +group_ops_bls12381_scalar_div_cost: 91 +group_ops_bls12381_g1_div_cost: 5091 +group_ops_bls12381_g2_div_cost: 9206 +group_ops_bls12381_gt_div_cost: 27804 +group_ops_bls12381_g1_hash_to_base_cost: 2962 +group_ops_bls12381_g2_hash_to_base_cost: 8688 +group_ops_bls12381_g1_hash_to_cost_per_byte: 2 +group_ops_bls12381_g2_hash_to_cost_per_byte: 2 +group_ops_bls12381_g1_msm_base_cost: 62648 +group_ops_bls12381_g2_msm_base_cost: 131192 +group_ops_bls12381_g1_msm_base_cost_per_input: 1333 +group_ops_bls12381_g2_msm_base_cost_per_input: 3216 +group_ops_bls12381_msm_max_len: 32 +group_ops_bls12381_pairing_cost: 26897 +group_ops_bls12381_g1_to_uncompressed_g1_cost: 2099 +group_ops_bls12381_uncompressed_g1_to_g1_cost: 677 +group_ops_bls12381_uncompressed_g1_sum_base_cost: 77 +group_ops_bls12381_uncompressed_g1_sum_cost_per_term: 26 +group_ops_bls12381_uncompressed_g1_sum_max_terms: 1200 +hmac_hmac_sha3_256_cost_base: 52 +hmac_hmac_sha3_256_input_cost_per_byte: 2 +hmac_hmac_sha3_256_input_cost_per_block: 2 +check_zklogin_id_cost_base: 200 +check_zklogin_issuer_cost_base: 200 +bcs_per_byte_serialized_cost: 2 +bcs_legacy_min_output_size_cost: 1 +bcs_failure_cost: 52 +hash_sha2_256_base_cost: 52 +hash_sha2_256_per_byte_cost: 2 +hash_sha2_256_legacy_min_input_len_cost: 1 +hash_sha3_256_base_cost: 52 +hash_sha3_256_per_byte_cost: 2 +hash_sha3_256_legacy_min_input_len_cost: 1 +type_name_get_base_cost: 52 +type_name_get_per_byte_cost: 2 +string_check_utf8_base_cost: 52 +string_check_utf8_per_byte_cost: 2 +string_is_char_boundary_base_cost: 52 +string_sub_string_base_cost: 52 +string_sub_string_per_byte_cost: 2 +string_index_of_base_cost: 52 +string_index_of_per_byte_pattern_cost: 2 +string_index_of_per_byte_searched_cost: 2 +vector_empty_base_cost: 52 +vector_length_base_cost: 52 +vector_push_back_base_cost: 52 +vector_push_back_legacy_per_abstract_memory_unit_cost: 2 +vector_borrow_base_cost: 52 +vector_pop_back_base_cost: 52 +vector_destroy_empty_base_cost: 52 +vector_swap_base_cost: 52 +debug_print_base_cost: 52 +debug_print_stack_trace_base_cost: 52 +execution_version: 1 +consensus_bad_nodes_stake_threshold: 20 +max_jwk_votes_per_validator_per_epoch: 240 +max_age_of_jwk_in_epochs: 1 +random_beacon_reduction_allowed_delta: 800 +random_beacon_reduction_lower_bound: 1000 +random_beacon_dkg_timeout_round: 3000 +random_beacon_min_round_interval_ms: 500 +random_beacon_dkg_version: 1 +consensus_max_transaction_size_bytes: 262144 +consensus_max_transactions_in_block_bytes: 524288 +consensus_max_num_transactions_in_block: 512 +max_deferral_rounds_for_congestion_control: 10 +min_checkpoint_interval_ms: 200 +checkpoint_summary_version_specific_data: 1 +max_soft_bundle_size: 5 +max_accumulated_txn_cost_per_object_in_mysticeti_commit: 10 +max_committee_members_count: 100 +consensus_gc_depth: 60 diff --git a/crates/iota-protocol-config/src/snapshots/iota_protocol_config__test__version_16.snap b/crates/iota-protocol-config/src/snapshots/iota_protocol_config__test__version_16.snap index 7d1da4f4745..70ade87c5dc 100644 --- a/crates/iota-protocol-config/src/snapshots/iota_protocol_config__test__version_16.snap +++ b/crates/iota-protocol-config/src/snapshots/iota_protocol_config__test__version_16.snap @@ -1,6 +1,7 @@ --- source: crates/iota-protocol-config/src/lib.rs expression: "ProtocolConfig::get_for_version(cur, *chain_id)" +snapshot_kind: text --- version: 16 feature_flags: @@ -38,7 +39,6 @@ feature_flags: select_committee_supporting_next_epoch_version: true consensus_median_timestamp_with_checkpoint_enforcement: true consensus_commit_transactions_only_for_traversed_headers: true - score_based_rewards: true max_tx_size_bytes: 131072 max_input_objects: 2048 max_size_written_objects: 5000000 diff --git a/crates/iota-protocol-config/src/snapshots/iota_protocol_config__test__version_18.snap b/crates/iota-protocol-config/src/snapshots/iota_protocol_config__test__version_18.snap new file mode 100644 index 00000000000..aa5627d4a07 --- /dev/null +++ b/crates/iota-protocol-config/src/snapshots/iota_protocol_config__test__version_18.snap @@ -0,0 +1,316 @@ +--- +source: crates/iota-protocol-config/src/lib.rs +expression: "ProtocolConfig::get_for_version(cur, *chain_id)" +snapshot_kind: text +--- +version: 18 +feature_flags: + consensus_transaction_ordering: ByGasPrice + enable_poseidon: true + enable_group_ops_native_function_msm: true + per_object_congestion_control_mode: TotalTxCount + consensus_choice: Starfish + zklogin_max_epoch_upper_bound_delta: 30 + enable_vdf: true + passkey_auth: true + relocate_event_module: true + protocol_defined_base_fee: true + uncompressed_g1_group_elements: true + disallow_new_modules_in_deps_only_packages: true + native_charging_v2: true + convert_type_argument_error: true + consensus_round_prober: true + consensus_distributed_vote_scoring_strategy: true + consensus_linearize_subdag_v2: true + variant_nodes: true + consensus_round_prober_probe_accepted_rounds: true + consensus_zstd_compression: true + congestion_control_min_free_execution_slot: true + accept_passkey_in_multisig: true + consensus_batched_block_sync: true + congestion_control_gas_price_feedback_mechanism: true + validate_identifier_inputs: true + minimize_child_object_mutations: true + dependency_linkage_error: true + additional_multisig_checks: true + normalize_ptb_arguments: true + select_committee_from_eligible_validators: true + track_non_committee_eligible_validators: true + select_committee_supporting_next_epoch_version: true + consensus_median_timestamp_with_checkpoint_enforcement: true + consensus_commit_transactions_only_for_traversed_headers: true + score_based_rewards: true +max_tx_size_bytes: 131072 +max_input_objects: 2048 +max_size_written_objects: 5000000 +max_size_written_objects_system_tx: 50000000 +max_serialized_tx_effects_size_bytes: 524288 +max_serialized_tx_effects_size_bytes_system_tx: 8388608 +max_gas_payment_objects: 256 +max_modules_in_publish: 64 +max_package_dependencies: 32 +max_arguments: 512 +max_type_arguments: 16 +max_type_argument_depth: 16 +max_pure_argument_size: 16384 +max_programmable_tx_commands: 1024 +move_binary_format_version: 7 +min_move_binary_format_version: 6 +binary_module_handles: 100 +binary_struct_handles: 300 +binary_function_handles: 1500 +binary_function_instantiations: 750 +binary_signatures: 1000 +binary_constant_pool: 4000 +binary_identifiers: 10000 +binary_address_identifiers: 100 +binary_struct_defs: 200 +binary_struct_def_instantiations: 100 +binary_function_defs: 1000 +binary_field_handles: 500 +binary_field_instantiations: 250 +binary_friend_decls: 100 +max_move_object_size: 256000 +max_move_package_size: 102400 +max_publish_or_upgrade_per_ptb: 5 +max_tx_gas: 50000000000 +max_gas_price: 100000 +max_gas_computation_bucket: 5000000 +gas_rounding_step: 1000 +max_loop_depth: 5 +max_generic_instantiation_length: 32 +max_function_parameters: 128 +max_basic_blocks: 1024 +max_value_stack_size: 1024 +max_type_nodes: 256 +max_push_size: 10000 +max_struct_definitions: 200 +max_function_definitions: 1000 +max_fields_in_struct: 32 +max_dependency_depth: 100 +max_num_event_emit: 1024 +max_num_new_move_object_ids: 2048 +max_num_new_move_object_ids_system_tx: 32768 +max_num_deleted_move_object_ids: 2048 +max_num_deleted_move_object_ids_system_tx: 32768 +max_num_transferred_move_object_ids: 2048 +max_num_transferred_move_object_ids_system_tx: 32768 +max_event_emit_size: 256000 +max_event_emit_size_total: 65536000 +max_move_vector_len: 262144 +max_move_identifier_len: 128 +max_move_value_depth: 128 +max_back_edges_per_function: 10000 +max_back_edges_per_module: 10000 +max_verifier_meter_ticks_per_function: 16000000 +max_meter_ticks_per_module: 16000000 +max_meter_ticks_per_package: 16000000 +object_runtime_max_num_cached_objects: 1000 +object_runtime_max_num_cached_objects_system_tx: 16000 +object_runtime_max_num_store_entries: 1000 +object_runtime_max_num_store_entries_system_tx: 16000 +base_tx_cost_fixed: 1000 +package_publish_cost_fixed: 1000 +base_tx_cost_per_byte: 0 +package_publish_cost_per_byte: 80 +obj_access_cost_read_per_byte: 15 +obj_access_cost_mutate_per_byte: 40 +obj_access_cost_delete_per_byte: 40 +obj_access_cost_verify_per_byte: 200 +max_type_to_layout_nodes: 512 +max_ptb_value_size: 1048576 +gas_model_version: 2 +obj_data_cost_refundable: 100 +obj_metadata_cost_non_refundable: 50 +storage_rebate_rate: 10000 +reward_slashing_rate: 10000 +storage_gas_price: 76 +base_gas_price: 1000 +validator_target_reward: 767000000000000 +max_transactions_per_checkpoint: 10000 +max_checkpoint_size_bytes: 31457280 +buffer_stake_for_protocol_upgrade_bps: 5000 +address_from_bytes_cost_base: 52 +address_to_u256_cost_base: 52 +address_from_u256_cost_base: 52 +config_read_setting_impl_cost_base: 100 +config_read_setting_impl_cost_per_byte: 40 +dynamic_field_hash_type_and_key_cost_base: 100 +dynamic_field_hash_type_and_key_type_cost_per_byte: 2 +dynamic_field_hash_type_and_key_value_cost_per_byte: 2 +dynamic_field_hash_type_and_key_type_tag_cost_per_byte: 2 +dynamic_field_add_child_object_cost_base: 100 +dynamic_field_add_child_object_type_cost_per_byte: 10 +dynamic_field_add_child_object_value_cost_per_byte: 10 +dynamic_field_add_child_object_struct_tag_cost_per_byte: 10 +dynamic_field_borrow_child_object_cost_base: 100 +dynamic_field_borrow_child_object_child_ref_cost_per_byte: 10 +dynamic_field_borrow_child_object_type_cost_per_byte: 10 +dynamic_field_remove_child_object_cost_base: 100 +dynamic_field_remove_child_object_child_cost_per_byte: 2 +dynamic_field_remove_child_object_type_cost_per_byte: 2 +dynamic_field_has_child_object_cost_base: 100 +dynamic_field_has_child_object_with_ty_cost_base: 100 +dynamic_field_has_child_object_with_ty_type_cost_per_byte: 2 +dynamic_field_has_child_object_with_ty_type_tag_cost_per_byte: 2 +event_emit_cost_base: 52 +event_emit_value_size_derivation_cost_per_byte: 2 +event_emit_tag_size_derivation_cost_per_byte: 5 +event_emit_output_cost_per_byte: 10 +object_borrow_uid_cost_base: 52 +object_delete_impl_cost_base: 52 +object_record_new_uid_cost_base: 52 +transfer_transfer_internal_cost_base: 52 +transfer_freeze_object_cost_base: 52 +transfer_share_object_cost_base: 52 +transfer_receive_object_cost_base: 52 +tx_context_derive_id_cost_base: 52 +types_is_one_time_witness_cost_base: 52 +types_is_one_time_witness_type_tag_cost_per_byte: 2 +types_is_one_time_witness_type_cost_per_byte: 2 +validator_validate_metadata_cost_base: 20000 +validator_validate_metadata_data_cost_per_byte: 2 +crypto_invalid_arguments_cost: 100 +bls12381_bls12381_min_sig_verify_cost_base: 44064 +bls12381_bls12381_min_sig_verify_msg_cost_per_byte: 2 +bls12381_bls12381_min_sig_verify_msg_cost_per_block: 2 +bls12381_bls12381_min_pk_verify_cost_base: 49282 +bls12381_bls12381_min_pk_verify_msg_cost_per_byte: 2 +bls12381_bls12381_min_pk_verify_msg_cost_per_block: 2 +ecdsa_k1_ecrecover_keccak256_cost_base: 500 +ecdsa_k1_ecrecover_keccak256_msg_cost_per_byte: 2 +ecdsa_k1_ecrecover_keccak256_msg_cost_per_block: 2 +ecdsa_k1_ecrecover_sha256_cost_base: 500 +ecdsa_k1_ecrecover_sha256_msg_cost_per_byte: 2 +ecdsa_k1_ecrecover_sha256_msg_cost_per_block: 2 +ecdsa_k1_decompress_pubkey_cost_base: 52 +ecdsa_k1_secp256k1_verify_keccak256_cost_base: 1470 +ecdsa_k1_secp256k1_verify_keccak256_msg_cost_per_byte: 2 +ecdsa_k1_secp256k1_verify_keccak256_msg_cost_per_block: 2 +ecdsa_k1_secp256k1_verify_sha256_cost_base: 1470 +ecdsa_k1_secp256k1_verify_sha256_msg_cost_per_byte: 2 +ecdsa_k1_secp256k1_verify_sha256_msg_cost_per_block: 2 +ecdsa_r1_ecrecover_keccak256_cost_base: 1173 +ecdsa_r1_ecrecover_keccak256_msg_cost_per_byte: 2 +ecdsa_r1_ecrecover_keccak256_msg_cost_per_block: 2 +ecdsa_r1_ecrecover_sha256_cost_base: 1173 +ecdsa_r1_ecrecover_sha256_msg_cost_per_byte: 2 +ecdsa_r1_ecrecover_sha256_msg_cost_per_block: 2 +ecdsa_r1_secp256r1_verify_keccak256_cost_base: 4225 +ecdsa_r1_secp256r1_verify_keccak256_msg_cost_per_byte: 2 +ecdsa_r1_secp256r1_verify_keccak256_msg_cost_per_block: 2 +ecdsa_r1_secp256r1_verify_sha256_cost_base: 4225 +ecdsa_r1_secp256r1_verify_sha256_msg_cost_per_byte: 2 +ecdsa_r1_secp256r1_verify_sha256_msg_cost_per_block: 2 +ecvrf_ecvrf_verify_cost_base: 4848 +ecvrf_ecvrf_verify_alpha_string_cost_per_byte: 2 +ecvrf_ecvrf_verify_alpha_string_cost_per_block: 2 +ed25519_ed25519_verify_cost_base: 1802 +ed25519_ed25519_verify_msg_cost_per_byte: 2 +ed25519_ed25519_verify_msg_cost_per_block: 2 +groth16_prepare_verifying_key_bls12381_cost_base: 53838 +groth16_prepare_verifying_key_bn254_cost_base: 82010 +groth16_verify_groth16_proof_internal_bls12381_cost_base: 72090 +groth16_verify_groth16_proof_internal_bls12381_cost_per_public_input: 8213 +groth16_verify_groth16_proof_internal_bn254_cost_base: 115502 +groth16_verify_groth16_proof_internal_bn254_cost_per_public_input: 9484 +groth16_verify_groth16_proof_internal_public_input_cost_per_byte: 2 +hash_blake2b256_cost_base: 10 +hash_blake2b256_data_cost_per_byte: 2 +hash_blake2b256_data_cost_per_block: 2 +hash_keccak256_cost_base: 10 +hash_keccak256_data_cost_per_byte: 2 +hash_keccak256_data_cost_per_block: 2 +poseidon_bn254_cost_base: 260 +poseidon_bn254_cost_per_block: 388 +group_ops_bls12381_decode_scalar_cost: 7 +group_ops_bls12381_decode_g1_cost: 2848 +group_ops_bls12381_decode_g2_cost: 3770 +group_ops_bls12381_decode_gt_cost: 3068 +group_ops_bls12381_scalar_add_cost: 10 +group_ops_bls12381_g1_add_cost: 1556 +group_ops_bls12381_g2_add_cost: 3048 +group_ops_bls12381_gt_add_cost: 188 +group_ops_bls12381_scalar_sub_cost: 10 +group_ops_bls12381_g1_sub_cost: 1550 +group_ops_bls12381_g2_sub_cost: 3019 +group_ops_bls12381_gt_sub_cost: 497 +group_ops_bls12381_scalar_mul_cost: 11 +group_ops_bls12381_g1_mul_cost: 4842 +group_ops_bls12381_g2_mul_cost: 9108 +group_ops_bls12381_gt_mul_cost: 27490 +group_ops_bls12381_scalar_div_cost: 91 +group_ops_bls12381_g1_div_cost: 5091 +group_ops_bls12381_g2_div_cost: 9206 +group_ops_bls12381_gt_div_cost: 27804 +group_ops_bls12381_g1_hash_to_base_cost: 2962 +group_ops_bls12381_g2_hash_to_base_cost: 8688 +group_ops_bls12381_g1_hash_to_cost_per_byte: 2 +group_ops_bls12381_g2_hash_to_cost_per_byte: 2 +group_ops_bls12381_g1_msm_base_cost: 62648 +group_ops_bls12381_g2_msm_base_cost: 131192 +group_ops_bls12381_g1_msm_base_cost_per_input: 1333 +group_ops_bls12381_g2_msm_base_cost_per_input: 3216 +group_ops_bls12381_msm_max_len: 32 +group_ops_bls12381_pairing_cost: 26897 +group_ops_bls12381_g1_to_uncompressed_g1_cost: 2099 +group_ops_bls12381_uncompressed_g1_to_g1_cost: 677 +group_ops_bls12381_uncompressed_g1_sum_base_cost: 77 +group_ops_bls12381_uncompressed_g1_sum_cost_per_term: 26 +group_ops_bls12381_uncompressed_g1_sum_max_terms: 1200 +hmac_hmac_sha3_256_cost_base: 52 +hmac_hmac_sha3_256_input_cost_per_byte: 2 +hmac_hmac_sha3_256_input_cost_per_block: 2 +check_zklogin_id_cost_base: 200 +check_zklogin_issuer_cost_base: 200 +vdf_verify_vdf_cost: 1500 +vdf_hash_to_input_cost: 100 +bcs_per_byte_serialized_cost: 2 +bcs_legacy_min_output_size_cost: 1 +bcs_failure_cost: 52 +hash_sha2_256_base_cost: 52 +hash_sha2_256_per_byte_cost: 2 +hash_sha2_256_legacy_min_input_len_cost: 1 +hash_sha3_256_base_cost: 52 +hash_sha3_256_per_byte_cost: 2 +hash_sha3_256_legacy_min_input_len_cost: 1 +type_name_get_base_cost: 52 +type_name_get_per_byte_cost: 2 +string_check_utf8_base_cost: 52 +string_check_utf8_per_byte_cost: 2 +string_is_char_boundary_base_cost: 52 +string_sub_string_base_cost: 52 +string_sub_string_per_byte_cost: 2 +string_index_of_base_cost: 52 +string_index_of_per_byte_pattern_cost: 2 +string_index_of_per_byte_searched_cost: 2 +vector_empty_base_cost: 52 +vector_length_base_cost: 52 +vector_push_back_base_cost: 52 +vector_push_back_legacy_per_abstract_memory_unit_cost: 2 +vector_borrow_base_cost: 52 +vector_pop_back_base_cost: 52 +vector_destroy_empty_base_cost: 52 +vector_swap_base_cost: 52 +debug_print_base_cost: 52 +debug_print_stack_trace_base_cost: 52 +execution_version: 1 +consensus_bad_nodes_stake_threshold: 20 +max_jwk_votes_per_validator_per_epoch: 240 +max_age_of_jwk_in_epochs: 1 +random_beacon_reduction_allowed_delta: 800 +random_beacon_reduction_lower_bound: 1000 +random_beacon_dkg_timeout_round: 3000 +random_beacon_min_round_interval_ms: 500 +random_beacon_dkg_version: 1 +consensus_max_transaction_size_bytes: 262144 +consensus_max_transactions_in_block_bytes: 524288 +consensus_max_num_transactions_in_block: 512 +max_deferral_rounds_for_congestion_control: 10 +min_checkpoint_interval_ms: 200 +checkpoint_summary_version_specific_data: 1 +max_soft_bundle_size: 5 +max_accumulated_txn_cost_per_object_in_mysticeti_commit: 10 +max_committee_members_count: 100 +consensus_gc_depth: 60 +max_congestion_limit_overshoot_per_commit: 100 From 310abc75b81339e0515692e1ef6bf121932768bf Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 2 Dec 2025 15:05:12 +0000 Subject: [PATCH 8/8] add iota-framework snapshots --- ...000000000000000000000000000000000000000001 | Bin 0 -> 17093 bytes ...000000000000000000000000000000000000000002 | Bin 0 -> 72940 bytes ...000000000000000000000000000000000000000003 | Bin 0 -> 48392 bytes ...00000000000000000000000000000000000000107a | Bin 0 -> 8844 bytes crates/iota-framework-snapshot/manifest.json | 25 ++++++++++++++++++ 5 files changed, 25 insertions(+) create mode 100644 crates/iota-framework-snapshot/bytecode_snapshot/18/0x0000000000000000000000000000000000000000000000000000000000000001 create mode 100644 crates/iota-framework-snapshot/bytecode_snapshot/18/0x0000000000000000000000000000000000000000000000000000000000000002 create mode 100644 crates/iota-framework-snapshot/bytecode_snapshot/18/0x0000000000000000000000000000000000000000000000000000000000000003 create mode 100644 crates/iota-framework-snapshot/bytecode_snapshot/18/0x000000000000000000000000000000000000000000000000000000000000107a diff --git a/crates/iota-framework-snapshot/bytecode_snapshot/18/0x0000000000000000000000000000000000000000000000000000000000000001 b/crates/iota-framework-snapshot/bytecode_snapshot/18/0x0000000000000000000000000000000000000000000000000000000000000001 new file mode 100644 index 0000000000000000000000000000000000000000..235d243779fe42ad9ff8514862a792516828706f GIT binary patch literal 17093 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 z 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�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 MPDdCQ?)m5cf9vrs1^@s6 literal 0 HcmV?d00001 diff --git a/crates/iota-framework-snapshot/bytecode_snapshot/18/0x0000000000000000000000000000000000000000000000000000000000000003 b/crates/iota-framework-snapshot/bytecode_snapshot/18/0x0000000000000000000000000000000000000000000000000000000000000003 new file mode 100644 index 0000000000000000000000000000000000000000..e7a12a0969811a2ccad4fdb0c11370276b02f35e GIT binary patch literal 48392 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 cA{!m;v)lBl3o6kKN