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:
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"));
+ }
}