iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

commit c4e72b586405c929cb29937fdc6e7944abd4b9ca
parent ed0a98d9235fca4531ab45477c7a71177e39e353
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Wed, 19 Aug 2026 21:21:22 +0200

Add P2P and Stratum performance budget tests

Diffstat:
MPLAN.md | 5+++--
Msrc/adapters/p2p/line_codec.rs | 49++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/adapters/stratum.rs | 27+++++++++++++++++++++++++--
3 files changed, 74 insertions(+), 7 deletions(-)

diff --git a/PLAN.md b/PLAN.md @@ -86,8 +86,9 @@ Metrics: ## 4. Performance Budgets Status: started with deterministic tests for consensus block count/byte caps, -burn bundle 10kB selection, snapshot replay, and blockspace-flood bounded -mempool selection. +burn bundle 10kB selection, snapshot replay, blockspace-flood bounded mempool +selection, P2P batch parsing, P2P line-size enforcement, and Stratum request +line-size enforcement. Goal: make valid input DoS visible before launch. diff --git a/src/adapters/p2p/line_codec.rs b/src/adapters/p2p/line_codec.rs @@ -206,6 +206,8 @@ fn ensure_len(label: &str, len: usize, max: usize) -> Result<()> { #[cfg(test)] mod tests { + use tokio::io::AsyncWriteExt; + use crate::{ adapters::p2p::metrics::P2pMetricsCounters, app::{BlockInventory, GossipEnvelope, TRANSACTION_BATCH_LIMIT}, @@ -216,9 +218,9 @@ mod tests { }; use super::{ - MAX_BLOCK_BATCH, MAX_INVENTORY_ITEMS, MAX_OBJECT_REQUESTS, MAX_PEER_LIST, - MAX_SNAPSHOT_BLOCKS, parse_envelope, record_received_envelope_kind, - validate_envelope_limits, + LimitedLineReader, MAX_BLOCK_BATCH, MAX_GOSSIP_LINE_BYTES, MAX_INVENTORY_ITEMS, + MAX_OBJECT_REQUESTS, MAX_PEER_LIST, MAX_SNAPSHOT_BLOCKS, parse_envelope, + record_received_envelope_kind, validate_envelope_limits, }; fn burn(signature: &str) -> Transaction { @@ -431,4 +433,45 @@ mod tests { .is_err() ); } + + #[tokio::test] + async fn performance_budget_p2p_line_reader_enforces_message_size() { + let (mut client, server) = tokio::io::duplex(MAX_GOSSIP_LINE_BYTES + 1); + let mut reader = LimitedLineReader::new(server); + let line = vec![b'a'; MAX_GOSSIP_LINE_BYTES]; + client.write_all(&line).await.unwrap(); + client.write_all(b"\n").await.unwrap(); + + let read = reader.read_line().await.unwrap().unwrap(); + + assert_eq!(read.len(), MAX_GOSSIP_LINE_BYTES); + + let (mut client, server) = tokio::io::duplex(MAX_GOSSIP_LINE_BYTES + 2); + let mut reader = LimitedLineReader::new(server); + let line = vec![b'a'; MAX_GOSSIP_LINE_BYTES + 1]; + client.write_all(&line).await.unwrap(); + client.write_all(b"\n").await.unwrap(); + + let error = reader.read_line().await.unwrap_err(); + + assert!(error.to_string().contains("p2p message exceeds")); + } + + #[test] + fn performance_budget_p2p_batch_parser_enforces_item_limits() { + let at_budget = GossipEnvelope::PeerList { + peers: vec!["127.0.0.1:9444".to_string(); MAX_PEER_LIST], + }; + let line = serde_json::to_string(&at_budget).unwrap(); + + assert_eq!(parse_envelope(&line).unwrap(), at_budget); + + let over_budget = GossipEnvelope::PeerList { + peers: vec!["127.0.0.1:9444".to_string(); MAX_PEER_LIST + 1], + }; + let line = serde_json::to_string(&over_budget).unwrap(); + let error = parse_envelope(&line).unwrap_err(); + + assert!(error.to_string().contains("peer list has")); + } } diff --git a/src/adapters/stratum.rs b/src/adapters/stratum.rs @@ -30,6 +30,10 @@ const STRATUM_IDLE_TIMEOUT: Duration = Duration::from_secs(120); #[cfg(feature = "fuzzing")] pub fn fuzz_parse_stratum_request(line: &str) -> Result<Value> { + parse_stratum_request(line) +} + +fn parse_stratum_request(line: &str) -> Result<Value> { if line.len() > STRATUM_MAX_LINE_BYTES { bail!("Stratum request exceeds {STRATUM_MAX_LINE_BYTES} byte limit"); } @@ -141,7 +145,7 @@ async fn handle_connection(server: StratumServer, stream: TcpStream) -> Result<( if line.trim().is_empty() { continue; } - let request: Value = serde_json::from_str(&line).context("invalid Stratum JSON")?; + let request = parse_stratum_request(&line)?; session.handle_request(request).await?; } Ok(()) @@ -443,7 +447,7 @@ mod tests { use super::{ STRATUM_MAX_JOBS_PER_SESSION, STRATUM_MAX_LINE_BYTES, STRATUM_MAX_SESSIONS, StratumJob, - StratumLineReader, StratumSessionLimiter, insert_bounded_job, + StratumLineReader, StratumSessionLimiter, insert_bounded_job, parse_stratum_request, }; fn dummy_job() -> StratumJob { @@ -514,4 +518,23 @@ mod tests { drop(permits); assert!(limiter.try_acquire().is_some()); } + + #[test] + fn performance_budget_stratum_request_parser_enforces_line_size() { + let prefix = r#"{"id":1,"method":"mining.configure","params":[""#; + let suffix = r#""]}"#; + let fill_len = STRATUM_MAX_LINE_BYTES - prefix.len() - suffix.len(); + let at_budget = format!("{prefix}{}{suffix}", "a".repeat(fill_len)); + + let parsed = + parse_stratum_request(&at_budget).expect("request at line budget should parse"); + assert_eq!( + parsed.get("method").and_then(serde_json::Value::as_str), + Some("mining.configure") + ); + + let over_budget = format!("{prefix}{}{suffix}", "a".repeat(fill_len + 1)); + let error = parse_stratum_request(&over_budget).unwrap_err(); + assert!(error.to_string().contains("Stratum request exceeds")); + } }