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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion consensus/core/src/config/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion consensus/core/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
60 changes: 50 additions & 10 deletions protocol/flows/src/flow_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ 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;
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};
use kaspa_consensus_notify::{
notification::{Notification, PruningPointUtxoSetOverrideNotification},
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -712,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

Expand Down Expand Up @@ -760,12 +771,41 @@ impl ConnectionInitializer for FlowContext {

debug!("protocol versions - self: {}, peer: {}", PROTOCOL_VERSION, 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),
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)),
// 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
};

// 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).
//
// 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 {
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
Expand Down
186 changes: 185 additions & 1 deletion protocol/flows/src/ibd/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -392,6 +393,21 @@ impl IbdFlow {
}

async fn sync_and_validate_pruning_proof(&mut self, staging: &ConsensusProxy, relay_block: &Block) -> Result<Hash, ProtocolError> {
// [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
Expand All @@ -412,7 +428,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"));
Expand All @@ -423,6 +440,15 @@ impl IbdFlow {
}
drop(consensus);

// [Toccata] Reject IBD from outdated peers
validate_pruning_point_freshness_for_toccata(
self.ctx.config.as_ref(),
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?;
Expand Down Expand Up @@ -1031,3 +1057,161 @@ 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.
///
/// 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,
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, its header is validated as part of the pruning proof.
if params.covenants_activation.is_active(pp_daa_score) {
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_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())
}

#[test]
fn test_toccata_pruning_point_staleness_guard() {
const ONE_DAY_MILLIS: u64 = 24 * 60 * 60 * 1000;
let activation_daa_score = 10_000_000;
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;
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(&params) + 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(
&params_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.
// PP/now -------- 10d -------- activation
let pp_ten_days_before_activation = activation_daa_score - 10 * blocks_per_day;
assert!(
validate_pruning_point_freshness_for_toccata(&params, pp_hash, pp_timestamp, pp_ten_days_before_activation, pp_timestamp)
.is_ok()
);

// 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(&params, 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(&params, 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(&params, 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(
&params,
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(
&params,
pp_hash,
pp_just_before_activation_timestamp,
pp_just_before_activation,
now_two_days_after_activation
)
.is_ok()
);
}
}
2 changes: 1 addition & 1 deletion protocol/flows/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
File renamed without changes.
Loading