Skip to content

Commit 8fc2a3f

Browse files
committed
fix(rpc): engine_rest #793 interop w/ consensoor
1 parent c7ba19b commit 8fc2a3f

26 files changed

Lines changed: 919 additions & 1487 deletions

.github/workflows/pr-main_l1.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,20 @@ jobs:
119119

120120
- name: Run unit tests
121121
run: |
122+
# macOS runners have a low default process/thread limit; the per-Blockchain
123+
# merkle thread pool (crates/blockchain/blockchain.rs) multiplied across the
124+
# parallel test suite exhausts it (pthread_create EAGAIN, code 35). Raise it.
125+
if [ "$RUNNER_OS" = "macOS" ]; then
126+
ulimit -u 16384 2>/dev/null || ulimit -u "$(ulimit -H -u)" 2>/dev/null || true
127+
fi
122128
cargo test --workspace --exclude 'ethrex-l2*' --exclude ethrex-prover --exclude ethrex-guest-program
123129
124130
- name: Run Blockchain EF tests
125131
if: ${{ github.event_name != 'merge_group' }}
126132
run: |
133+
if [ "$RUNNER_OS" = "macOS" ]; then
134+
ulimit -u 16384 2>/dev/null || ulimit -u "$(ulimit -H -u)" 2>/dev/null || true
135+
fi
127136
make -C tooling/ef_tests/blockchain test
128137
129138
- name: Append Known Issues to job summary

crates/l2/tee/quote-gen/Cargo.lock

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/networking/rpc/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ license.workspace = true
99
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1010

1111
[dependencies]
12-
axum = { features = ["ws"], workspace = true }
12+
# "http2" is required so `axum::serve` serves HTTP/2 cleartext (h2c) on the
13+
# authrpc port: the engine REST/SSZ API (execution-apis #793) mandates HTTP/2,
14+
# and CLs like consensoor connect with h2c prior-knowledge and do NOT fall back
15+
# to HTTP/1.1. Without this feature h2c serving would only work incidentally via
16+
# reqwest's feature unification — declare it explicitly so it can't regress.
17+
axum = { features = ["ws", "http2"], workspace = true }
1318
tower-http.workspace = true
1419
serde.workspace = true
1520
serde_json = "1.0.117"

crates/networking/rpc/benches/engine_transport.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ fn bodies_bench(c: &mut Criterion) {
150150
});
151151
g.bench_function("decode_ssz", |b| {
152152
b.iter(|| {
153-
let r = ethrex_rpc::engine_rest::types::bodies::BodiesByHashResponseShanghai::from_ssz_bytes(
153+
let r = ethrex_rpc::engine_rest::types::bodies::BodiesResponseShanghai::from_ssz_bytes(
154154
&ssz_bytes,
155155
)
156156
.unwrap();

crates/networking/rpc/benches/fixtures.rs

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -281,23 +281,21 @@ pub fn bodies_range_json(
281281
.collect()
282282
}
283283

284-
/// SSZ-side bodies response wrapping `Vec<OptBodyShanghai>`.
284+
/// SSZ-side bodies response wrapping `List[BodyEntryShanghai]`.
285285
#[allow(dead_code)]
286286
pub fn bodies_range_ssz(
287287
seed: u64,
288288
n: usize,
289289
tx_per_body: usize,
290-
) -> ethrex_rpc::engine_rest::types::bodies::BodiesByHashResponseShanghai {
291-
use ethrex_rpc::engine_rest::types::bodies::{
292-
BodiesByHashResponseShanghai, BodyShanghai, OptBodyShanghai,
293-
};
290+
) -> ethrex_rpc::engine_rest::types::bodies::BodiesResponseShanghai {
291+
use ethrex_rpc::engine_rest::types::bodies::{BodyEntryShanghai, BodyShanghai};
294292
use ethrex_rpc::engine_rest::types::common::{
295293
MAX_BYTES_PER_TRANSACTION, MAX_TRANSACTIONS_PER_PAYLOAD, MAX_WITHDRAWALS_PER_PAYLOAD,
296294
};
297295
use libssz_types::SszList;
298296

299297
let mut rng = StdRng::seed_from_u64(seed);
300-
let bodies: Vec<OptBodyShanghai> = (0..n)
298+
let entries_vec: Vec<BodyEntryShanghai> = (0..n)
301299
.map(|_| {
302300
let tx_lists: Vec<SszList<u8, MAX_BYTES_PER_TRANSACTION>> = (0..tx_per_body)
303301
.map(|_| {
@@ -317,13 +315,15 @@ pub fn bodies_range_ssz(
317315
ethrex_rpc::engine_rest::types::shanghai::Withdrawal,
318316
MAX_WITHDRAWALS_PER_PAYLOAD,
319317
> = Vec::new().try_into().expect("empty withdrawals fits");
320-
OptBodyShanghai(Some(BodyShanghai {
318+
BodyEntryShanghai::available(BodyShanghai {
321319
transactions,
322320
withdrawals,
323-
}))
321+
})
324322
})
325323
.collect();
326-
BodiesByHashResponseShanghai { bodies }
324+
entries_vec
325+
.try_into()
326+
.expect("bodies fit MAX_BODIES_PER_REQUEST")
327327
}
328328

329329
/// JSON-side blobs response: `Vec<Option<BlobAndProofV1>>`.
@@ -354,7 +354,6 @@ pub fn blobs_v1_response_ssz(
354354
) -> ethrex_rpc::engine_rest::types::blobs::BlobsV1Response {
355355
use ethrex_rpc::engine_rest::types::blobs::{
356356
BYTES_PER_BLOB, BYTES_PER_PROOF, BlobAndProofV1 as SszBlobAndProofV1, BlobV1Entry,
357-
BlobsV1Response,
358357
};
359358
use libssz_types::SszVector;
360359

@@ -373,9 +372,7 @@ pub fn blobs_v1_response_ssz(
373372
})
374373
})
375374
.collect();
376-
BlobsV1Response {
377-
entries: entries.try_into().expect("n <= MAX_BLOBS_REQUEST"),
378-
}
375+
entries.try_into().expect("n <= MAX_BLOBS_REQUEST")
379376
}
380377

381378
/// JSON-side getPayload response — same `ExecutionPayload` shape as newPayload.

crates/networking/rpc/engine_rest/auth.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,14 @@ pub async fn engine_auth_middleware(
4949
}
5050
};
5151

52-
let token = match auth_str.strip_prefix(BEARER_PREFIX) {
53-
Some(t) => t,
54-
None => {
52+
// RFC 7235 §2.1: the auth-scheme token is case-insensitive. `BEARER_PREFIX`
53+
// is ASCII, so byte index 7 is also char index 7 — slicing is safe once the
54+
// prefix matches.
55+
let token = match auth_str.get(..BEARER_PREFIX.len()) {
56+
Some(prefix) if prefix.eq_ignore_ascii_case(BEARER_PREFIX) => {
57+
&auth_str[BEARER_PREFIX.len()..]
58+
}
59+
_ => {
5560
return ProblemJson::unauthorized("Authorization header must be Bearer")
5661
.into_response();
5762
}

crates/networking/rpc/engine_rest/extractors.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ use axum::http::{Request, header};
77
use http_body_util::LengthLimitError;
88
use libssz::SszDecode;
99

10+
use crate::engine_rest::CONTENT_TYPE_OCTET_STREAM;
1011
use crate::engine_rest::error::ProblemJson;
1112

12-
const CONTENT_TYPE_OCTET_STREAM: &str = "application/octet-stream";
13-
1413
/// Axum extractor that reads an SSZ-encoded request body into `T`.
1514
///
1615
/// Errors map to RFC 7807 responses:

crates/networking/rpc/engine_rest/handlers/blobs.rs

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
//! /blobs/v{1..4} — blob retrieval from mempool, per execution-apis #793.
22
//!
3-
//! Response shape is `List[BlobV*Entry]` where each entry carries an
4-
//! `available` flag plus `contents` (zeroed when unavailable). `/blobs/v2` is
3+
//! Requests are a bare `List[VersionedHash]` (v1/v2/v3) or a `BlobsRequestV4`
4+
//! container (v4). Responses are a bare `List[BlobV*Entry]`. `/blobs/v2` is
55
//! all-or-nothing: if any requested blob is missing the handler returns
66
//! `204 No Content` instead of emitting unavailable entries. `/blobs/v3`
77
//! surfaces missing blobs per entry. `/blobs/v4` requires per-cell data that
8-
//! the mempool does not store, so it returns `204 No Content` ("EL cannot
9-
//! serve this request at all").
8+
//! the mempool does not store, so it returns `204 No Content`.
109
1110
use axum::extract::State;
1211
use axum::http::StatusCode;
@@ -18,8 +17,8 @@ use crate::engine_rest::extractors::Ssz;
1817
use crate::engine_rest::handlers::capabilities::BLOBS_MAX_COUNT;
1918
use crate::engine_rest::responses::SszBody;
2019
use crate::engine_rest::types::blobs::{
21-
BlobAndProofV1, BlobAndProofV2, BlobV1Entry, BlobV2Entry, BlobsRequest, BlobsRequestV4,
22-
BlobsV1Response, BlobsV2Response, BlobsV3Response,
20+
BlobAndProofV1, BlobAndProofV2, BlobV1Entry, BlobV2Entry, BlobsRequestV4, BlobsV1Response,
21+
BlobsV2Response, BlobsV3Response, VersionedHashList,
2322
};
2423
use crate::rpc::RpcApiContext;
2524

@@ -33,8 +32,11 @@ fn request_hashes(versioned_hashes: &[[u8; 32]]) -> Result<Vec<H256>, ProblemJso
3332
Ok(versioned_hashes.iter().map(|h| H256::from(*h)).collect())
3433
}
3534

36-
pub async fn blobs_v1(State(ctx): State<RpcApiContext>, Ssz(req): Ssz<BlobsRequest>) -> Response {
37-
let hashes = match request_hashes(&req.versioned_hashes) {
35+
pub async fn blobs_v1(
36+
State(ctx): State<RpcApiContext>,
37+
Ssz(req): Ssz<VersionedHashList>,
38+
) -> Response {
39+
let hashes = match request_hashes(&req) {
3840
Ok(h) => h,
3941
Err(p) => return p.into_response(),
4042
};
@@ -66,17 +68,19 @@ pub async fn blobs_v1(State(ctx): State<RpcApiContext>, Ssz(req): Ssz<BlobsReque
6668
entries.push(entry);
6769
}
6870

69-
match entries.try_into() {
70-
Ok(entries) => SszBody(BlobsV1Response { entries }).into_response(),
71+
match TryInto::<BlobsV1Response>::try_into(entries) {
72+
Ok(resp) => SszBody(resp).into_response(),
7173
Err(_) => ProblemJson::internal("blobs response exceeds MAX_BLOBS_REQUEST").into_response(),
7274
}
7375
}
7476

7577
/// `/blobs/v2` — all-or-nothing (Osaka). If any requested blob is missing the
76-
/// EL MUST return `204 No Content` (spec §`POST /blobs/v2`), mirroring
77-
/// `engine_getBlobsV2`'s `null` response.
78-
pub async fn blobs_v2(State(ctx): State<RpcApiContext>, Ssz(req): Ssz<BlobsRequest>) -> Response {
79-
let hashes = match request_hashes(&req.versioned_hashes) {
78+
/// EL MUST return `204 No Content`, mirroring `engine_getBlobsV2`'s `null`.
79+
pub async fn blobs_v2(
80+
State(ctx): State<RpcApiContext>,
81+
Ssz(req): Ssz<VersionedHashList>,
82+
) -> Response {
83+
let hashes = match request_hashes(&req) {
8084
Ok(h) => h,
8185
Err(p) => return p.into_response(),
8286
};
@@ -113,17 +117,20 @@ pub async fn blobs_v2(State(ctx): State<RpcApiContext>, Ssz(req): Ssz<BlobsReque
113117
}
114118
}
115119

116-
match entries.try_into() {
117-
Ok(entries) => SszBody(BlobsV2Response { entries }).into_response(),
120+
match TryInto::<BlobsV2Response>::try_into(entries) {
121+
Ok(resp) => SszBody(resp).into_response(),
118122
Err(_) => ProblemJson::internal("blobs response exceeds MAX_BLOBS_REQUEST").into_response(),
119123
}
120124
}
121125

122126
/// `/blobs/v3` — partial responses (Osaka). Missing blobs surface as
123127
/// `available == false` entries; `204 No Content` is reserved for "EL cannot
124128
/// serve the request at all".
125-
pub async fn blobs_v3(State(ctx): State<RpcApiContext>, Ssz(req): Ssz<BlobsRequest>) -> Response {
126-
let hashes = match request_hashes(&req.versioned_hashes) {
129+
pub async fn blobs_v3(
130+
State(ctx): State<RpcApiContext>,
131+
Ssz(req): Ssz<VersionedHashList>,
132+
) -> Response {
133+
let hashes = match request_hashes(&req) {
127134
Ok(h) => h,
128135
Err(p) => return p.into_response(),
129136
};
@@ -155,8 +162,8 @@ pub async fn blobs_v3(State(ctx): State<RpcApiContext>, Ssz(req): Ssz<BlobsReque
155162
entries.push(entry);
156163
}
157164

158-
match entries.try_into() {
159-
Ok(entries) => SszBody(BlobsV3Response { entries }).into_response(),
165+
match TryInto::<BlobsV3Response>::try_into(entries) {
166+
Ok(resp) => SszBody(resp).into_response(),
160167
Err(_) => ProblemJson::internal("blobs response exceeds MAX_BLOBS_REQUEST").into_response(),
161168
}
162169
}

0 commit comments

Comments
 (0)