Skip to content

Gloas serve envelope rpc#8896

Open
eserilev wants to merge 30 commits intosigp:unstablefrom
eserilev:gloas-serve-envelope-rpc
Open

Gloas serve envelope rpc#8896
eserilev wants to merge 30 commits intosigp:unstablefrom
eserilev:gloas-serve-envelope-rpc

Conversation

@eserilev
Copy link
Member

Issue Addressed

Serves envelope by range and by root requests. Added PayloadEnvelopeStreamer so that we dont need to alter upstream code when we introduce blinded payload envelopes.

@eserilev eserilev requested a review from jxs as a code owner February 24, 2026 09:15
@eserilev eserilev added gloas ready-for-review The code is ready for review labels Feb 24, 2026
let requested_envelopes = request.beacon_block_roots.len();
let mut envelope_stream = match self
.chain
.get_payload_envelopes_checking_caches(request.beacon_block_roots.to_vec())
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is it necessary to have this stream gadget instead of a simple for loop?

for block_root in block_roots {
  if Some(payload) = chain.get_payload_checking_caches(block_root) {
    send_to_peer(payload)
  }
}

Copy link
Member Author

Choose a reason for hiding this comment

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

We're going to use getPayloadBodiesByRange which allows us to fetch a batch of payloads in one engine api request. If we did the simple for loop we'd have to make an engine api request for each payload

Copy link
Member

@jimmygchen jimmygchen left a comment

Choose a reason for hiding this comment

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

The new protocols need to be added to this function:

fn currently_supported(fork_context: &ForkContext) -> Vec<ProtocolId> {

Added some tests to catch this kind of bug:

Comment on lines +572 to +575
rpc_block_limits_by_fork(fork_context.current_fork_name())
}
Protocol::PayloadEnvelopesByRoot => {
rpc_block_limits_by_fork(fork_context.current_fork_name())
Copy link
Member

Choose a reason for hiding this comment

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

we may need a rpc_payload_envelope_limits function and update the rpc_block_limits_by_fork to include Gloas block size limit.

Copy link
Member Author

Choose a reason for hiding this comment

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

updated rpc_block_limits_by_fork and introduced rpc_payload_envelope_limits

Copy link
Member Author

Choose a reason for hiding this comment

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

We still need to set rpc_block_limits_by_fork max to be bellatrix_max for historical sync compatibility

// TODO(gloas) we'll want to use the execution layer directly to call
// the engine api method eth_getBlockByHash()
self.store
.get_payload_envelope(&beacon_block_root)
Copy link
Member

@jimmygchen jimmygchen Mar 2, 2026

Choose a reason for hiding this comment

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

synchronous db read inside async task here, use a blocking task (spawn_blocking)

Copy link
Member Author

Choose a reason for hiding this comment

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

added spawn_blocking. I had to introduce a new span_blocking_and_await fn inside task executor. its the same spawn_blocking wrapper we have in beacon chain. I did this because I didnt want envelope streamer to rely on beacon chain.

@jimmygchen jimmygchen added waiting-on-author The reviewer has suggested changes and awaits thier implementation. and removed ready-for-review The code is ready for review labels Mar 2, 2026
@eserilev eserilev added ready-for-review The code is ready for review and removed waiting-on-author The reviewer has suggested changes and awaits thier implementation. labels Mar 4, 2026
RequestType::PayloadEnvelopesByRoot(PayloadEnvelopesByRootRequest {
beacon_block_roots: RuntimeVariableList::from_ssz_bytes(
decoded_buffer,
spec.max_request_blocks(current_fork),
Copy link
Member

Choose a reason for hiding this comment

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

This should be spec.max_request_payloads() to be consistent with the other fixes

Copy link
Member

@jimmygchen jimmygchen left a comment

Choose a reason for hiding this comment

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

The new protocols need count validation here too, otherwise a peer can make us iterate the full chain instead of the expected max 128 slots:

@jimmygchen jimmygchen added waiting-on-author The reviewer has suggested changes and awaits thier implementation. and removed ready-for-review The code is ready for review labels Mar 6, 2026
let results = match self.load_envelopes(&beacon_block_roots).await {
Ok(results) => results,
Err(e) => {
send_errors(beacon_block_roots, sender, e).await;
Copy link
Member

Choose a reason for hiding this comment

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

may be worth logging some errors/warn here?

Copy link
Member Author

Choose a reason for hiding this comment

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

added warn log

@eserilev eserilev added ready-for-review The code is ready for review and removed waiting-on-author The reviewer has suggested changes and awaits thier implementation. labels Mar 14, 2026
@eserilev
Copy link
Member Author

This is ready for a review. We're skipping envelope verification here, since we dont have fork choice yet.

@dapplion
Copy link
Collaborator

Suggestion: avoid wrapping canonical_head in ArcBeaconChain is already behind an Arc everywhere, so the streamer can hold Arc<BeaconChain<T>> instead.

Implement CanonicalHeadReader on BeaconChain<T> (delegating to self.canonical_head) and pass self.clone() to the streamer. This avoids the structural change to the canonical_head field.

See: dapplion@3e6afae31

@dapplion
Copy link
Collaborator

Also, BeaconBlockGloas::full() is dead code — it's defined but never called (SignedExecutionPayloadEnvelope::max_size() computes the size arithmetically instead). Beyond being unused, the impl is fragile: it starts from empty(spec) then mutates fields with push loops, so adding a new field compiles fine but silently leaves it empty. A direct struct construction would catch missing fields at compile time.

Removed it along with 9 unused imports it pulled in: dapplion@ed8c5495f

@mergify
Copy link

mergify bot commented Mar 16, 2026

Some required checks have failed. Could you please take a look @eserilev? 🙏

@mergify mergify bot added waiting-on-author The reviewer has suggested changes and awaits thier implementation. and removed ready-for-review The code is ready for review labels Mar 16, 2026
@eserilev eserilev added ready-for-review The code is ready for review and removed waiting-on-author The reviewer has suggested changes and awaits thier implementation. labels Mar 16, 2026
@eserilev
Copy link
Member Author

@dapplion your changes make sense, i went ahead and merged them. thanks!

use tree_hash_derive::TreeHash;

#[derive(Debug, Clone, Serialize, Encode, Decode, Deserialize, TestRandom, TreeHash, Educe)]
#[derive(
Copy link
Member

Choose a reason for hiding this comment

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

Can we create an explicit empty() method for the default case?

Copy link
Member Author

Choose a reason for hiding this comment

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


/// Returns the maximum SSZ-encoded size.
#[allow(clippy::arithmetic_side_effects)]
pub fn max_size() -> usize {
Copy link
Member

Choose a reason for hiding this comment

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

might be better to have a max_size on the ExecutionPayloadEnvelope as well and use it here instead so we don't forget to update it if the spec changes

Copy link
Member Author

Choose a reason for hiding this comment

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


/// Error type for spawning a blocking task and awaiting its result.
#[derive(Debug)]
pub enum SpawnBlockingError {
Copy link
Member

Choose a reason for hiding this comment

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

I'm not a fan of doing this without refactoring the spawn_blocking_handle function in beacon_chain.rs to also use this.
We'll just end up having multiple functions that do the same thing for no reason.

Copy link
Member Author

Choose a reason for hiding this comment

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

this is no longer needed due to the adapter pattern change and has been removed

// TODO(gloas) remove expect when execution layer field
// is no longer dead.
#[expect(dead_code)]
execution_layer: ExecutionLayer<T::EthSpec>,
Copy link
Member

Choose a reason for hiding this comment

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

Why don't we start with using the beacon chain here instead and do beacon_chain.execution_layer when we need to access it? that way we get to keep pretty similar logic to the beaocn block streamer.
We need the beacon_chain to access the db anyway

Copy link
Member

Choose a reason for hiding this comment

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

If testing is the concern, we can take a similar approach to the fetch_blobs adapter.

Here, it seems like we need ExecutionLayer, CanonicalHead and the store all of which are present in the BeaconChain

Copy link
Member Author

Choose a reason for hiding this comment

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

ive went with the adapter pattern

Ok(Some(cached_envelope))
} else {
// TODO(gloas) we'll want to use the execution layer directly to call
// the engine api method eth_getBlockByHash()
Copy link
Member

Choose a reason for hiding this comment

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

do you mean getPayloadBodiesByRange or some equivalent thing that returns in the right format for the CL?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes, i've updated the comment

@eserilev
Copy link
Member Author

went ahead with the adapter pattern, i think its the cleaner approach. thanks pawan for suggesting it. theres a bit of code churn from those changes, sorry about that... this should be ready for another round of reviews

@mergify
Copy link

mergify bot commented Mar 17, 2026

This pull request has merge conflicts. Could you please resolve them @eserilev? 🙏

@mergify mergify bot added waiting-on-author The reviewer has suggested changes and awaits thier implementation. and removed ready-for-review The code is ready for review labels Mar 17, 2026
@eserilev eserilev added ready-for-review The code is ready for review and removed waiting-on-author The reviewer has suggested changes and awaits thier implementation. labels Mar 18, 2026
@dapplion
Copy link
Collaborator

dapplion commented Mar 18, 2026

🤖 Note on memory amplification (inherited pattern, not new to this PR):

load_envelopes() materializes all results into a Vec before streaming, and launch_stream() uses an unbounded_channel(). With max_request_payloads = 128 and practical envelope sizes ~2MB (gas-limited), one request buffers ~256MB. Per-peer limits allow 4 concurrent requests (2 by-range + 2 by-root via MAX_CONCURRENT_REQUESTS=2), so ~1GB per peer. With 220 max peers, theoretical worst case is ~220GB.

(lion) However in practice the Beacon processor has limited worker threads, so not all 1024 queued items run simultaneously, on high CPU machine it may be a slight concern.

The BlockStreamer has the exact same pattern and has been running in production, so this isn't a regression — just worth noting as the envelope payloads can be larger than blocks. Rate limiting (128 items/10s per peer) and peer scoring provide some mitigation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gloas ready-for-review The code is ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants