iuna

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

commit 6bc7084835e9319af8f8715e844611f00f9f1e9e
parent 3ac9f5b5b76641402723899066abe7d4cae3556e
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Tue,  4 Aug 2026 12:53:37 +0200

Release v0.2.16

Diffstat:
MCargo.lock | 2+-
MCargo.toml | 2+-
Msrc/adapters/http.rs | 62+++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/adapters/p2p.rs | 134++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 177 insertions(+), 23 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -537,7 +537,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "iuna" -version = "0.2.15" +version = "0.2.16" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iuna" -version = "0.2.15" +version = "0.2.16" edition = "2024" license = "Apache-2.0" diff --git a/src/adapters/http.rs b/src/adapters/http.rs @@ -121,11 +121,21 @@ struct NetworkHealthResponse { stale_peers: usize, banned_peers: usize, pending_transactions: usize, + pending_plain_transactions: usize, + pending_blinded_transactions: usize, + pending_blinded_reveals: usize, network_time_offset_ms: Option<i64>, bad_clock_peers: usize, last_error: Option<String>, } +#[derive(Clone, Copy, Debug, Default)] +struct MempoolCounts { + plain_transactions: usize, + blinded_transactions: usize, + blinded_reveals: usize, +} + #[derive(Debug, Deserialize)] struct BurnSettingsForm { enabled: Option<bool>, @@ -894,9 +904,19 @@ async fn api_metrics(State(state): State<HttpState>) -> Json<MetricsResponse> { } async fn api_network_health(State(state): State<HttpState>) -> Json<NetworkHealthResponse> { - let status = state.node.lock().await.status(); + let (status, mempool) = { + let node = state.node.lock().await; + ( + node.status(), + MempoolCounts { + plain_transactions: node.pending_transactions().len(), + blinded_transactions: node.pending_blinded_transactions().len(), + blinded_reveals: node.pending_blinded_reveals().len(), + }, + ) + }; let peers = state.peers.lock().await.list(); - Json(network_health(&status, &peers)) + Json(network_health(&status, &peers, mempool)) } async fn api_config_form( @@ -1233,8 +1253,12 @@ fn validate_peer_address(peer: String) -> Result<String> { Ok(peer) } -fn network_health(status: &NodeStatus, peers: &[PeerInfo]) -> NetworkHealthResponse { - network_health_at(status, peers, now_ms()) +fn network_health( + status: &NodeStatus, + peers: &[PeerInfo], + mempool: MempoolCounts, +) -> NetworkHealthResponse { + network_health_at(status, peers, mempool, now_ms()) } fn metrics_response(enabled: bool, rows: Vec<BlockMetricRow>) -> MetricsResponse { @@ -1363,6 +1387,7 @@ fn micro_iuna_as_iuna(amount: Amount) -> f64 { fn network_health_at( status: &NodeStatus, peers: &[PeerInfo], + mempool: MempoolCounts, now_ms: u64, ) -> NetworkHealthResponse { let local_height = status.chain.height; @@ -1455,6 +1480,9 @@ fn network_health_at( stale_peers, banned_peers, pending_transactions: status.chain.pending_transactions, + pending_plain_transactions: mempool.plain_transactions, + pending_blinded_transactions: mempool.blinded_transactions, + pending_blinded_reveals: mempool.blinded_reveals, network_time_offset_ms, bad_clock_peers, last_error, @@ -3200,9 +3228,9 @@ const INDEX_HTML: &str = r#"<!doctype html> <div class="peer-summary-item"><div class="peer-summary-label">Stale</div><div class="peer-summary-value" x-text="networkHealth.stale_peers ?? '-'"></div></div> <div class="peer-summary-item"><div class="peer-summary-label">Banned</div><div class="peer-summary-value" x-text="networkHealth.banned_peers ?? '-'"></div></div> <div class="peer-summary-item"><div class="peer-summary-label">Mempool</div><div class="peer-summary-value" x-text="networkHealth.pending_transactions ?? '-'"></div></div> - <div class="peer-summary-item"><div class="peer-summary-label">Peer Mempools</div><div class="peer-summary-value" x-text="networkHealth.mempool_known_peers ?? '-'"></div></div> - <div class="peer-summary-item"><div class="peer-summary-label">Divergent</div><div class="peer-summary-value" x-text="networkHealth.mempool_divergent_peers ?? '-'"></div></div> - <div class="peer-summary-item"><div class="peer-summary-label">Missing Tx</div><div class="peer-summary-value" x-text="networkHealth.mempool_missing_transactions ?? '-'"></div></div> + <div class="peer-summary-item"><div class="peer-summary-label">Plain Tx</div><div class="peer-summary-value" x-text="networkHealth.pending_plain_transactions ?? '-'"></div></div> + <div class="peer-summary-item"><div class="peer-summary-label">Commits</div><div class="peer-summary-value" x-text="networkHealth.pending_blinded_transactions ?? '-'"></div></div> + <div class="peer-summary-item"><div class="peer-summary-label">Reveals</div><div class="peer-summary-value" x-text="networkHealth.pending_blinded_reveals ?? '-'"></div></div> <div class="peer-summary-item"><div class="peer-summary-label">Time Offset</div><div class="peer-summary-value" x-text="networkTimeOffsetLabel()"></div></div> <div class="peer-summary-item"><div class="peer-summary-label">Clock Warnings</div><div class="peer-summary-value" x-text="networkHealth.bad_clock_peers ?? '-'"></div></div> </div> @@ -3266,6 +3294,10 @@ const INDEX_HTML: &str = r#"<!doctype html> <div class="metric"><div class="label">Hello Rx</div><div class="value" x-text="p2pMetrics.hello_envelopes_received ?? 0"></div></div> <div class="metric"><div class="label">Inventory Rx</div><div class="value" x-text="p2pMetrics.inventory_envelopes_received ?? 0"></div></div> <div class="metric"><div class="label">Data Rx</div><div class="value" x-text="p2pMetrics.data_envelopes_received ?? 0"></div></div> + <div class="metric"><div class="label">Commit Rx</div><div class="value" x-text="p2pMetrics.blinded_transactions_received ?? 0"></div></div> + <div class="metric"><div class="label">Commit Batches Rx</div><div class="value" x-text="p2pMetrics.blinded_transaction_envelopes_received ?? 0"></div></div> + <div class="metric"><div class="label">Reveal Rx</div><div class="value" x-text="p2pMetrics.blinded_reveals_received ?? 0"></div></div> + <div class="metric"><div class="label">Reveal Batches Rx</div><div class="value" x-text="p2pMetrics.blinded_reveal_envelopes_received ?? 0"></div></div> <div class="metric"><div class="label">Control Rx</div><div class="value" x-text="p2pMetrics.control_envelopes_received ?? 0"></div></div> </div> <div class="metric-context"> @@ -4343,12 +4375,20 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let state = auth_test_state(dir.path().join("config.json"), UiConfig::default()).await; let status = state.node.lock().await.status(); + let mempool = super::MempoolCounts { + plain_transactions: 1, + blinded_transactions: 2, + blinded_reveals: 3, + }; - let isolated = super::network_health(&status, &[]); + let isolated = super::network_health(&status, &[], mempool); assert!(!isolated.ok); assert_eq!(isolated.state, "isolated"); assert_eq!(isolated.local_height, 0); assert_eq!(isolated.best_known_height, 0); + assert_eq!(isolated.pending_plain_transactions, 1); + assert_eq!(isolated.pending_blinded_transactions, 2); + assert_eq!(isolated.pending_blinded_reveals, 3); let mut clock_peers = PeerBook::from_addresses(vec![ "127.0.0.1:9450".to_string(), @@ -4368,7 +4408,7 @@ mod tests { 11 * 60 * 1_000, 10_000, ); - let clock_health = super::network_health_at(&status, &clock_peers.list(), 10_000); + let clock_health = super::network_health_at(&status, &clock_peers.list(), mempool, 10_000); assert_eq!(clock_health.network_time_offset_ms, Some(500)); assert_eq!(clock_health.bad_clock_peers, 1); @@ -4392,6 +4432,7 @@ mod tests { banned_until_ms: None, ban_reason: None, }], + mempool, ); assert!(!syncing.ok); assert_eq!(syncing.state, "syncing"); @@ -4418,6 +4459,7 @@ mod tests { banned_until_ms: None, ban_reason: Some("connection refused".to_string()), }], + mempool, ); assert!(!peer_errors.ok); assert_eq!(peer_errors.state, "peer errors"); @@ -4446,6 +4488,7 @@ mod tests { banned_until_ms: None, ban_reason: None, }], + mempool, PEER_STALE_AFTER_MS + 2, ); assert!(!stale.ok); @@ -4472,6 +4515,7 @@ mod tests { banned_until_ms: Some(1_000), ban_reason: Some("invalid block".to_string()), }], + mempool, 20, ); assert!(!banned.ok); diff --git a/src/adapters/p2p.rs b/src/adapters/p2p.rs @@ -243,6 +243,10 @@ struct P2pMetricsCounters { peer_status_envelopes_received: AtomicU64, inventory_envelopes_received: AtomicU64, data_envelopes_received: AtomicU64, + blinded_transaction_envelopes_received: AtomicU64, + blinded_transactions_received: AtomicU64, + blinded_reveal_envelopes_received: AtomicU64, + blinded_reveals_received: AtomicU64, control_envelopes_received: AtomicU64, bytes_received: AtomicU64, parse_errors: AtomicU64, @@ -272,6 +276,10 @@ pub struct P2pMetrics { pub peer_status_envelopes_received: u64, pub inventory_envelopes_received: u64, pub data_envelopes_received: u64, + pub blinded_transaction_envelopes_received: u64, + pub blinded_transactions_received: u64, + pub blinded_reveal_envelopes_received: u64, + pub blinded_reveals_received: u64, pub control_envelopes_received: u64, pub bytes_received: u64, pub parse_errors: u64, @@ -318,6 +326,16 @@ impl P2pMetricsCounters { .load(Ordering::Relaxed), inventory_envelopes_received: self.inventory_envelopes_received.load(Ordering::Relaxed), data_envelopes_received: self.data_envelopes_received.load(Ordering::Relaxed), + blinded_transaction_envelopes_received: self + .blinded_transaction_envelopes_received + .load(Ordering::Relaxed), + blinded_transactions_received: self + .blinded_transactions_received + .load(Ordering::Relaxed), + blinded_reveal_envelopes_received: self + .blinded_reveal_envelopes_received + .load(Ordering::Relaxed), + blinded_reveals_received: self.blinded_reveals_received.load(Ordering::Relaxed), control_envelopes_received: self.control_envelopes_received.load(Ordering::Relaxed), bytes_received: self.bytes_received.load(Ordering::Relaxed), parse_errors: self.parse_errors.load(Ordering::Relaxed), @@ -1210,22 +1228,29 @@ async fn catchup_payload_for_peer( if node.ledger().is_setup_placeholder() { return Vec::new(); } + let mempool = node.mempool_gossip(); if peer_status.push_snapshot { - return vec![GossipEnvelope::ChainSnapshot(node.chain_snapshot())]; + let mut payload = vec![GossipEnvelope::ChainSnapshot(node.chain_snapshot())]; + payload.extend(mempool); + return payload; } if peer_status.height < local_status.height { let blocks = node.blocks_from(peer_status.height + 1, MAX_BLOCK_BATCH); if blocks.is_empty() { - Vec::new() + mempool } else { - vec![GossipEnvelope::Blocks { blocks }] + let mut payload = vec![GossipEnvelope::Blocks { blocks }]; + payload.extend(mempool); + payload } } else if peer_status.height == local_status.height && peer_status.tip_hash != local_status.tip_hash { - vec![GossipEnvelope::ChainSnapshot(node.chain_snapshot())] + let mut payload = vec![GossipEnvelope::ChainSnapshot(node.chain_snapshot())]; + payload.extend(mempool); + payload } else { - Vec::new() + mempool } } @@ -1372,11 +1397,30 @@ fn record_received_envelope_kind(metrics: &P2pMetricsCounters, envelope: &Gossip GossipEnvelope::Inventory { .. } => { P2pMetricsCounters::inc(&metrics.inventory_envelopes_received); } - GossipEnvelope::BlindedTransaction(_) - | GossipEnvelope::BlindedTransactions { .. } - | GossipEnvelope::BlindedReveal(_) - | GossipEnvelope::BlindedReveals { .. } - | GossipEnvelope::Block(_) + GossipEnvelope::BlindedTransaction(_) => { + P2pMetricsCounters::inc(&metrics.data_envelopes_received); + P2pMetricsCounters::inc(&metrics.blinded_transaction_envelopes_received); + P2pMetricsCounters::inc(&metrics.blinded_transactions_received); + } + GossipEnvelope::BlindedTransactions { transactions } => { + P2pMetricsCounters::inc(&metrics.data_envelopes_received); + P2pMetricsCounters::inc(&metrics.blinded_transaction_envelopes_received); + P2pMetricsCounters::add( + &metrics.blinded_transactions_received, + transactions.len() as u64, + ); + } + GossipEnvelope::BlindedReveal(_) => { + P2pMetricsCounters::inc(&metrics.data_envelopes_received); + P2pMetricsCounters::inc(&metrics.blinded_reveal_envelopes_received); + P2pMetricsCounters::inc(&metrics.blinded_reveals_received); + } + GossipEnvelope::BlindedReveals { reveals } => { + P2pMetricsCounters::inc(&metrics.data_envelopes_received); + P2pMetricsCounters::inc(&metrics.blinded_reveal_envelopes_received); + P2pMetricsCounters::add(&metrics.blinded_reveals_received, reveals.len() as u64); + } + GossipEnvelope::Block(_) | GossipEnvelope::Blocks { .. } | GossipEnvelope::ChainSnapshot(_) => { P2pMetricsCounters::inc(&metrics.data_envelopes_received); @@ -2433,7 +2477,9 @@ mod tests { BlockInventory, GossipEnvelope, NETWORK_ID, NodeCore, PROTOCOL_VERSION, PeerBook, PeerDirection, ProtocolHello, }, - domain::{Amount, GenesisBurn, Ledger, Transaction, Wallet}, + domain::{ + Amount, BlindedReveal, BlindedTransaction, GenesisBurn, Ledger, Transaction, Wallet, + }, }; use tokio::io::AsyncWriteExt; @@ -2547,6 +2593,19 @@ mod tests { #[test] fn received_envelope_metrics_are_categorized() { let metrics = super::P2pMetricsCounters::default(); + let blinded_tx = BlindedTransaction { + commitment: "commitment".to_string(), + fee: 3, + encrypted_size: 128, + expires_at_height: 20, + nonce: "nonce".to_string(), + ciphertext: "ciphertext".to_string(), + payload_hash: "payload-hash".to_string(), + }; + let blinded_reveal = BlindedReveal { + commitment: "commitment".to_string(), + key: "key".to_string(), + }; super::record_received_envelope_kind( &metrics, @@ -2564,12 +2623,26 @@ mod tests { &metrics, &GossipEnvelope::Blocks { blocks: Vec::new() }, ); + super::record_received_envelope_kind( + &metrics, + &GossipEnvelope::BlindedTransactions { + transactions: vec![blinded_tx.clone(), blinded_tx], + }, + ); + super::record_received_envelope_kind( + &metrics, + &GossipEnvelope::BlindedReveal(blinded_reveal), + ); super::record_received_envelope_kind(&metrics, &GossipEnvelope::ChainSnapshotRequest); let snapshot = metrics.snapshot(); assert_eq!(snapshot.peer_status_envelopes_received, 1); assert_eq!(snapshot.inventory_envelopes_received, 1); - assert_eq!(snapshot.data_envelopes_received, 1); + assert_eq!(snapshot.data_envelopes_received, 3); + assert_eq!(snapshot.blinded_transaction_envelopes_received, 1); + assert_eq!(snapshot.blinded_transactions_received, 2); + assert_eq!(snapshot.blinded_reveal_envelopes_received, 1); + assert_eq!(snapshot.blinded_reveals_received, 1); assert_eq!(snapshot.control_envelopes_received, 1); } @@ -2832,6 +2905,43 @@ mod tests { } #[tokio::test] + async fn session_catchup_payload_pushes_blinded_mempool_to_synced_peer() { + let alice = Wallet::from_seed("catchup-mempool-alice"); + let bob = Wallet::from_seed("catchup-mempool-bob"); + let allocations = allocations(&[alice.clone(), bob], 1_000); + let node = Arc::new(tokio::sync::Mutex::new(node( + "alice", + alice.clone(), + allocations, + ))); + let expected_commitment = { + let mut node = node.lock().await; + let tx = node.ledger().build_burn(&alice, 1, 0).unwrap(); + let built = node.ledger().build_blinded_transaction(tx, 20).unwrap(); + let commitment = built.transaction.commitment.clone(); + node.receive_blinded_transaction(built.transaction).unwrap(); + node.drain_outbox(); + commitment + }; + let peer_status = { + let node = node.lock().await; + let status = node.ledger().status(); + super::PeerStatus::new(status.height, status.tip_hash) + }; + + let payload = super::catchup_payload_for_peer(&node, &peer_status).await; + + assert_eq!(payload.len(), 1); + match &payload[0] { + GossipEnvelope::BlindedTransactions { transactions } => { + assert_eq!(transactions.len(), 1); + assert_eq!(transactions[0].commitment, expected_commitment); + } + other => panic!("expected blinded mempool payload, got {other:?}"), + } + } + + #[tokio::test] async fn hello_rejects_wrong_network_or_genesis_without_banning() { let alice = Wallet::from_seed("hello-alice"); let allocations = allocations(std::slice::from_ref(&alice), 1_000);