From afdcf41fb606d5d8495a56f7019f9dc499ac206a Mon Sep 17 00:00:00 2001 From: Michael Sutton Date: Wed, 13 May 2026 15:21:56 +0300 Subject: [PATCH 1/6] bump p2p protocol version for toccata Advertise protocol 10 for Toccata while normalizing TN12 peers that still advertise protocol 9 into the same Toccata flow set (now named 10). --- protocol/flows/src/flow_context.rs | 23 +++++++++++++++---- protocol/flows/src/lib.rs | 2 +- protocol/flows/src/{v9 => v10}/mod.rs | 0 .../request_pruning_point_smt_state.rs | 0 4 files changed, 20 insertions(+), 5 deletions(-) rename protocol/flows/src/{v9 => v10}/mod.rs (100%) rename protocol/flows/src/{v9 => v10}/request_pruning_point_smt_state.rs (100%) diff --git a/protocol/flows/src/flow_context.rs b/protocol/flows/src/flow_context.rs index 1a17f417ad..1c1733b9b1 100644 --- a/protocol/flows/src/flow_context.rs +++ b/protocol/flows/src/flow_context.rs @@ -4,7 +4,7 @@ use crate::flowcontext::{ transactions::TransactionsSpread, }; use crate::user_agent_rule::{UserAgentRuleRejectReason, UserAgentRuleSet}; -use crate::{v7, v8, v9}; +use crate::{v7, v8, v10}; use async_trait::async_trait; use futures::future::join_all; use kaspa_addressmanager::AddressManager; @@ -13,6 +13,7 @@ use kaspa_consensus_core::api::{BlockValidationFuture, BlockValidationFutures}; use kaspa_consensus_core::block::Block; use kaspa_consensus_core::config::Config; use kaspa_consensus_core::errors::block::RuleError; +use kaspa_consensus_core::network::{NetworkId, NetworkType}; use kaspa_consensus_core::tx::{Transaction, TransactionId}; use kaspa_consensus_notify::{ notification::{Notification, PruningPointUtxoSetOverrideNotification}, @@ -60,7 +61,11 @@ use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use uuid::Uuid; /// The P2P protocol version. -const PROTOCOL_VERSION: u32 = 9; +const PROTOCOL_VERSION: u32 = 10; + +/// Testnet 12 was launched with the Toccata flow set under protocol version 9. +const TN12_LAUNCH_PROTOCOL_VERSION: u32 = 9; +const TN12_NETWORK: NetworkId = NetworkId::with_suffix(NetworkType::Testnet, 12); /// See `check_orphan_resolution_range` const BASELINE_ORPHAN_RESOLUTION_RANGE: u32 = 5; @@ -760,9 +765,19 @@ impl ConnectionInitializer for FlowContext { debug!("protocol versions - self: {}, peer: {}", PROTOCOL_VERSION, peer_version.protocol_version); + // TN12 launched Toccata flows under protocol 9. Normalize those peers to the current + // protocol locally while preserving the originally advertised version in peer properties. + let peer_protocol_version = if self.config.net == TN12_NETWORK && peer_version.protocol_version == TN12_LAUNCH_PROTOCOL_VERSION + { + PROTOCOL_VERSION + } else { + peer_version.protocol_version + }; + // Register all flows according to version - let (flows, applied_protocol_version) = match peer_version.protocol_version { - v if v >= PROTOCOL_VERSION => (v9::register(self.clone(), router.clone(), PROTOCOL_VERSION), PROTOCOL_VERSION), + let (flows, applied_protocol_version) = match peer_protocol_version { + v if v >= PROTOCOL_VERSION => (v10::register(self.clone(), router.clone(), PROTOCOL_VERSION), PROTOCOL_VERSION), + 9 => (v8::register(self.clone(), router.clone(), 9), 9), 8 => (v8::register(self.clone(), router.clone(), 8), 8), 7 => (v7::register(self.clone(), router.clone()), 7), v => return Err(ProtocolError::VersionMismatch(PROTOCOL_VERSION, v)), diff --git a/protocol/flows/src/lib.rs b/protocol/flows/src/lib.rs index 6dbcd27bbf..6070ec780f 100644 --- a/protocol/flows/src/lib.rs +++ b/protocol/flows/src/lib.rs @@ -4,6 +4,6 @@ pub mod flowcontext; pub mod ibd; pub mod service; pub mod user_agent_rule; +pub mod v10; pub mod v7; pub mod v8; -pub mod v9; diff --git a/protocol/flows/src/v9/mod.rs b/protocol/flows/src/v10/mod.rs similarity index 100% rename from protocol/flows/src/v9/mod.rs rename to protocol/flows/src/v10/mod.rs diff --git a/protocol/flows/src/v9/request_pruning_point_smt_state.rs b/protocol/flows/src/v10/request_pruning_point_smt_state.rs similarity index 100% rename from protocol/flows/src/v9/request_pruning_point_smt_state.rs rename to protocol/flows/src/v10/request_pruning_point_smt_state.rs From c57b7365adf9b48f4c9dd3440ec285353b47ee3e Mon Sep 17 00:00:00 2001 From: Michael Sutton Date: Wed, 13 May 2026 16:22:23 +0300 Subject: [PATCH 2/6] enforce toccata p2p version before activation Start rejecting outdated peer protocol versions one day before toccata activation --- protocol/flows/src/flow_context.rs | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/protocol/flows/src/flow_context.rs b/protocol/flows/src/flow_context.rs index 1c1733b9b1..72cac69d36 100644 --- a/protocol/flows/src/flow_context.rs +++ b/protocol/flows/src/flow_context.rs @@ -774,13 +774,29 @@ impl ConnectionInitializer for FlowContext { peer_version.protocol_version }; - // Register all flows according to version - let (flows, applied_protocol_version) = match peer_protocol_version { - v if v >= PROTOCOL_VERSION => (v10::register(self.clone(), router.clone(), PROTOCOL_VERSION), PROTOCOL_VERSION), - 9 => (v8::register(self.clone(), router.clone(), 9), 9), - 8 => (v8::register(self.clone(), router.clone(), 8), 8), - 7 => (v7::register(self.clone(), router.clone()), 7), - v => return Err(ProtocolError::VersionMismatch(PROTOCOL_VERSION, v)), + // One day before activation, upgraded nodes start disconnecting outdated peers from the P2P network. + const ONE_DAY_SECONDS: u64 = 24 * 60 * 60; + let daa_threshold = ONE_DAY_SECONDS * self.config.bps(); + let virtual_daa_score = self.consensus().unguarded_session().get_virtual_daa_score(); + let connect_only_new_versions = self.config.covenants_activation.is_active(virtual_daa_score.saturating_add(daa_threshold)); + + // Until the one-day pre-activation threshold is reached, older protocol versions remain accepted. + // Once it is reached, peers must advertise protocol 10 (TN12 launch peers were normalized above). + let (flows, applied_protocol_version) = if connect_only_new_versions { + // Register all flows according to version + match peer_protocol_version { + v if v >= PROTOCOL_VERSION => (v10::register(self.clone(), router.clone(), PROTOCOL_VERSION), PROTOCOL_VERSION), + v => return Err(ProtocolError::VersionMismatch(PROTOCOL_VERSION, v)), + } + } else { + // Register all flows according to version + match peer_protocol_version { + v if v >= PROTOCOL_VERSION => (v10::register(self.clone(), router.clone(), PROTOCOL_VERSION), PROTOCOL_VERSION), + 9 => (v8::register(self.clone(), router.clone(), 9), 9), + 8 => (v8::register(self.clone(), router.clone(), 8), 8), + 7 => (v7::register(self.clone(), router.clone()), 7), + v => return Err(ProtocolError::VersionMismatch(PROTOCOL_VERSION, v)), + } }; // Build and register the peer properties From 5cf7044cf62d4663fef677e90c2d1ad95d962790 Mon Sep 17 00:00:00 2001 From: Michael Sutton Date: Wed, 13 May 2026 19:03:08 +0300 Subject: [PATCH 3/6] guard toccata ibd from outdated peers Reject stale pre-activation pruning points after Toccata should have produced a newer pruning point, and reject post-activation pruning points from peers not using the Toccata protocol version. --- consensus/core/src/config/params.rs | 2 +- protocol/flows/src/flow_context.rs | 5 +- protocol/flows/src/ibd/flow.rs | 180 +++++++++++++++++++++++++++- 3 files changed, 183 insertions(+), 4 deletions(-) diff --git a/consensus/core/src/config/params.rs b/consensus/core/src/config/params.rs index c7746c73cb..7e26064648 100644 --- a/consensus/core/src/config/params.rs +++ b/consensus/core/src/config/params.rs @@ -371,7 +371,7 @@ impl Params { self.blockrate.difficulty_sample_rate } - /// Returns the target time per block + /// Returns the target time per block (milliseconds) #[inline] #[must_use] pub fn target_time_per_block(&self) -> u64 { diff --git a/protocol/flows/src/flow_context.rs b/protocol/flows/src/flow_context.rs index 72cac69d36..fcd61df59a 100644 --- a/protocol/flows/src/flow_context.rs +++ b/protocol/flows/src/flow_context.rs @@ -61,7 +61,7 @@ use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use uuid::Uuid; /// The P2P protocol version. -const PROTOCOL_VERSION: u32 = 10; +pub(crate) const PROTOCOL_VERSION: u32 = 10; /// Testnet 12 was launched with the Toccata flow set under protocol version 9. const TN12_LAUNCH_PROTOCOL_VERSION: u32 = 9; @@ -782,6 +782,9 @@ impl ConnectionInitializer for FlowContext { // Until the one-day pre-activation threshold is reached, older protocol versions remain accepted. // Once it is reached, peers must advertise protocol 10 (TN12 launch peers were normalized above). + // + // Note: post-activation fresh nodes with virtual DAA score near genesis are not covered here and + // are guarded later during IBD by `validate_pruning_point_freshness_for_toccata`. let (flows, applied_protocol_version) = if connect_only_new_versions { // Register all flows according to version match peer_protocol_version { diff --git a/protocol/flows/src/ibd/flow.rs b/protocol/flows/src/ibd/flow.rs index 6b3a174532..d2b18d3242 100644 --- a/protocol/flows/src/ibd/flow.rs +++ b/protocol/flows/src/ibd/flow.rs @@ -1,5 +1,5 @@ use crate::{ - flow_context::FlowContext, + flow_context::{FlowContext, PROTOCOL_VERSION}, flow_trait::Flow, ibd::{HeadersChunkStream, TrustedEntryStream, negotiate::ChainNegotiationOutput}, }; @@ -9,6 +9,7 @@ use kaspa_consensus_core::{ BlockHashSet, api::BlockValidationFuture, block::Block, + config::params::{ForkActivation, Params}, header::Header, pruning::{PruningPointProof, PruningPointsList, PruningProofMetadata}, trusted::TrustedBlock, @@ -412,7 +413,8 @@ impl IbdFlow { let proof = consensus.clone().spawn_blocking(move |c| c.validate_pruning_proof(&proof, &proof_metadata).map(|()| proof)).await?; - let proof_pruning_point = proof[0].last().expect("was just ensured by validation").hash; + let proof_pruning_point_header = proof[0].last().expect("was just ensured by validation"); + let proof_pruning_point = proof_pruning_point_header.hash; if proof_pruning_point == self.ctx.config.genesis.hash { return Err(ProtocolError::Other("the proof pruning point is the genesis block")); @@ -423,6 +425,16 @@ impl IbdFlow { } drop(consensus); + // [Toccata] Reject IBD from outdated peers + validate_pruning_point_freshness_for_toccata( + self.ctx.config.as_ref(), + self.router.properties().protocol_version, + proof_pruning_point_header.hash, + proof_pruning_point_header.timestamp, + proof_pruning_point_header.daa_score, + unix_now(), + )?; + self.router .enqueue(make_message!(Payload::RequestPruningPointAndItsAnticone, RequestPruningPointAndItsAnticoneMessage {})) .await?; @@ -1031,3 +1043,167 @@ staging selected tip ({}) is too small or negative. Aborting IBD...", Ok(QueueChunkOutput { jobs, daa_score: current_daa_score, timestamp: current_timestamp }) } } + +/// [Toccata] Fresh nodes cannot easily identify outdated peers after activation, so we guard +/// against syncers advertising pruning points that are clearly stale. +fn validate_pruning_point_freshness_for_toccata( + params: &Params, + applied_protocol_version: u32, + pp_hash: Hash, + pp_timestamp: u64, + pp_daa_score: u64, + now: u64, +) -> Result<(), ProtocolError> { + // No activation is expected. + if params.covenants_activation == ForkActivation::never() { + return Ok(()); + } + + // If the pruning point is post-activation, the peer must already be using the Toccata protocol version. + // Peers declaring that version but still violating Toccata rules will be caught during normal IBD validation. + if params.covenants_activation.is_active(pp_daa_score) { + if applied_protocol_version < PROTOCOL_VERSION { + return Err(ProtocolError::OtherOwned(format!( + "syncer pruning point {} is post-Toccata activation, but peer protocol version {} is below required version {}", + pp_hash, applied_protocol_version, PROTOCOL_VERSION + ))); + } + return Ok(()); + } + + // Otherwise, protect fresh nodes from outdated syncers with stale pre-activation pruning points. + + let activation_daa_score = params.covenants_activation.daa_score(); + + // Reject if: + // 1. the syncer's pruning point is still pre-activation; + // 2. based on its timestamp and DAA score, activation should have happened long enough ago + // for the syncer to already expose a post-activation pruning point. + const ONE_DAY_MILLIS: u64 = 24 * 60 * 60 * 1000; + let millis_per_block = params.target_time_per_block(); + + let pp_to_activation_blocks = activation_daa_score.saturating_sub(pp_daa_score); + let pp_to_activation_millis = pp_to_activation_blocks.saturating_mul(millis_per_block); + let estimated_activation_time = pp_timestamp.saturating_add(pp_to_activation_millis); + + let pruning_period_millis = params.pruning_depth().saturating_add(params.finality_depth()).saturating_mul(millis_per_block); + // The oldest activation estimate for which a pre-activation pruning point is still tolerated. + let stale_activation_time_cutoff = now.saturating_sub(pruning_period_millis).saturating_sub(ONE_DAY_MILLIS); + + // If activation should have happened before this cutoff, the syncer should already + // expose a post-activation pruning point. + if estimated_activation_time < stale_activation_time_cutoff { + return Err(ProtocolError::OtherOwned(format!( + "syncer pruning point {} is stale: DAA score {} is below Toccata activation DAA score {}, but based on its timestamp {} a post-activation pruning point is expected by now", + pp_hash, pp_daa_score, activation_daa_score, pp_timestamp + ))); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use kaspa_consensus_core::config::params::MAINNET_PARAMS; + + fn params_with_covenants_activation(activation_daa_score: u64) -> Params { + let mut params = MAINNET_PARAMS.clone(); + params.covenants_activation = ForkActivation::new(activation_daa_score); + params + } + + fn pruning_period_millis(params: &Params) -> u64 { + params.pruning_depth().saturating_add(params.finality_depth()).saturating_mul(params.target_time_per_block()) + } + + #[test] + fn test_toccata_pruning_point_staleness_guard() { + const V: u32 = PROTOCOL_VERSION; + const ONE_DAY_MILLIS: u64 = 24 * 60 * 60 * 1000; + let blocks_per_day = ONE_DAY_MILLIS / MAINNET_PARAMS.target_time_per_block(); + let activation_daa_score = 10_000_000; + let params = params_with_covenants_activation(activation_daa_score); + let pp_hash = Hash::from_u64_word(1); + let pp_daa_score = activation_daa_score - 10; + let pp_timestamp = 1_000_000_000_000; + let pp_to_activation_millis = 10 * params.target_time_per_block(); + let estimated_activation_time = pp_timestamp + pp_to_activation_millis; + let stale_after = estimated_activation_time + pruning_period_millis(¶ms) + ONE_DAY_MILLIS; + + // No activation is configured: + // PP(pre-activation by score) ---- estimated activation ---- pruning period + margin ---- now + assert!( + validate_pruning_point_freshness_for_toccata(&MAINNET_PARAMS, V, pp_hash, pp_timestamp, pp_daa_score, stale_after + 1) + .is_ok() + ); + + // Normal pre-activation IBD: activation is still ten days away. + // PP/now -------- 10d -------- activation + let pp_ten_days_before_activation = activation_daa_score - 10 * blocks_per_day; + assert!( + validate_pruning_point_freshness_for_toccata( + ¶ms, + V - 1, + pp_hash, + pp_timestamp, + pp_ten_days_before_activation, + pp_timestamp + ) + .is_ok() + ); + + // The syncer's pruning point is already post-activation, so ordinary IBD validation takes over: + // PP(post-activation by score) ----------------------------------------------- now + assert!( + validate_pruning_point_freshness_for_toccata(¶ms, V, pp_hash, pp_timestamp, activation_daa_score, stale_after + 1) + .is_ok() + ); + + // Last tolerated instant for a pre-activation pruning point: + // PP ---- estimated activation ---- pruning period + margin == now + assert!(validate_pruning_point_freshness_for_toccata(¶ms, V, pp_hash, pp_timestamp, pp_daa_score, stale_after).is_ok()); + + // One millisecond later, the same pre-activation pruning point is stale: + // PP ---- estimated activation ---- pruning period + margin < now + assert!( + validate_pruning_point_freshness_for_toccata(¶ms, V, pp_hash, pp_timestamp, pp_daa_score, stale_after + 1).is_err() + ); + + // Stale IBD: the syncer's pruning point is three days before activation, and now is + // six days after that pruning point. Activation should have happened long enough ago + // for the syncer to already expose a post-activation pruning point. + // PP -------- 3d -------- activation -------- 3d -------- now + let pp_three_days_before_activation = activation_daa_score - 3 * blocks_per_day; + let now_six_days_after_pp = pp_timestamp + 6 * ONE_DAY_MILLIS; + assert!( + validate_pruning_point_freshness_for_toccata( + ¶ms, + V, + pp_hash, + pp_timestamp, + pp_three_days_before_activation, + now_six_days_after_pp + ) + .is_err() + ); + + // Normal IBD: two days after activation, a pruning point just before activation is + // still expected because pruning points trail the live chain by the pruning period. + // PP - activation -------- 2d -------- now + let pp_just_before_activation = activation_daa_score - 1; + let pp_just_before_activation_timestamp = pp_timestamp + 3 * ONE_DAY_MILLIS - params.target_time_per_block(); + let now_two_days_after_activation = pp_timestamp + 5 * ONE_DAY_MILLIS; + assert!( + validate_pruning_point_freshness_for_toccata( + ¶ms, + V, + pp_hash, + pp_just_before_activation_timestamp, + pp_just_before_activation, + now_two_days_after_activation + ) + .is_ok() + ); + } +} From fa99704ff036b6bb993689198c051a26025866dd Mon Sep 17 00:00:00 2001 From: Michael Sutton Date: Thu, 14 May 2026 15:02:05 +0300 Subject: [PATCH 4/6] guard toccata ibd by header version Check the IBD relay block against the active forked block version before requesting the pruning proof, and keep the pruning-point freshness guard focused on stale pre-activation syncers. --- consensus/core/src/constants.rs | 2 +- protocol/flows/src/ibd/flow.rs | 56 +++++++++++++++------------------ 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/consensus/core/src/constants.rs b/consensus/core/src/constants.rs index 863a444aad..1b5e8a571b 100644 --- a/consensus/core/src/constants.rs +++ b/consensus/core/src/constants.rs @@ -4,7 +4,7 @@ pub const BLOCK_VERSION: u16 = 1; /// The block version activated by the Toccata hardfork. This change denotes the use of /// the new sequencing commit described in KIP-21. -// TOOD(post-toccata): Remove this and change BLOCK_VERSION to 2. +// TODO(post-toccata): Remove this and change BLOCK_VERSION to 2. pub const TOCCATA_BLOCK_VERSION: u16 = 2; /// TX_VERSION is the current latest supported transaction version. diff --git a/protocol/flows/src/ibd/flow.rs b/protocol/flows/src/ibd/flow.rs index d2b18d3242..ce00fee406 100644 --- a/protocol/flows/src/ibd/flow.rs +++ b/protocol/flows/src/ibd/flow.rs @@ -1,5 +1,5 @@ use crate::{ - flow_context::{FlowContext, PROTOCOL_VERSION}, + flow_context::FlowContext, flow_trait::Flow, ibd::{HeadersChunkStream, TrustedEntryStream, negotiate::ChainNegotiationOutput}, }; @@ -393,6 +393,21 @@ impl IbdFlow { } async fn sync_and_validate_pruning_proof(&mut self, staging: &ConsensusProxy, relay_block: &Block) -> Result { + // [Toccata] Guard IBD from outdated nodes. P2P flow registration does not protect + // fresh IBD peers, and the relay block is usually the syncer sink, so reject an unexpected + // block version before requesting the pruning proof. The pruning point itself is + // checked below by `validate_pruning_point_freshness_for_toccata`. + let expected_relay_block_version = self.ctx.config.block_version().get(relay_block.header.daa_score); + if relay_block.header.version != expected_relay_block_version { + return Err(ProtocolError::OtherOwned(format!( + "peer relayed block {} header version mismatch: got {}, expected {} at DAA score {} (Toccata guard)", + relay_block.hash(), + relay_block.header.version, + expected_relay_block_version, + relay_block.header.daa_score + ))); + } + self.router.enqueue(make_message!(Payload::RequestPruningPointProof, RequestPruningPointProofMessage {})).await?; // Pruning proof generation and communication might take several minutes, so we allow a long 10 minute timeout @@ -428,7 +443,6 @@ impl IbdFlow { // [Toccata] Reject IBD from outdated peers validate_pruning_point_freshness_for_toccata( self.ctx.config.as_ref(), - self.router.properties().protocol_version, proof_pruning_point_header.hash, proof_pruning_point_header.timestamp, proof_pruning_point_header.daa_score, @@ -1046,9 +1060,10 @@ staging selected tip ({}) is too small or negative. Aborting IBD...", /// [Toccata] Fresh nodes cannot easily identify outdated peers after activation, so we guard /// against syncers advertising pruning points that are clearly stale. +/// +/// TODO(post-toccata): remove or adjust this stale pruning-point guard once Toccata is cleaned up. fn validate_pruning_point_freshness_for_toccata( params: &Params, - applied_protocol_version: u32, pp_hash: Hash, pp_timestamp: u64, pp_daa_score: u64, @@ -1059,15 +1074,8 @@ fn validate_pruning_point_freshness_for_toccata( return Ok(()); } - // If the pruning point is post-activation, the peer must already be using the Toccata protocol version. - // Peers declaring that version but still violating Toccata rules will be caught during normal IBD validation. + // If the pruning point is post-activation, its header is validated as part of the pruning proof. if params.covenants_activation.is_active(pp_daa_score) { - if applied_protocol_version < PROTOCOL_VERSION { - return Err(ProtocolError::OtherOwned(format!( - "syncer pruning point {} is post-Toccata activation, but peer protocol version {} is below required version {}", - pp_hash, applied_protocol_version, PROTOCOL_VERSION - ))); - } return Ok(()); } @@ -1119,7 +1127,6 @@ mod tests { #[test] fn test_toccata_pruning_point_staleness_guard() { - const V: u32 = PROTOCOL_VERSION; const ONE_DAY_MILLIS: u64 = 24 * 60 * 60 * 1000; let blocks_per_day = ONE_DAY_MILLIS / MAINNET_PARAMS.target_time_per_block(); let activation_daa_score = 10_000_000; @@ -1134,7 +1141,7 @@ mod tests { // No activation is configured: // PP(pre-activation by score) ---- estimated activation ---- pruning period + margin ---- now assert!( - validate_pruning_point_freshness_for_toccata(&MAINNET_PARAMS, V, pp_hash, pp_timestamp, pp_daa_score, stale_after + 1) + validate_pruning_point_freshness_for_toccata(&MAINNET_PARAMS, pp_hash, pp_timestamp, pp_daa_score, stale_after + 1) .is_ok() ); @@ -1142,33 +1149,24 @@ mod tests { // PP/now -------- 10d -------- activation let pp_ten_days_before_activation = activation_daa_score - 10 * blocks_per_day; assert!( - validate_pruning_point_freshness_for_toccata( - ¶ms, - V - 1, - pp_hash, - pp_timestamp, - pp_ten_days_before_activation, - pp_timestamp - ) - .is_ok() + validate_pruning_point_freshness_for_toccata(¶ms, pp_hash, pp_timestamp, pp_ten_days_before_activation, pp_timestamp) + .is_ok() ); - // The syncer's pruning point is already post-activation, so ordinary IBD validation takes over: + // The syncer's pruning point is already post-activation, so the staleness guard is done: // PP(post-activation by score) ----------------------------------------------- now assert!( - validate_pruning_point_freshness_for_toccata(¶ms, V, pp_hash, pp_timestamp, activation_daa_score, stale_after + 1) + validate_pruning_point_freshness_for_toccata(¶ms, pp_hash, pp_timestamp, activation_daa_score, stale_after + 1) .is_ok() ); // Last tolerated instant for a pre-activation pruning point: // PP ---- estimated activation ---- pruning period + margin == now - assert!(validate_pruning_point_freshness_for_toccata(¶ms, V, pp_hash, pp_timestamp, pp_daa_score, stale_after).is_ok()); + assert!(validate_pruning_point_freshness_for_toccata(¶ms, pp_hash, pp_timestamp, pp_daa_score, stale_after).is_ok()); // One millisecond later, the same pre-activation pruning point is stale: // PP ---- estimated activation ---- pruning period + margin < now - assert!( - validate_pruning_point_freshness_for_toccata(¶ms, V, pp_hash, pp_timestamp, pp_daa_score, stale_after + 1).is_err() - ); + assert!(validate_pruning_point_freshness_for_toccata(¶ms, pp_hash, pp_timestamp, pp_daa_score, stale_after + 1).is_err()); // Stale IBD: the syncer's pruning point is three days before activation, and now is // six days after that pruning point. Activation should have happened long enough ago @@ -1179,7 +1177,6 @@ mod tests { assert!( validate_pruning_point_freshness_for_toccata( ¶ms, - V, pp_hash, pp_timestamp, pp_three_days_before_activation, @@ -1197,7 +1194,6 @@ mod tests { assert!( validate_pruning_point_freshness_for_toccata( ¶ms, - V, pp_hash, pp_just_before_activation_timestamp, pp_just_before_activation, From 18af1b4131dd49649efda61463a5f6710751f1b1 Mon Sep 17 00:00:00 2001 From: Michael Sutton Date: Thu, 14 May 2026 15:45:45 +0300 Subject: [PATCH 5/6] gate toccata p2p advertisement by activation Keep protocol 10 support available locally, but advertise protocol 9 on networks without a scheduled Toccata activation. This lets future enforcing peers reject pre-rollout mainnet nodes while automatically advertising protocol 10 once activation is configured. --- protocol/flows/src/flow_context.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/protocol/flows/src/flow_context.rs b/protocol/flows/src/flow_context.rs index fcd61df59a..89f4b1d273 100644 --- a/protocol/flows/src/flow_context.rs +++ b/protocol/flows/src/flow_context.rs @@ -11,7 +11,7 @@ use kaspa_addressmanager::AddressManager; use kaspa_connectionmanager::ConnectionManager; use kaspa_consensus_core::api::{BlockValidationFuture, BlockValidationFutures}; use kaspa_consensus_core::block::Block; -use kaspa_consensus_core::config::Config; +use kaspa_consensus_core::config::{Config, params::ForkActivation}; use kaspa_consensus_core::errors::block::RuleError; use kaspa_consensus_core::network::{NetworkId, NetworkType}; use kaspa_consensus_core::tx::{Transaction, TransactionId}; @@ -61,7 +61,7 @@ use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use uuid::Uuid; /// The P2P protocol version. -pub(crate) const PROTOCOL_VERSION: u32 = 10; +const PROTOCOL_VERSION: u32 = 10; /// Testnet 12 was launched with the Toccata flow set under protocol version 9. const TN12_LAUNCH_PROTOCOL_VERSION: u32 = 9; @@ -717,9 +717,15 @@ impl ConnectionInitializer for FlowContext { let local_address = self.address_manager.lock().best_local_address(); + // Networks with a scheduled Toccata activation advertise protocol 10. Other networks + // still support v10 locally, but advertise v9 so future Toccata-activated peers reject them. + let advertise_toccata_p2p = self.config.covenants_activation != ForkActivation::never(); + let advertised_protocol_version = if advertise_toccata_p2p { PROTOCOL_VERSION } else { 9 }; + // Build the local version message // Subnets are not currently supported - let mut self_version_message = Version::new(local_address, self.node_id, network_name.clone(), None, PROTOCOL_VERSION); + let mut self_version_message = + Version::new(local_address, self.node_id, network_name.clone(), None, advertised_protocol_version); self_version_message.add_user_agent(name(), version(), &self.config.user_agent_comments); // TODO: disable_relay_tx from config/cmd From e4ecd533edb4eb95c2705ab5b402b21d7378c19a Mon Sep 17 00:00:00 2001 From: Michael Sutton Date: Thu, 14 May 2026 16:22:07 +0300 Subject: [PATCH 6/6] review comment: make toccata ibd freshness tests independent of mainnet params --- protocol/flows/src/ibd/flow.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/protocol/flows/src/ibd/flow.rs b/protocol/flows/src/ibd/flow.rs index ce00fee406..633a2e676a 100644 --- a/protocol/flows/src/ibd/flow.rs +++ b/protocol/flows/src/ibd/flow.rs @@ -1115,12 +1115,18 @@ mod tests { use super::*; use kaspa_consensus_core::config::params::MAINNET_PARAMS; - fn params_with_covenants_activation(activation_daa_score: u64) -> Params { + fn params_with_toccata_activation(activation_daa_score: u64) -> Params { let mut params = MAINNET_PARAMS.clone(); params.covenants_activation = ForkActivation::new(activation_daa_score); params } + fn params_without_toccata_activation() -> Params { + let mut params = MAINNET_PARAMS.clone(); + params.covenants_activation = ForkActivation::never(); + params + } + fn pruning_period_millis(params: &Params) -> u64 { params.pruning_depth().saturating_add(params.finality_depth()).saturating_mul(params.target_time_per_block()) } @@ -1128,9 +1134,9 @@ mod tests { #[test] fn test_toccata_pruning_point_staleness_guard() { const ONE_DAY_MILLIS: u64 = 24 * 60 * 60 * 1000; - let blocks_per_day = ONE_DAY_MILLIS / MAINNET_PARAMS.target_time_per_block(); let activation_daa_score = 10_000_000; - let params = params_with_covenants_activation(activation_daa_score); + let params = params_with_toccata_activation(activation_daa_score); + let blocks_per_day = ONE_DAY_MILLIS / params.target_time_per_block(); let pp_hash = Hash::from_u64_word(1); let pp_daa_score = activation_daa_score - 10; let pp_timestamp = 1_000_000_000_000; @@ -1141,8 +1147,14 @@ mod tests { // No activation is configured: // PP(pre-activation by score) ---- estimated activation ---- pruning period + margin ---- now assert!( - validate_pruning_point_freshness_for_toccata(&MAINNET_PARAMS, pp_hash, pp_timestamp, pp_daa_score, stale_after + 1) - .is_ok() + validate_pruning_point_freshness_for_toccata( + ¶ms_without_toccata_activation(), + pp_hash, + pp_timestamp, + pp_daa_score, + stale_after + 1 + ) + .is_ok() ); // Normal pre-activation IBD: activation is still ten days away.