From 1d152677d4641d6d583adddd13feb67985be434a Mon Sep 17 00:00:00 2001 From: Subhasish Date: Fri, 3 Apr 2026 01:44:45 +0530 Subject: [PATCH] fix(rpc): compute envelope max size arithmetically to avoid OOM - The previous implementation used `SignedExecutionPayloadEnvelope::full()` to determine the max SSZ size for RPC limits. This initiated all variable-length fields at maximum capacity, including MAX_TRANSACTIONS_PER_PAYLOAD transactions each at MAX_BYTES_PER_TRANSACTION, causing multi-GB allocations that triggered OOM during libp2p initialization. - not preffering ghost value style calculations like SIGNED_BEACON_BLOCK_BELLATRIX_MAX for futrure reference about the calculations - Replace with `full_gloas_signed_execution_payload_envelope_size

()` which serializes the envelope with default (empty) transactions and adds the transaction contribution arithmetically: N * (BYTES_PER_LENGTH_OFFSET + max_bytes_per_transaction) - This produces the same max size value without allocating the actual transaction bytes. - account for `block_access_list` in max envelope size --- src/rpc/protocol.rs | 64 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/src/rpc/protocol.rs b/src/rpc/protocol.rs index 926ef96..091338b 100644 --- a/src/rpc/protocol.rs +++ b/src/rpc/protocol.rs @@ -6,7 +6,10 @@ use futures::prelude::{AsyncRead, AsyncWrite}; use futures::{FutureExt, StreamExt}; use helper_functions::misc; use libp2p::core::{InboundUpgrade, UpgradeInfo}; -use ssz::{H256, ReadError, SszSize as _, SszWrite as _, WriteError}; +use ssz::{ + BYTES_PER_LENGTH_OFFSET, ByteList, ContiguousList, H256, ReadError, SszSize as _, + SszWrite as _, WriteError, +}; use std::io; use std::marker::PhantomData; use std::sync::Arc; @@ -19,10 +22,15 @@ use tokio_util::{ compat::{Compat, FuturesAsyncReadCompatExt}, }; use typenum::Unsigned as _; +use types::capella::containers::Withdrawal; use types::deneb::containers::BlobIdentifier; +use types::electra::containers::{ + ConsolidationRequest, DepositRequest, ExecutionRequests, WithdrawalRequest, +}; use types::fulu::containers::DataColumnSidecar as FuluDataColumnSidecar; use types::gloas::containers::{ - DataColumnSidecar as GloasDataColumnSidecar, SignedExecutionPayloadEnvelope, + DataColumnSidecar as GloasDataColumnSidecar, ExecutionPayload, ExecutionPayloadEnvelope, + SignedExecutionPayloadEnvelope, }; use types::phase0::primitives::Epoch; use types::{ @@ -84,15 +92,6 @@ pub static SIGNED_EXECUTION_PAYLOAD_ENVELOPE_GLOAS_MIN: LazyLock = LazyLo .len() }); -/// Maximum SSZ size of SignedExecutionPayloadEnvelope. -/// Uses .full() method which fills all variable-length fields to maximum. -pub static SIGNED_EXECUTION_PAYLOAD_ENVELOPE_GLOAS_MAX: LazyLock = LazyLock::new(|| { - SignedExecutionPayloadEnvelope::::full() - .to_ssz() - .expect("should serialize") - .len() -}); - pub const ERROR_TYPE_MIN: usize = 0; pub const ERROR_TYPE_MAX: usize = 256; @@ -187,6 +186,49 @@ fn rpc_light_client_bootstrap_limits_by_fork(current_fork: Phase) -> } } +/// Maximum SSZ size of SignedExecutionPayloadEnvelope. +/// Uses arithmetic calculation for transactions instead of allocating full max-size payloads. +pub static SIGNED_EXECUTION_PAYLOAD_ENVELOPE_GLOAS_MAX: LazyLock = + LazyLock::new(full_gloas_signed_execution_payload_envelope_size); + +/// Compute max SSZ size of SignedExecutionPayloadEnvelope without materializing transactions. +/// +/// Populates all cheap fields at full capacity (extra_data, withdrawals, execution_requests). +/// Transactions are left default (empty) and their max size is added arithmetically: +/// N offsets (4 bytes each) + N * max_bytes_per_transaction. +/// block_access_list is also default (empty); max size = max_bytes_per_transaction. +fn full_gloas_signed_execution_payload_envelope_size() -> usize { + let envelope_with_default_txs = SignedExecutionPayloadEnvelope:: { + message: ExecutionPayloadEnvelope { + payload: ExecutionPayload { + extra_data: Arc::new(ByteList::from(ContiguousList::full(u8::MAX))), + transactions: Arc::new(ContiguousList::default()), + withdrawals: ContiguousList::full(Withdrawal::default()), + ..Default::default() + }, + execution_requests: ExecutionRequests { + deposits: ContiguousList::full(DepositRequest::default()), + withdrawals: ContiguousList::full(WithdrawalRequest::default()), + consolidations: ContiguousList::full(ConsolidationRequest::default()), + }, + ..Default::default() + }, + ..Default::default() + }; + + let base = envelope_with_default_txs + .to_ssz() + .expect("should serialize") + .len(); + + let max_transactions = ::MaxTransactionsPerPayload::USIZE + * (BYTES_PER_LENGTH_OFFSET + ::MaxBytesPerTransaction::USIZE); + + let max_block_access_list = ::MaxBytesPerTransaction::USIZE; + + base + max_transactions + max_block_access_list +} + fn rpc_execution_payload_envelope_limits() -> RpcLimits { RpcLimits::new( *SIGNED_EXECUTION_PAYLOAD_ENVELOPE_GLOAS_MIN,