From e3b5e6ca0d399b55183f848b46dff8492e384c61 Mon Sep 17 00:00:00 2001 From: Avi Cohen Date: Sun, 7 Jun 2026 14:26:12 +0300 Subject: [PATCH] starknet_transaction_prover: add request body size limit tests --- .../starknet_transaction_prover/src/server.rs | 3 + .../src/server/config.rs | 2 +- .../src/server/request_body_size_test.rs | 105 ++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 crates/starknet_transaction_prover/src/server/request_body_size_test.rs diff --git a/crates/starknet_transaction_prover/src/server.rs b/crates/starknet_transaction_prover/src/server.rs index 4732102e68d..8ea87982e18 100644 --- a/crates/starknet_transaction_prover/src/server.rs +++ b/crates/starknet_transaction_prover/src/server.rs @@ -42,6 +42,9 @@ pub use health::{HealthLayer, HEALTH_PATH}; #[cfg(test)] mod rpc_spec_test; +#[cfg(test)] +mod request_body_size_test; + #[cfg(test)] #[path = "server/ohttp_integration_test.rs"] mod ohttp_integration_test; diff --git a/crates/starknet_transaction_prover/src/server/config.rs b/crates/starknet_transaction_prover/src/server/config.rs index 403c97a750d..034177e8481 100644 --- a/crates/starknet_transaction_prover/src/server/config.rs +++ b/crates/starknet_transaction_prover/src/server/config.rs @@ -29,7 +29,7 @@ const DEFAULT_MAX_CONCURRENT_REQUESTS: usize = 2; const DEFAULT_MAX_CONNECTIONS: u32 = 10; const DEFAULT_COMPILED_CLASS_CACHE_SIZE: usize = 600; /// 5 MiB — matches the convention used elsewhere in the sequencer. -const DEFAULT_MAX_REQUEST_BODY_SIZE: u32 = 5 * 1024 * 1024; +pub(crate) const DEFAULT_MAX_REQUEST_BODY_SIZE: u32 = 5 * 1024 * 1024; const DEFAULT_OHTTP_KEY_CACHE_MAX_AGE_SECS: u64 = 3600; /// Transport mode for the JSON-RPC server. diff --git a/crates/starknet_transaction_prover/src/server/request_body_size_test.rs b/crates/starknet_transaction_prover/src/server/request_body_size_test.rs new file mode 100644 index 00000000000..16b38054119 --- /dev/null +++ b/crates/starknet_transaction_prover/src/server/request_body_size_test.rs @@ -0,0 +1,105 @@ +//! Integration tests for the HTTP server's `max_request_body_size` enforcement. +//! +//! Verifies that the production default limit accepts realistically large +//! `starknet_proveTransaction` requests (5,000 full-width calldata felts) and that +//! jsonrpsee rejects bodies exceeding the configured limit with `413 Payload Too Large`. + +use std::net::SocketAddr; +use std::sync::Arc; + +use blockifier_reexecution::state_reader::rpc_objects::BlockId; +use jsonrpsee::server::ServerHandle; +use reqwest::StatusCode; +use rstest::rstest; +use serde_json::{json, Value}; +use starknet_api::invoke_tx_args; +use starknet_api::test_utils::invoke::rpc_invoke_tx; +use starknet_api::transaction::fields::Calldata; +use starknet_types_core::felt::Felt; + +use crate::server::config::{TransportMode, DEFAULT_MAX_REQUEST_BODY_SIZE}; +use crate::server::mock_rpc::MockProvingRpc; +use crate::server::rpc_api::ProvingRpcServer; +use crate::server::start_server; + +const NUM_CALLDATA_FELTS: usize = 5_000; + +async fn start_test_http_server(max_request_body_size: u32) -> (SocketAddr, ServerHandle) { + let methods = MockProvingRpc::from_expected_json().into_rpc(); + let addr: SocketAddr = "127.0.0.1:0".parse().unwrap(); + start_server(addr, &TransportMode::Http, methods.into(), 10, max_request_body_size, None, None) + .await + .expect("Failed to start HTTP server") +} + +/// A `starknet_proveTransaction` request whose invoke transaction carries `num_felts` +/// calldata felts. Each felt is `Felt::MAX` so it serializes at full hex width, +/// making the body the largest JSON encoding a calldata of that length can produce. +fn prove_transaction_request(num_felts: usize) -> Value { + let transaction = rpc_invoke_tx(invoke_tx_args!( + calldata: Calldata(Arc::new(vec![Felt::MAX; num_felts])) + )); + json!({ + "jsonrpc": "2.0", + "id": "1", + "method": "starknet_proveTransaction", + "params": { "block_id": BlockId::Latest, "transaction": transaction } + }) +} + +async fn post_json_body(addr: SocketAddr, body: String) -> reqwest::Response { + reqwest::Client::new() + .post(format!("http://{addr}")) + .header("content-type", "application/json") + .body(body) + .send() + .await + .expect("HTTP request failed") +} + +#[tokio::test] +async fn test_default_limit_accepts_5000_felt_calldata() { + let (addr, handle) = start_test_http_server(DEFAULT_MAX_REQUEST_BODY_SIZE).await; + + let request_body = prove_transaction_request(NUM_CALLDATA_FELTS).to_string(); + // Guard that the request is genuinely large (~350 KiB) yet within the default limit, + // so the assertions below keep their meaning if the encoding ever changes. + assert!( + request_body.len() > 300 * 1024, + "expected a large request body, got {} bytes", + request_body.len() + ); + assert!(request_body.len() < usize::try_from(DEFAULT_MAX_REQUEST_BODY_SIZE).unwrap()); + + let response = post_json_body(addr, request_body).await; + assert_eq!(response.status(), StatusCode::OK); + let response_json: Value = response.json().await.unwrap(); + assert!( + response_json.get("error").is_none() && response_json.get("result").is_some(), + "expected a JSON-RPC success, got: {response_json}" + ); + + handle.stop().unwrap(); +} + +/// `max_request_body_size` is inclusive: a body of exactly the configured size is +/// served, one byte more is rejected with HTTP 413. The body is padded with trailing +/// spaces (valid JSON whitespace) to hit exact byte counts. +#[rstest] +#[case::body_at_limit(0, StatusCode::OK)] +#[case::body_one_byte_over_limit(1, StatusCode::PAYLOAD_TOO_LARGE)] +#[tokio::test] +async fn test_body_size_limit_boundary( + #[case] num_bytes_over_limit: usize, + #[case] expected_status: StatusCode, +) { + let unpadded_body = prove_transaction_request(NUM_CALLDATA_FELTS).to_string(); + let body_size_limit = u32::try_from(unpadded_body.len()).unwrap(); + let (addr, handle) = start_test_http_server(body_size_limit).await; + + let padded_body = format!("{}{}", unpadded_body, " ".repeat(num_bytes_over_limit)); + let response = post_json_body(addr, padded_body).await; + assert_eq!(response.status(), expected_status); + + handle.stop().unwrap(); +}