iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

commit 6dfdff5f8a027e1abfd71fc397a680b82e8c2473
parent 7c72b1f8f7679ef4aff2558d62a5696c41b961e1
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Tue, 28 Jul 2026 00:38:32 +0200

Fix p2p setup sync and private peer discovery

Diffstat:
MCargo.lock | 2+-
MCargo.toml | 2+-
Msrc/adapters/p2p.rs | 253++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
3 files changed, 213 insertions(+), 44 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -502,7 +502,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "iuna" -version = "0.1.6" +version = "0.1.7" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iuna" -version = "0.1.6" +version = "0.1.7" edition = "2024" license = "Apache-2.0" diff --git a/src/adapters/p2p.rs b/src/adapters/p2p.rs @@ -49,7 +49,8 @@ const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(30); struct PeerStatus { height: u64, tip_hash: String, - force_snapshot: bool, + request_snapshot: bool, + push_snapshot: bool, } impl PeerStatus { @@ -57,15 +58,26 @@ impl PeerStatus { Self { height, tip_hash, - force_snapshot: false, + request_snapshot: false, + push_snapshot: false, } } - fn with_forced_snapshot(height: u64, tip_hash: String) -> Self { + fn with_snapshot_request(height: u64, tip_hash: String) -> Self { Self { height, tip_hash, - force_snapshot: true, + request_snapshot: true, + push_snapshot: false, + } + } + + fn with_snapshot_push(height: u64, tip_hash: String) -> Self { + Self { + height, + tip_hash, + request_snapshot: false, + push_snapshot: true, } } } @@ -836,11 +848,20 @@ async fn process_envelope( } GossipEnvelope::PeerAnnouncement { address } => { let peer = normalize_advertised_peer(&address, remote_addr)?; - *known_peer = Some(peer.clone()); + if is_self_peer_address(&peer, network.inner.listen_addr) { + P2pMetricsCounters::inc(&network.inner.metrics.self_peer_rejections); + } else if remember_discoverable_advertised_peer( + network, + remote_addr, + known_peer, + peer.clone(), + ) + .await? { - let mut peers = network.inner.peers.lock().await; - peers.add_peer(peer.clone()); - peers.record_received(&peer, 1); + if let Some(peer) = known_peer { + let mut peers = network.inner.peers.lock().await; + peers.record_received(peer, 1); + } } let snapshot = network.inner.node.lock().await.chain_snapshot(); write_envelope(writer, &GossipEnvelope::ChainSnapshot(snapshot)).await?; @@ -993,7 +1014,7 @@ async fn maybe_request_catchup( let status = node.ledger().status(); (status.height, status.tip_hash) }; - if peer_status.force_snapshot { + if peer_status.request_snapshot { write_envelope(writer, &GossipEnvelope::ChainSnapshotRequest).await?; } else if peer_status.height > local_height { write_envelope( @@ -1043,6 +1064,9 @@ async fn catchup_payload_for_peer( if node.ledger().is_setup_placeholder() { return Vec::new(); } + if peer_status.push_snapshot { + return vec![GossipEnvelope::ChainSnapshot(node.chain_snapshot())]; + } if peer_status.height < local_status.height { let blocks = node.blocks_from(peer_status.height + 1, MAX_BLOCK_BATCH); if blocks.is_empty() { @@ -1664,23 +1688,19 @@ async fn process_hello( NETWORK_ID ); } - let local_genesis = network - .inner - .node - .lock() - .await - .ledger() - .genesis_hash() - .to_string(); - let local_accepts_remote_genesis = network - .inner - .node - .lock() - .await - .ledger() - .is_setup_placeholder(); - let force_snapshot = hello.genesis_hash != local_genesis && local_accepts_remote_genesis; - if hello.genesis_hash != local_genesis && !local_accepts_remote_genesis { + let (local_genesis, local_accepts_remote_genesis) = { + let node = network.inner.node.lock().await; + ( + node.ledger().genesis_hash().to_string(), + node.ledger().is_setup_placeholder(), + ) + }; + let genesis_mismatch = hello.genesis_hash != local_genesis; + let remote_is_setup_placeholder = + hello.height == 0 && hello.genesis_hash == setup_placeholder_genesis_hash(); + let request_snapshot = genesis_mismatch && local_accepts_remote_genesis; + let push_snapshot = genesis_mismatch && remote_is_setup_placeholder; + if genesis_mismatch && !local_accepts_remote_genesis && !remote_is_setup_placeholder { anyhow::bail!( "wrong genesis {}; expected {local_genesis}", hello.genesis_hash @@ -1700,17 +1720,7 @@ async fn process_hello( .await; anyhow::bail!("peer announced our own p2p address {peer}"); } - if let Some(previous_peer) = known_peer.as_deref() { - network - .inner - .peers - .lock() - .await - .replace_peer_address(previous_peer, peer.clone()); - } else { - network.inner.peers.lock().await.add_peer(peer.clone()); - } - *known_peer = Some(peer.clone()); + remember_discoverable_advertised_peer(network, remote_addr, known_peer, peer).await?; } record_peer_status( network, @@ -1720,16 +1730,45 @@ async fn process_hello( hello.tip_hash.clone(), ) .await; - if force_snapshot { - Ok(PeerStatus::with_forced_snapshot( + if request_snapshot { + Ok(PeerStatus::with_snapshot_request( hello.height, hello.tip_hash, )) + } else if push_snapshot { + Ok(PeerStatus::with_snapshot_push(hello.height, hello.tip_hash)) } else { Ok(PeerStatus::new(hello.height, hello.tip_hash)) } } +fn setup_placeholder_genesis_hash() -> String { + Ledger::new(BTreeMap::new(), 1).genesis_hash().to_string() +} + +async fn remember_discoverable_advertised_peer( + network: &GossipNetwork, + remote_addr: SocketAddr, + known_peer: &mut Option<String>, + peer: String, +) -> Result<bool> { + if !advertised_peer_is_discoverable(&peer, remote_addr)? { + return Ok(false); + } + if let Some(previous_peer) = known_peer.as_deref() { + network + .inner + .peers + .lock() + .await + .replace_peer_address(previous_peer, peer.clone()); + } else { + network.inner.peers.lock().await.add_peer(peer.clone()); + } + *known_peer = Some(peer); + Ok(true) +} + async fn record_inbound_result( network: &GossipNetwork, known_peer: &Option<String>, @@ -1805,10 +1844,21 @@ fn peer_list_address_is_discoverable(address: &str, remote_addr: SocketAddr) -> let candidate = address .parse::<SocketAddr>() .with_context(|| format!("invalid peer-list address {address}"))?; + Ok(socket_addr_is_discoverable(candidate, remote_addr)) +} + +fn advertised_peer_is_discoverable(address: &str, remote_addr: SocketAddr) -> Result<bool> { + let candidate = address + .parse::<SocketAddr>() + .with_context(|| format!("invalid announced peer address {address}"))?; + Ok(socket_addr_is_discoverable(candidate, remote_addr)) +} + +fn socket_addr_is_discoverable(candidate: SocketAddr, remote_addr: SocketAddr) -> bool { if candidate.ip().is_loopback() { - return Ok(remote_addr.ip().is_loopback()); + return remote_addr.ip().is_loopback(); } - Ok(ip_is_publicly_discoverable(candidate.ip())) + ip_is_publicly_discoverable(candidate.ip()) } fn ip_is_publicly_discoverable(ip: IpAddr) -> bool { @@ -2560,7 +2610,8 @@ mod tests { .await .unwrap(); - assert!(peer_status.force_snapshot); + assert!(peer_status.request_snapshot); + assert!(!peer_status.push_snapshot); assert_eq!(known_peer.as_deref(), Some("142.132.164.59:9444")); let listed = network.inner.peers.lock().await.list(); assert_eq!(listed.len(), 1); @@ -2591,6 +2642,92 @@ mod tests { } #[tokio::test] + async fn real_node_accepts_setup_placeholder_peer_and_pushes_snapshot() { + let wallet = Wallet::from_seed("setup-placeholder-peer-real-node"); + let node = Arc::new(tokio::sync::Mutex::new(node( + "real", + wallet.clone(), + allocations(std::slice::from_ref(&wallet), 1_000), + ))); + let network = super::GossipNetwork { + inner: Arc::new(super::GossipNetworkInner { + node: Arc::clone(&node), + peers: Arc::new(tokio::sync::Mutex::new(PeerBook::default())), + listen_addr: "127.0.0.1:9544".parse().unwrap(), + sessions: tokio::sync::Mutex::new(BTreeMap::new()), + tx_delivery: tokio::sync::Mutex::new(BTreeMap::new()), + metrics: super::P2pMetricsCounters::default(), + }), + }; + let setup_ledger = Ledger::new(BTreeMap::new(), 1); + let hello = ProtocolHello { + protocol_version: PROTOCOL_VERSION, + network_id: NETWORK_ID.to_string(), + genesis_hash: setup_ledger.genesis_hash().to_string(), + listen_addr: Some("127.0.0.1:9545".to_string()), + height: 0, + tip_hash: setup_ledger.status().tip_hash, + }; + + let peer_status = super::process_hello( + &network, + "127.0.0.1:51234".parse().unwrap(), + &mut None, + hello, + ) + .await + .unwrap(); + + assert!(!peer_status.request_snapshot); + assert!(peer_status.push_snapshot); + let payload = super::catchup_payload_for_peer(&node, &peer_status).await; + assert!(matches!( + payload.as_slice(), + [GossipEnvelope::ChainSnapshot(_)] + )); + } + + #[tokio::test] + async fn hello_ignores_private_advertised_listen_address() { + let alice = Wallet::from_seed("hello-private-listen-alice"); + let allocations = allocations(std::slice::from_ref(&alice), 1_000); + let node = Arc::new(tokio::sync::Mutex::new(node("alice", alice, allocations))); + let peers = Arc::new(tokio::sync::Mutex::new(PeerBook::default())); + let network = super::GossipNetwork { + inner: Arc::new(super::GossipNetworkInner { + node: Arc::clone(&node), + peers: Arc::clone(&peers), + listen_addr: "0.0.0.0:9444".parse().unwrap(), + sessions: tokio::sync::Mutex::new(BTreeMap::new()), + tx_delivery: tokio::sync::Mutex::new(BTreeMap::new()), + metrics: super::P2pMetricsCounters::default(), + }), + }; + let status = node.lock().await.ledger().status(); + let hello = ProtocolHello { + protocol_version: PROTOCOL_VERSION, + network_id: NETWORK_ID.to_string(), + genesis_hash: node.lock().await.ledger().genesis_hash().to_string(), + listen_addr: Some("10.42.1.1:12138".to_string()), + height: status.height, + tip_hash: status.tip_hash, + }; + + let mut known_peer = None; + super::process_hello( + &network, + "142.132.164.59:51234".parse().unwrap(), + &mut known_peer, + hello, + ) + .await + .unwrap(); + + assert!(known_peer.is_none()); + assert!(peers.lock().await.addresses().is_empty()); + } + + #[tokio::test] async fn inbound_status_does_not_create_outbound_ephemeral_peer() { let alice = Wallet::from_seed("inbound-status-alice"); let allocations = allocations(std::slice::from_ref(&alice), 1_000); @@ -2745,6 +2882,38 @@ mod tests { } #[tokio::test] + async fn peer_announcement_ignores_private_ephemeral_address() { + let alice = Wallet::from_seed("px-private-announcement-alice"); + let allocations = allocations(std::slice::from_ref(&alice), 1_000); + let node = Arc::new(tokio::sync::Mutex::new(node("alice", alice, allocations))); + let peers = Arc::new(tokio::sync::Mutex::new(PeerBook::default())); + let network = super::GossipNetwork { + inner: Arc::new(super::GossipNetworkInner { + node, + peers: Arc::clone(&peers), + listen_addr: "0.0.0.0:9444".parse().unwrap(), + sessions: tokio::sync::Mutex::new(BTreeMap::new()), + tx_delivery: tokio::sync::Mutex::new(BTreeMap::new()), + metrics: super::P2pMetricsCounters::default(), + }), + }; + let mut known_peer = None; + + let remembered = super::remember_discoverable_advertised_peer( + &network, + "142.132.164.59:51234".parse().unwrap(), + &mut known_peer, + "10.42.1.1:10091".to_string(), + ) + .await + .unwrap(); + + assert!(!remembered); + assert!(known_peer.is_none()); + assert!(peers.lock().await.addresses().is_empty()); + } + + #[tokio::test] async fn peer_list_ignores_loopback_alias_for_unspecified_self() { let alice = Wallet::from_seed("px-self-alias-alice"); let allocations = allocations(std::slice::from_ref(&alice), 1_000);