diff --git a/Cargo.lock b/Cargo.lock index 0c70b33ba97..a4af411eb71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6335,6 +6335,7 @@ dependencies = [ "futures", "hotshot-example-types", "hotshot-types", + "insta", "lazy_static", "libp2p", "libp2p-identity", diff --git a/crates/espresso/node/src/api/unlock_schedule.rs b/crates/espresso/node/src/api/unlock_schedule.rs index f9a408f6060..9bbd3518d0d 100644 --- a/crates/espresso/node/src/api/unlock_schedule.rs +++ b/crates/espresso/node/src/api/unlock_schedule.rs @@ -5,9 +5,9 @@ use chrono::{Months, NaiveDate, NaiveDateTime, NaiveTime}; use espresso_types::{v0_1::ChainId, v0_3::RewardAmount}; use serde::Deserialize; -const SCHEDULE_TOML: &str = include_str!("../../../../../data/token-unlock-schedule.toml"); +use crate::MAINNET_CHAIN_ID; -pub(crate) const MAINNET_CHAIN_ID: u64 = 1; +const SCHEDULE_TOML: &str = include_str!("../../../../../data/token-unlock-schedule.toml"); #[derive(Deserialize)] struct UnlockEntry { @@ -120,7 +120,7 @@ impl SupplyCalculator { /// Tokens still locked on L1 per the unlock schedule. /// Mainnet: `initial_supply - unlocked(now)`. Non-mainnet: `0`. fn locked(&self) -> U256 { - if self.chain_id == U256::from(MAINNET_CHAIN_ID) { + if self.chain_id == MAINNET_CHAIN_ID.0 { self.initial_supply .saturating_sub(unlocked_amount_at(self.now_secs)) } else { @@ -245,7 +245,7 @@ mod tests { // --- SupplyCalculator tests --- fn mainnet_id() -> ChainId { - ChainId(U256::from(MAINNET_CHAIN_ID)) + MAINNET_CHAIN_ID } fn testnet_id() -> ChainId { diff --git a/crates/espresso/node/src/lib.rs b/crates/espresso/node/src/lib.rs index 1dc86d24050..a0cfd1bdd86 100644 --- a/crates/espresso/node/src/lib.rs +++ b/crates/espresso/node/src/lib.rs @@ -32,8 +32,11 @@ use espresso_types::{ SeqTypes, ValidatedState, traits::{EventConsumer, MembershipPersistence}, v0::traits::SequencerPersistence, + v0_1::ChainId, v0_3::Fetcher, }; + +pub(crate) const MAINNET_CHAIN_ID: ChainId = ChainId(U256::ONE); pub use genesis::Genesis; use genesis::L1Finalized; use hotshot::{ @@ -758,6 +761,9 @@ where let combined_network = { info!("Initializing Libp2p network"); + // Mainnet keeps today's libp2p protocol strings byte-identical. + let chain_id = genesis.chain_config.chain_id; + let network_discriminator = (chain_id != MAINNET_CHAIN_ID).then_some(chain_id.0); let p2p_network = Libp2pNetwork::from_config( network_config.clone(), persistence.clone(), @@ -770,6 +776,7 @@ where // (using https://docs.rs/blake3/latest/blake3/fn.derive_key.html) &validator_config.private_key, hotshot::traits::implementations::Libp2pMetricsValue::new(&*metrics), + network_discriminator, ) .await .with_context(|| { diff --git a/crates/hotshot/examples/src/infra.rs b/crates/hotshot/examples/src/infra.rs index 7d0fadf50ec..053b2f028e2 100755 --- a/crates/hotshot/examples/src/infra.rs +++ b/crates/hotshot/examples/src/infra.rs @@ -769,6 +769,7 @@ where public_key, private_key, Libp2pMetricsValue::default(), + None, ) .await .expect("failed to create libp2p network"); diff --git a/crates/hotshot/hotshot/src/traits/networking/libp2p_network.rs b/crates/hotshot/hotshot/src/traits/networking/libp2p_network.rs index ac14ec762a6..27a489988b4 100644 --- a/crates/hotshot/hotshot/src/traits/networking/libp2p_network.rs +++ b/crates/hotshot/hotshot/src/traits/networking/libp2p_network.rs @@ -22,6 +22,7 @@ use std::{ #[cfg(feature = "hotshot-testing")] use std::{collections::HashMap, str::FromStr}; +use alloy::primitives::U256; use anyhow::{Context, anyhow}; use async_lock::RwLock; use async_trait::async_trait; @@ -407,6 +408,7 @@ impl Libp2pNetwork { pub_key: &T::SignatureKey, priv_key: &::PrivateKey, metrics: Libp2pMetricsValue, + network_discriminator: Option, ) -> anyhow::Result { // Try to take our Libp2p config from our broader network config let libp2p_config = config @@ -456,7 +458,8 @@ impl Libp2pNetwork { .keypair(keypair) .replication_factor(replication_factor) .bind_address(Some(bind_address.clone())) - .announce_addresses(announce_addresses); + .announce_addresses(announce_addresses) + .network_discriminator(network_discriminator); // Connect to the provided bootstrap nodes config_builder.to_connect_addrs(HashSet::from_iter(libp2p_config.bootstrap_nodes.clone())); diff --git a/crates/hotshot/libp2p-networking/Cargo.toml b/crates/hotshot/libp2p-networking/Cargo.toml index 9e658dd9248..f90b79e3cac 100644 --- a/crates/hotshot/libp2p-networking/Cargo.toml +++ b/crates/hotshot/libp2p-networking/Cargo.toml @@ -38,6 +38,7 @@ tracing-subscriber = { workspace = true } [dev-dependencies] hotshot-example-types = { workspace = true } +insta = { workspace = true } tracing-test = { workspace = true } [lints] diff --git a/crates/hotshot/libp2p-networking/src/network/node.rs b/crates/hotshot/libp2p-networking/src/network/node.rs index a58871ad0aa..14e65ee0f94 100644 --- a/crates/hotshot/libp2p-networking/src/network/node.rs +++ b/crates/hotshot/libp2p-networking/src/network/node.rs @@ -19,6 +19,7 @@ use std::{ time::{Duration, Instant}, }; +use alloy::primitives::U256; use bimap::BiMap; use futures::{SinkExt, StreamExt, channel::mpsc}; use hotshot_types::{ @@ -87,6 +88,53 @@ pub const ESTABLISHED_LIMIT: NonZeroU32 = NonZeroU32::new(ESTABLISHED_LIMIT_UNWR /// Number of connections to a single peer before logging an error pub const ESTABLISHED_LIMIT_UNWR: u32 = 10; +/// Mainnet libp2p protocol identifiers. The snapshot tests below lock these down so a +/// change that would partition mainnet (e.g. a stray `protocol_id_prefix` call) is caught. +/// `None` for gossipsub means "do not call `protocol_id_prefix`" — libp2p's defaults +/// (`/meshsub/1.1.0` and `/meshsub/1.0.0`) are then used. +pub(crate) fn mainnet_gossipsub_prefix() -> Option<&'static str> { + None +} + +pub(crate) fn mainnet_kad_protocol() -> StreamProtocol { + StreamProtocol::new("/ipfs/kad/1.0.0") +} + +pub(crate) fn mainnet_direct_message_protocol() -> StreamProtocol { + StreamProtocol::new("/HotShot/direct_message/1.0") +} + +/// Resolve the gossipsub `protocol_id_prefix` for the given network discriminator. +/// `None` returns the mainnet value (the libp2p default). +pub(crate) fn gossipsub_prefix(discriminator: Option) -> Option { + match discriminator { + None => mainnet_gossipsub_prefix().map(String::from), + Some(d) => Some(format!("/HotShot/gossipsub/1.0/{d:#x}")), + } +} + +/// Resolve the kademlia stream protocol for the given network discriminator. +pub(crate) fn kad_protocol(discriminator: Option) -> Result { + match discriminator { + None => Ok(mainnet_kad_protocol()), + Some(d) => StreamProtocol::try_from_owned(format!("/ipfs/kad/1.0.0/{d:#x}")) + .map_err(|err| NetworkError::ConfigError(format!("invalid kademlia protocol: {err}"))), + } +} + +/// Resolve the direct-message stream protocol for the given network discriminator. +pub(crate) fn direct_message_protocol( + discriminator: Option, +) -> Result { + match discriminator { + None => Ok(mainnet_direct_message_protocol()), + Some(d) => StreamProtocol::try_from_owned(format!("/HotShot/direct_message/1.0/{d:#x}")) + .map_err(|err| { + NetworkError::ConfigError(format!("invalid direct_message protocol: {err}")) + }), + } +} + /// Network definition #[derive(derive_more::Debug)] pub struct NetworkNode { @@ -215,7 +263,11 @@ impl NetworkNode { }; // Derive a `Gossipsub` config from our gossip config - let gossipsub_config = GossipsubConfigBuilder::default() + let mut gossipsub_builder = GossipsubConfigBuilder::default(); + if let Some(prefix) = gossipsub_prefix(config.network_discriminator) { + gossipsub_builder.protocol_id_prefix(prefix); + } + let gossipsub_config = gossipsub_builder .message_id_fn(message_id_fn) // Use the (blake3) hash of a message as its ID .validation_mode(ValidationMode::Strict) // Force all messages to have valid signatures .heartbeat_interval(config.gossip_config.heartbeat_interval) // Time between gossip heartbeats @@ -263,7 +315,7 @@ impl NetworkNode { let identify = IdentifyBehaviour::new(identify_cfg); // - Build DHT needed for peer discovery - let mut kconfig = Config::new(StreamProtocol::new("/ipfs/kad/1.0.0")); + let mut kconfig = Config::new(kad_protocol(config.network_discriminator)?); kconfig .set_parallelism(NonZeroUsize::new(5).unwrap()) .set_provider_publication_interval(Some(kademlia_record_republication_interval)) @@ -303,7 +355,7 @@ impl NetworkNode { RequestResponse::with_codec( cbor, [( - StreamProtocol::new("/HotShot/direct_message/1.0"), + direct_message_protocol(config.network_discriminator)?, ProtocolSupport::Full, )], rrconfig.clone(), @@ -817,3 +869,30 @@ impl NetworkNode { self.peer_id } } + +#[cfg(test)] +mod tests { + use super::{U256, direct_message_protocol, gossipsub_prefix, kad_protocol}; + + fn snapshot_for(discriminator: Option) -> String { + format!( + "gossipsub_prefix: {:?}\nkad: {}\ndirect_message: {}", + gossipsub_prefix(discriminator), + kad_protocol(discriminator).unwrap(), + direct_message_protocol(discriminator).unwrap(), + ) + } + + #[test] + fn mainnet_libp2p_protocol_identifiers() { + insta::assert_snapshot!("mainnet_libp2p_protocol_identifiers", snapshot_for(None)); + } + + #[test] + fn decaf_libp2p_protocol_identifiers() { + insta::assert_snapshot!( + "decaf_libp2p_protocol_identifiers", + snapshot_for(Some(U256::from(0xdecafu64))) + ); + } +} diff --git a/crates/hotshot/libp2p-networking/src/network/node/config.rs b/crates/hotshot/libp2p-networking/src/network/node/config.rs index d4b08a4ffdb..6159e2cccf2 100644 --- a/crates/hotshot/libp2p-networking/src/network/node/config.rs +++ b/crates/hotshot/libp2p-networking/src/network/node/config.rs @@ -6,6 +6,7 @@ use std::{collections::HashSet, num::NonZeroUsize, time::Duration}; +use alloy::primitives::U256; use libp2p::{Multiaddr, identity::Keypair}; use libp2p_identity::PeerId; @@ -70,6 +71,10 @@ pub struct NetworkNodeConfig { #[builder(default)] /// The timeout for DHT lookups. pub dht_timeout: Option, + + /// `None` is the legacy value, used for mainnet. + #[builder(default)] + pub network_discriminator: Option, } impl Clone for NetworkNodeConfig { @@ -87,6 +92,7 @@ impl Clone for NetworkNodeConfig { dht_file_path: self.dht_file_path.clone(), auth_message: self.auth_message.clone(), dht_timeout: self.dht_timeout, + network_discriminator: self.network_discriminator, } } } diff --git a/crates/hotshot/libp2p-networking/src/network/snapshots/hotshot_libp2p_networking__network__node__tests__decaf_libp2p_protocol_identifiers.snap b/crates/hotshot/libp2p-networking/src/network/snapshots/hotshot_libp2p_networking__network__node__tests__decaf_libp2p_protocol_identifiers.snap new file mode 100644 index 00000000000..44282439d43 --- /dev/null +++ b/crates/hotshot/libp2p-networking/src/network/snapshots/hotshot_libp2p_networking__network__node__tests__decaf_libp2p_protocol_identifiers.snap @@ -0,0 +1,7 @@ +--- +source: crates/hotshot/libp2p-networking/src/network/node.rs +expression: "snapshot_for(Some(U256::from(0xdecafu64)))" +--- +gossipsub_prefix: Some("/HotShot/gossipsub/1.0/0xdecaf") +kad: /ipfs/kad/1.0.0/0xdecaf +direct_message: /HotShot/direct_message/1.0/0xdecaf diff --git a/crates/hotshot/libp2p-networking/src/network/snapshots/hotshot_libp2p_networking__network__node__tests__mainnet_libp2p_protocol_identifiers.snap b/crates/hotshot/libp2p-networking/src/network/snapshots/hotshot_libp2p_networking__network__node__tests__mainnet_libp2p_protocol_identifiers.snap new file mode 100644 index 00000000000..c87fecf77bf --- /dev/null +++ b/crates/hotshot/libp2p-networking/src/network/snapshots/hotshot_libp2p_networking__network__node__tests__mainnet_libp2p_protocol_identifiers.snap @@ -0,0 +1,7 @@ +--- +source: crates/hotshot/libp2p-networking/src/network/node.rs +expression: snapshot_for(None) +--- +gossipsub_prefix: None +kad: /ipfs/kad/1.0.0 +direct_message: /HotShot/direct_message/1.0