commit 6bc7084835e9319af8f8715e844611f00f9f1e9e
parent 3ac9f5b5b76641402723899066abe7d4cae3556e
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Tue, 4 Aug 2026 12:53:37 +0200
Release v0.2.16
Diffstat:
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);