Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions examples/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 0 additions & 19 deletions linera-base/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,25 +169,6 @@ impl Response {
}
}

#[cfg(with_reqwest)]
impl Response {
/// Creates a [`Response`] from a [`reqwest::Response`], waiting for it to be fully
/// received.
pub async fn from_reqwest(response: reqwest::Response) -> reqwest::Result<Self> {
let headers = response
.headers()
.into_iter()
.map(|(name, value)| Header::new(name.to_string(), value.as_bytes()))
.collect();

Ok(Response {
status: response.status().as_u16(),
headers,
body: response.bytes().await?.to_vec(),
})
}
}

/// A header for an HTTP request or response.
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, WitLoad, WitStore, WitType)]
#[witty(name = "http-header")]
Expand Down
2 changes: 1 addition & 1 deletion linera-chain/src/unit_tests/chain_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ async fn test_block_size_limit() {
let mut chain = ChainStateView::new(chain_id).await;

// The size of the executed valid block below.
let maximum_executed_block_size = 742;
let maximum_executed_block_size = 750;

// Initialize the chain.
let mut config = make_open_chain_config();
Expand Down
2 changes: 1 addition & 1 deletion linera-execution/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ lru.workspace = true
oneshot.workspace = true
prometheus = { workspace = true, optional = true }
proptest = { workspace = true, optional = true }
reqwest = { workspace = true, features = ["blocking", "json"] }
reqwest = { workspace = true, features = ["blocking", "json", "stream"] }
revm = { workspace = true, optional = true, features = ["serde"] }
revm-primitives = { workspace = true, optional = true }
serde.workspace = true
Expand Down
74 changes: 72 additions & 2 deletions linera-execution/src/execution_state_actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::sync::LazyLock;
use std::time::Duration;

use custom_debug_derive::Debug;
use futures::channel::mpsc;
use futures::{channel::mpsc, StreamExt as _};
#[cfg(with_metrics)]
use linera_base::prometheus_util::{
exponential_bucket_latencies, register_histogram_vec, MeasureLatency as _,
Expand Down Expand Up @@ -406,7 +406,12 @@ where

let response = request.send().await?;

callback.respond(http::Response::from_reqwest(response).await?);
let response_size_limit = committee.policy().maximum_http_response_bytes;

callback.respond(
self.receive_http_response(response, response_size_limit)
.await?,
);
}

ReadBlobContent { blob_id, callback } => {
Expand All @@ -430,6 +435,71 @@ where
}
}

impl<C> ExecutionStateView<C>
where
C: Context + Clone + Send + Sync + 'static,
C::Extra: ExecutionRuntimeContext,
{
/// Receives an HTTP response, returning the prepared [`http::Response`] instance.
///
/// Ensures that the response does not exceed the provided `size_limit`.
async fn receive_http_response(
&mut self,
response: reqwest::Response,
size_limit: u64,
) -> Result<http::Response, ExecutionError> {
let status = response.status().as_u16();
let maybe_content_length = response.content_length();

let headers = response
.headers()
.iter()
.map(|(name, value)| http::Header::new(name.to_string(), value.as_bytes()))
.collect::<Vec<_>>();

let total_header_size = headers
.iter()
.map(|header| (header.name.as_bytes().len() + header.value.len()) as u64)
.sum();
Comment on lines +460 to +463
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder - could we collect the sizes when we map response.headers() already? we have the data there (name, value, etc.).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, but I think doing it separately is cleaner.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but not faster :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Premature optimization is the root of all evil ;)


let mut remaining_bytes = size_limit.checked_sub(total_header_size).ok_or(
ExecutionError::HttpResponseSizeLimitExceeded {
limit: size_limit,
size: total_header_size,
},
)?;

if let Some(content_length) = maybe_content_length {
if content_length > remaining_bytes {
return Err(ExecutionError::HttpResponseSizeLimitExceeded {
limit: size_limit,
size: content_length + total_header_size,
});
}
}

let mut body = Vec::with_capacity(maybe_content_length.unwrap_or(0) as usize);
let mut body_stream = response.bytes_stream();

while let Some(bytes) = body_stream.next().await.transpose()? {
remaining_bytes = remaining_bytes.checked_sub(bytes.len() as u64).ok_or(
ExecutionError::HttpResponseSizeLimitExceeded {
limit: size_limit,
size: bytes.len() as u64 + (size_limit - remaining_bytes),
},
)?;

body.extend(&bytes);
}

Ok(http::Response {
status,
headers,
body,
})
}
}

/// Requests to the execution state.
#[derive(Debug)]
pub enum ExecutionRequest {
Expand Down
2 changes: 2 additions & 0 deletions linera-execution/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ pub enum ExecutionError {
MaximumServiceOracleExecutionTimeExceeded,
#[error("Serialized size of the executed block exceeds limit")]
ExecutedBlockTooLarge,
#[error("HTTP response exceeds the size limit of {limit} bytes, having at least {size} bytes")]
HttpResponseSizeLimitExceeded { limit: u64, size: u64 },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make this a limit for all types of oracle responses?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right, I'll open an issue to limit the oracle response size as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue #3563 created.

#[error("Runtime failed to respond to application")]
MissingRuntimeResponse,
#[error("Module ID {0:?} is invalid")]
Expand Down
6 changes: 6 additions & 0 deletions linera-execution/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ pub struct ResourceControlPolicy {
pub maximum_bytes_read_per_block: u64,
/// The maximum data to write per block
pub maximum_bytes_written_per_block: u64,
/// The maximum size in bytes of a received HTTP response.
pub maximum_http_response_bytes: u64,
/// The maximum amount of time allowed to wait for an HTTP response.
pub http_request_timeout_ms: u64,
/// The list of hosts that contracts and services can send HTTP requests to.
Expand Down Expand Up @@ -92,6 +94,7 @@ impl fmt::Display for ResourceControlPolicy {
maximum_block_proposal_size,
maximum_bytes_read_per_block,
maximum_bytes_written_per_block,
maximum_http_response_bytes,
http_request_allow_list,
http_request_timeout_ms,
} = self;
Expand Down Expand Up @@ -119,6 +122,7 @@ impl fmt::Display for ResourceControlPolicy {
{maximum_block_proposal_size} maximum size of a block proposal\n\
{maximum_bytes_read_per_block} maximum number bytes read per block\n\
{maximum_bytes_written_per_block} maximum number bytes written per block\n\
{maximum_http_response_bytes} maximum number of bytes of an HTTP response\n\
{http_request_timeout_ms} ms timeout for HTTP requests\n\
HTTP hosts allowed for contracts and services: {http_request_allow_list:#?}\n",
)?;
Expand Down Expand Up @@ -158,6 +162,7 @@ impl ResourceControlPolicy {
maximum_block_proposal_size: u64::MAX,
maximum_bytes_read_per_block: u64::MAX,
maximum_bytes_written_per_block: u64::MAX,
maximum_http_response_bytes: u64::MAX,
http_request_timeout_ms: u64::MAX,
http_request_allow_list: BTreeSet::new(),
}
Expand Down Expand Up @@ -225,6 +230,7 @@ impl ResourceControlPolicy {
maximum_block_proposal_size: 13_000_000,
maximum_bytes_read_per_block: 100_000_000,
maximum_bytes_written_per_block: 10_000_000,
maximum_http_response_bytes: 10_000,
http_request_timeout_ms: 20_000,
http_request_allow_list: BTreeSet::new(),
}
Expand Down
3 changes: 2 additions & 1 deletion linera-execution/tests/fee_consumption.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ async fn test_fee_consumption(
maximum_block_proposal_size: 59,
maximum_bytes_read_per_block: 61,
maximum_bytes_written_per_block: 67,
http_request_timeout_ms: 71,
maximum_http_response_bytes: 71,
http_request_timeout_ms: 73,
http_request_allow_list: BTreeSet::new(),
};

Expand Down
1 change: 1 addition & 0 deletions linera-rpc/tests/snapshots/format__format.yaml.snap
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,7 @@ ResourceControlPolicy:
- maximum_block_proposal_size: U64
- maximum_bytes_read_per_block: U64
- maximum_bytes_written_per_block: U64
- maximum_http_response_bytes: U64
- http_request_timeout_ms: U64
- http_request_allow_list:
SEQ: STR
Expand Down
4 changes: 4 additions & 0 deletions linera-service-graphql-client/gql/service_schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,10 @@ input ResourceControlPolicy {
"""
maximumBytesWrittenPerBlock: Int!
"""
The maximum size in bytes of a received HTTP response.
"""
maximumHttpResponseBytes: Int!
"""
The maximum amount of time allowed to wait for an HTTP response.
"""
httpRequestTimeoutMs: Int!
Expand Down