Skip to content

Commit e274ed9

Browse files
[Backport release-mainnet-1.2.0-rc] fix(libp2p): add network discriminators (#4443)
fix(libp2p): add network discriminators (#4418) * test(libp2p): snapshot mainnet protocol identifiers - Extract three helpers in `network/node.rs` returning the libp2p protocol identifiers used today (gossipsub prefix `None` = libp2p default, kademlia `/ipfs/kad/1.0.0`, direct message `/HotShot/direct_message/1.0`). - Wire the helpers into the swarm builder so they are the single source of truth. - Add an `insta` snapshot test that locks in the values. A future change that would silently shift these strings (and partition mainnet from itself) now fails the snapshot. * feat(libp2p): partition non-mainnet networks via chain-id discriminator - Add `network_discriminator: Option<String>` to `NetworkNodeConfig`. `None` keeps mainnet's libp2p protocol strings byte-identical to today; `Some(d)` appends `/{d}` to gossipsub's `protocol_id_prefix`, the kademlia `StreamProtocol`, and the request-response direct message `StreamProtocol`. Identify's `protocol_version` is left untouched since it does not gate substream negotiation. - Plumb the discriminator through `Libp2pNetwork::from_config`. - At the espresso-node call site, derive the discriminator from `genesis.chain_config.chain_id`: `None` when chain_id is 1 (mainnet), else `Some(chain_id.to_string())`. Decaf and other non-mainnet networks now run on distinct libp2p protocols and cannot merge with mainnet at the gossipsub or kademlia layers. - The snapshot test added in the previous commit confirms the mainnet path is unchanged. * doc(libp2p): tighten network_discriminator field comment * test(libp2p): snapshot decaf protocol identifiers - Extract three `gossipsub_prefix` / `kad_protocol` / `direct_message_protocol` wrappers that take an optional discriminator and call the existing mainnet helpers when `None`. - Replace the inline match arms in `NetworkNode::new` with calls to these wrappers so production code and tests share a single path. - Add a decaf snapshot test alongside the mainnet one, both routed through a shared `snapshot_for(discriminator)` helper. Decaf's chain_id (`0xdecaf` = 912559) produces protocol IDs like `/ipfs/kad/1.0.0/912559`. * refactor(libp2p): type network_discriminator as Option<U256> - Discriminator is always a chain id; use `U256` rather than `String` at the libp2p-networking boundary. - Format chain ids as hex with the `0x` prefix in the protocol strings, matching the spelling in `data/genesis/decaf.toml`. Decaf's protocols become `/ipfs/kad/1.0.0/0xdecaf`, etc. - Test reads naturally as `snapshot_for(Some(U256::from(0xdecafu64)))`. - Mainnet snapshot is unchanged (still goes through the `None` branch). * refactor(node): promote MAINNET_CHAIN_ID to lib.rs, type as ChainId - Move the constant out of `api/unlock_schedule.rs` so the libp2p call site can reuse it instead of comparing against a bare `U256::ONE`. - Retype from `u64 = 1` to `ChainId = ChainId(U256::ONE)` so callers no longer wrap it in `U256::from(...)` or `ChainId(...)` at each use site. (cherry picked from commit 284de7a) Co-authored-by: Mathis <sveitser@gmail.com>
1 parent ada9582 commit e274ed9

10 files changed

Lines changed: 120 additions & 8 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/espresso/node/src/api/unlock_schedule.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ use chrono::{Months, NaiveDate, NaiveDateTime, NaiveTime};
55
use espresso_types::{v0_1::ChainId, v0_3::RewardAmount};
66
use serde::Deserialize;
77

8-
const SCHEDULE_TOML: &str = include_str!("../../../../../data/token-unlock-schedule.toml");
8+
use crate::MAINNET_CHAIN_ID;
99

10-
pub(crate) const MAINNET_CHAIN_ID: u64 = 1;
10+
const SCHEDULE_TOML: &str = include_str!("../../../../../data/token-unlock-schedule.toml");
1111

1212
#[derive(Deserialize)]
1313
struct UnlockEntry {
@@ -120,7 +120,7 @@ impl SupplyCalculator {
120120
/// Tokens still locked on L1 per the unlock schedule.
121121
/// Mainnet: `initial_supply - unlocked(now)`. Non-mainnet: `0`.
122122
fn locked(&self) -> U256 {
123-
if self.chain_id == U256::from(MAINNET_CHAIN_ID) {
123+
if self.chain_id == MAINNET_CHAIN_ID.0 {
124124
self.initial_supply
125125
.saturating_sub(unlocked_amount_at(self.now_secs))
126126
} else {
@@ -245,7 +245,7 @@ mod tests {
245245
// --- SupplyCalculator tests ---
246246

247247
fn mainnet_id() -> ChainId {
248-
ChainId(U256::from(MAINNET_CHAIN_ID))
248+
MAINNET_CHAIN_ID
249249
}
250250

251251
fn testnet_id() -> ChainId {

crates/espresso/node/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@ use espresso_types::{
3232
SeqTypes, ValidatedState,
3333
traits::{EventConsumer, MembershipPersistence},
3434
v0::traits::SequencerPersistence,
35+
v0_1::ChainId,
3536
v0_3::Fetcher,
3637
};
38+
39+
pub(crate) const MAINNET_CHAIN_ID: ChainId = ChainId(U256::ONE);
3740
pub use genesis::Genesis;
3841
use genesis::L1Finalized;
3942
use hotshot::{
@@ -758,6 +761,9 @@ where
758761

759762
let combined_network = {
760763
info!("Initializing Libp2p network");
764+
// Mainnet keeps today's libp2p protocol strings byte-identical.
765+
let chain_id = genesis.chain_config.chain_id;
766+
let network_discriminator = (chain_id != MAINNET_CHAIN_ID).then_some(chain_id.0);
761767
let p2p_network = Libp2pNetwork::from_config(
762768
network_config.clone(),
763769
persistence.clone(),
@@ -770,6 +776,7 @@ where
770776
// (using https://docs.rs/blake3/latest/blake3/fn.derive_key.html)
771777
&validator_config.private_key,
772778
hotshot::traits::implementations::Libp2pMetricsValue::new(&*metrics),
779+
network_discriminator,
773780
)
774781
.await
775782
.with_context(|| {

crates/hotshot/examples/src/infra.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,7 @@ where
769769
public_key,
770770
private_key,
771771
Libp2pMetricsValue::default(),
772+
None,
772773
)
773774
.await
774775
.expect("failed to create libp2p network");

crates/hotshot/hotshot/src/traits/networking/libp2p_network.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use std::{
2222
#[cfg(feature = "hotshot-testing")]
2323
use std::{collections::HashMap, str::FromStr};
2424

25+
use alloy::primitives::U256;
2526
use anyhow::{Context, anyhow};
2627
use async_lock::RwLock;
2728
use async_trait::async_trait;
@@ -407,6 +408,7 @@ impl<T: NodeType> Libp2pNetwork<T> {
407408
pub_key: &T::SignatureKey,
408409
priv_key: &<T::SignatureKey as SignatureKey>::PrivateKey,
409410
metrics: Libp2pMetricsValue,
411+
network_discriminator: Option<U256>,
410412
) -> anyhow::Result<Self> {
411413
// Try to take our Libp2p config from our broader network config
412414
let libp2p_config = config
@@ -456,7 +458,8 @@ impl<T: NodeType> Libp2pNetwork<T> {
456458
.keypair(keypair)
457459
.replication_factor(replication_factor)
458460
.bind_address(Some(bind_address.clone()))
459-
.announce_addresses(announce_addresses);
461+
.announce_addresses(announce_addresses)
462+
.network_discriminator(network_discriminator);
460463

461464
// Connect to the provided bootstrap nodes
462465
config_builder.to_connect_addrs(HashSet::from_iter(libp2p_config.bootstrap_nodes.clone()));

crates/hotshot/libp2p-networking/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ tracing-subscriber = { workspace = true }
3838

3939
[dev-dependencies]
4040
hotshot-example-types = { workspace = true }
41+
insta = { workspace = true }
4142
tracing-test = { workspace = true }
4243

4344
[lints]

crates/hotshot/libp2p-networking/src/network/node.rs

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use std::{
1919
time::{Duration, Instant},
2020
};
2121

22+
use alloy::primitives::U256;
2223
use bimap::BiMap;
2324
use futures::{SinkExt, StreamExt, channel::mpsc};
2425
use hotshot_types::{
@@ -87,6 +88,53 @@ pub const ESTABLISHED_LIMIT: NonZeroU32 = NonZeroU32::new(ESTABLISHED_LIMIT_UNWR
8788
/// Number of connections to a single peer before logging an error
8889
pub const ESTABLISHED_LIMIT_UNWR: u32 = 10;
8990

91+
/// Mainnet libp2p protocol identifiers. The snapshot tests below lock these down so a
92+
/// change that would partition mainnet (e.g. a stray `protocol_id_prefix` call) is caught.
93+
/// `None` for gossipsub means "do not call `protocol_id_prefix`" — libp2p's defaults
94+
/// (`/meshsub/1.1.0` and `/meshsub/1.0.0`) are then used.
95+
pub(crate) fn mainnet_gossipsub_prefix() -> Option<&'static str> {
96+
None
97+
}
98+
99+
pub(crate) fn mainnet_kad_protocol() -> StreamProtocol {
100+
StreamProtocol::new("/ipfs/kad/1.0.0")
101+
}
102+
103+
pub(crate) fn mainnet_direct_message_protocol() -> StreamProtocol {
104+
StreamProtocol::new("/HotShot/direct_message/1.0")
105+
}
106+
107+
/// Resolve the gossipsub `protocol_id_prefix` for the given network discriminator.
108+
/// `None` returns the mainnet value (the libp2p default).
109+
pub(crate) fn gossipsub_prefix(discriminator: Option<U256>) -> Option<String> {
110+
match discriminator {
111+
None => mainnet_gossipsub_prefix().map(String::from),
112+
Some(d) => Some(format!("/HotShot/gossipsub/1.0/{d:#x}")),
113+
}
114+
}
115+
116+
/// Resolve the kademlia stream protocol for the given network discriminator.
117+
pub(crate) fn kad_protocol(discriminator: Option<U256>) -> Result<StreamProtocol, NetworkError> {
118+
match discriminator {
119+
None => Ok(mainnet_kad_protocol()),
120+
Some(d) => StreamProtocol::try_from_owned(format!("/ipfs/kad/1.0.0/{d:#x}"))
121+
.map_err(|err| NetworkError::ConfigError(format!("invalid kademlia protocol: {err}"))),
122+
}
123+
}
124+
125+
/// Resolve the direct-message stream protocol for the given network discriminator.
126+
pub(crate) fn direct_message_protocol(
127+
discriminator: Option<U256>,
128+
) -> Result<StreamProtocol, NetworkError> {
129+
match discriminator {
130+
None => Ok(mainnet_direct_message_protocol()),
131+
Some(d) => StreamProtocol::try_from_owned(format!("/HotShot/direct_message/1.0/{d:#x}"))
132+
.map_err(|err| {
133+
NetworkError::ConfigError(format!("invalid direct_message protocol: {err}"))
134+
}),
135+
}
136+
}
137+
90138
/// Network definition
91139
#[derive(derive_more::Debug)]
92140
pub struct NetworkNode<T: NodeType, D: DhtPersistentStorage> {
@@ -215,7 +263,11 @@ impl<T: NodeType, D: DhtPersistentStorage> NetworkNode<T, D> {
215263
};
216264

217265
// Derive a `Gossipsub` config from our gossip config
218-
let gossipsub_config = GossipsubConfigBuilder::default()
266+
let mut gossipsub_builder = GossipsubConfigBuilder::default();
267+
if let Some(prefix) = gossipsub_prefix(config.network_discriminator) {
268+
gossipsub_builder.protocol_id_prefix(prefix);
269+
}
270+
let gossipsub_config = gossipsub_builder
219271
.message_id_fn(message_id_fn) // Use the (blake3) hash of a message as its ID
220272
.validation_mode(ValidationMode::Strict) // Force all messages to have valid signatures
221273
.heartbeat_interval(config.gossip_config.heartbeat_interval) // Time between gossip heartbeats
@@ -263,7 +315,7 @@ impl<T: NodeType, D: DhtPersistentStorage> NetworkNode<T, D> {
263315
let identify = IdentifyBehaviour::new(identify_cfg);
264316

265317
// - Build DHT needed for peer discovery
266-
let mut kconfig = Config::new(StreamProtocol::new("/ipfs/kad/1.0.0"));
318+
let mut kconfig = Config::new(kad_protocol(config.network_discriminator)?);
267319
kconfig
268320
.set_parallelism(NonZeroUsize::new(5).unwrap())
269321
.set_provider_publication_interval(Some(kademlia_record_republication_interval))
@@ -303,7 +355,7 @@ impl<T: NodeType, D: DhtPersistentStorage> NetworkNode<T, D> {
303355
RequestResponse::with_codec(
304356
cbor,
305357
[(
306-
StreamProtocol::new("/HotShot/direct_message/1.0"),
358+
direct_message_protocol(config.network_discriminator)?,
307359
ProtocolSupport::Full,
308360
)],
309361
rrconfig.clone(),
@@ -817,3 +869,30 @@ impl<T: NodeType, D: DhtPersistentStorage> NetworkNode<T, D> {
817869
self.peer_id
818870
}
819871
}
872+
873+
#[cfg(test)]
874+
mod tests {
875+
use super::{U256, direct_message_protocol, gossipsub_prefix, kad_protocol};
876+
877+
fn snapshot_for(discriminator: Option<U256>) -> String {
878+
format!(
879+
"gossipsub_prefix: {:?}\nkad: {}\ndirect_message: {}",
880+
gossipsub_prefix(discriminator),
881+
kad_protocol(discriminator).unwrap(),
882+
direct_message_protocol(discriminator).unwrap(),
883+
)
884+
}
885+
886+
#[test]
887+
fn mainnet_libp2p_protocol_identifiers() {
888+
insta::assert_snapshot!("mainnet_libp2p_protocol_identifiers", snapshot_for(None));
889+
}
890+
891+
#[test]
892+
fn decaf_libp2p_protocol_identifiers() {
893+
insta::assert_snapshot!(
894+
"decaf_libp2p_protocol_identifiers",
895+
snapshot_for(Some(U256::from(0xdecafu64)))
896+
);
897+
}
898+
}

crates/hotshot/libp2p-networking/src/network/node/config.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use std::{collections::HashSet, num::NonZeroUsize, time::Duration};
88

9+
use alloy::primitives::U256;
910
use libp2p::{Multiaddr, identity::Keypair};
1011
use libp2p_identity::PeerId;
1112

@@ -70,6 +71,10 @@ pub struct NetworkNodeConfig {
7071
#[builder(default)]
7172
/// The timeout for DHT lookups.
7273
pub dht_timeout: Option<Duration>,
74+
75+
/// `None` is the legacy value, used for mainnet.
76+
#[builder(default)]
77+
pub network_discriminator: Option<U256>,
7378
}
7479

7580
impl Clone for NetworkNodeConfig {
@@ -87,6 +92,7 @@ impl Clone for NetworkNodeConfig {
8792
dht_file_path: self.dht_file_path.clone(),
8893
auth_message: self.auth_message.clone(),
8994
dht_timeout: self.dht_timeout,
95+
network_discriminator: self.network_discriminator,
9096
}
9197
}
9298
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
source: crates/hotshot/libp2p-networking/src/network/node.rs
3+
expression: "snapshot_for(Some(U256::from(0xdecafu64)))"
4+
---
5+
gossipsub_prefix: Some("/HotShot/gossipsub/1.0/0xdecaf")
6+
kad: /ipfs/kad/1.0.0/0xdecaf
7+
direct_message: /HotShot/direct_message/1.0/0xdecaf
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
source: crates/hotshot/libp2p-networking/src/network/node.rs
3+
expression: snapshot_for(None)
4+
---
5+
gossipsub_prefix: None
6+
kad: /ipfs/kad/1.0.0
7+
direct_message: /HotShot/direct_message/1.0

0 commit comments

Comments
 (0)