From 04b25898072de2271904b950285cf2fe3f9fb494 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 29 Apr 2026 17:43:30 +0200 Subject: [PATCH 1/3] Import execution payload envelope locally during HTTP API publication (#9226) Fixes a bug where a proposer votes payload missing on its own block. The payload is published to the network but never imported locally. This PR adds gossip verification and import when a payload is sent to the http API Co-Authored-By: Jimmy Chen --- .../src/beacon/execution_payload_envelope.rs | 58 ++++++++---- beacon_node/http_api/tests/tests.rs | 88 +++++++++++++++++++ 2 files changed, 131 insertions(+), 15 deletions(-) diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs index 06a5915c08..65e1a83840 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs @@ -7,7 +7,7 @@ use crate::version::{ execution_optimistic_finalized_beacon_response, }; use beacon_chain::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumn}; -use beacon_chain::{BeaconChain, BeaconChainTypes}; +use beacon_chain::{BeaconChain, BeaconChainTypes, NotifyExecutionLayer}; use bytes::Bytes; use eth2::types as api_types; use eth2::{CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER}; @@ -18,7 +18,7 @@ use std::future::Future; use std::sync::Arc; use tokio::sync::mpsc::UnboundedSender; use tracing::{debug, error, info, warn}; -use types::{EthSpec, SignedExecutionPayloadEnvelope}; +use types::{BlockImportSource, EthSpec, SignedExecutionPayloadEnvelope}; use warp::{ Filter, Rejection, Reply, hyper::{Body, Response}, @@ -99,14 +99,12 @@ pub async fn publish_execution_payload_envelope( let slot = envelope.slot(); let beacon_block_root = envelope.message.beacon_block_root; - // TODO(gloas): Replace this check once we have gossip validation. if !chain.spec.is_gloas_scheduled() { return Err(warp_utils::reject::custom_bad_request( "Execution payload envelopes are not supported before the Gloas fork".into(), )); } - // TODO(gloas): We should probably add validation here i.e. BroadcastValidation::Gossip info!( %slot, %beacon_block_root, @@ -118,7 +116,7 @@ pub async fn publish_execution_payload_envelope( // Spawn the column-build task (CPU-bound KZG cell-and-proof computation) before // publishing the envelope so it runs in parallel with envelope gossip, narrowing - // the window in which peers see envelope-without-columns. If envelope publication + // the window in which peers see envelope-without-columns. If envelope import // fails below, dropping this future drops the spawned `JoinHandle` (the running // closure on the blocking pool finishes and is then discarded — no work cancellation). let column_build_future = match blobs_and_proofs { @@ -131,17 +129,47 @@ pub async fn publish_execution_payload_envelope( _ => None, }; - // Publish the envelope to the network. - crate::utils::publish_pubsub_message( - network_tx, - PubsubMessage::ExecutionPayload(Box::new(envelope)), - ) - .map_err(|_| { - warn!(%slot, "Failed to publish execution payload envelope to network"); - warp_utils::reject::custom_server_error( - "Unable to publish execution payload envelope to network".into(), + // Gossip-verify the envelope before publishing. + let gossip_verified = chain + .verify_envelope_for_gossip(Arc::new(envelope)) + .await + .map_err(|e| { + warn!(%slot, error = ?e, "Execution payload envelope failed gossip verification"); + warp_utils::reject::custom_bad_request(format!( + "envelope failed gossip verification: {e}" + )) + })?; + + let network_tx_clone = network_tx.clone(); + let envelope_for_gossip = gossip_verified.signed_envelope.as_ref().clone(); + let publish_fn = || { + crate::utils::publish_pubsub_message( + &network_tx_clone, + PubsubMessage::ExecutionPayload(Box::new(envelope_for_gossip)), ) - })?; + .map_err(|_| { + beacon_chain::payload_envelope_verification::EnvelopeError::BeaconChainError(Arc::new( + beacon_chain::BeaconChainError::UnableToPublish, + )) + }) + }; + + let import_result = chain + .process_execution_payload_envelope( + beacon_block_root, + gossip_verified, + NotifyExecutionLayer::Yes, + BlockImportSource::HttpApi, + publish_fn, + ) + .await; + + if let Err(e) = import_result { + warn!(%slot, error = ?e, "Failed to import execution payload envelope"); + return Err(warp_utils::reject::custom_server_error(format!( + "envelope import failed: {e}" + ))); + } // From here on the envelope is on the wire. `take_blobs` already consumed the cache // entry, so a retry would not republish columns; returning Err would mislead the diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 56835da459..01a77ad4d7 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -4644,6 +4644,86 @@ impl ApiTester { self } + /// Regression test: publishing an envelope via the HTTP API must import it locally so + /// that `produce_payload_attestation_data` returns `payload_present = true`. Without + /// local import, the `envelope_times_cache` is never populated and PTC voters on the + /// same node incorrectly vote MISSING for their own payload. + pub async fn test_payload_attestation_present_after_envelope_publish(self) -> Self { + if !self.chain.spec.is_gloas_scheduled() { + return self; + } + + let fork = self.chain.canonical_head.cached_head().head_fork(); + let genesis_validators_root = self.chain.genesis_validators_root; + + for _ in 0..E::slots_per_epoch() * 3 { + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + let fork_name = self.chain.spec.fork_name_at_slot::(slot); + + if !fork_name.gloas_enabled() { + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + continue; + } + + let (sk, randao_reveal) = self + .proposer_setup(slot, epoch, &fork, genesis_validators_root) + .await; + + // Produce and publish a block. + let (response, _metadata) = self + .client + .get_validator_blocks_v4::(slot, &randao_reveal, None, None, None) + .await + .unwrap(); + let block = response.data; + let block_root = block.tree_hash_root(); + + let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + let signed_block_request = + PublishBlockRequest::try_from(Arc::new(signed_block)).unwrap(); + self.client + .post_beacon_blocks_v2(&signed_block_request, None) + .await + .unwrap(); + + // Retrieve and publish the envelope. + let envelope = self + .client + .get_validator_execution_payload_envelope::(slot, BUILDER_INDEX_SELF_BUILD) + .await + .unwrap() + .data; + + let signed_envelope = + self.sign_envelope(envelope, &sk, epoch, &fork, genesis_validators_root); + self.client + .post_beacon_execution_payload_envelope(&signed_envelope, fork_name) + .await + .unwrap(); + + // The payload attestation data endpoint must now report the payload as present. + let pa_data = self + .client + .get_validator_payload_attestation_data(slot) + .await + .unwrap() + .into_data(); + + assert_eq!(pa_data.beacon_block_root, block_root); + assert_eq!(pa_data.slot, slot); + assert!( + pa_data.payload_present, + "payload attestation should report payload_present=true after publishing \ + the envelope via the HTTP API (slot {slot})" + ); + + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + } + + self + } + pub async fn test_get_validator_payload_attestation_data_pre_gloas(self) -> Self { let slot = self.chain.slot().unwrap(); @@ -8333,6 +8413,14 @@ async fn get_validator_payload_attestation_data_pre_gloas() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn payload_attestation_present_after_envelope_publish() { + ApiTester::new_with_hard_forks() + .await + .test_payload_attestation_present_after_envelope_publish() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn post_beacon_pool_payload_attestations_valid() { if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { From 8d77b1c08d445b2d47cd223f37da1223d87a3ad1 Mon Sep 17 00:00:00 2001 From: chonghe <44791194+chong-he@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:57:47 +0800 Subject: [PATCH 2/3] Remove `test_logger` feature (#9125) - #9107 Remove all instances of `test_logger` in the code Co-Authored-By: Tan Chee Keong --- beacon_node/network/Cargo.toml | 1 - beacon_node/network/src/sync/tests/mod.rs | 4 +-- book/src/contributing_setup.md | 39 +++++++++++------------ common/logging/Cargo.toml | 9 +++--- common/logging/src/lib.rs | 17 +++++----- 5 files changed, 33 insertions(+), 37 deletions(-) diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index 68c77252ab..319ea2b149 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -10,7 +10,6 @@ disable-backfill = [] fork_from_env = ["beacon_chain/fork_from_env"] fake_crypto = ["bls/fake_crypto", "kzg/fake_crypto"] portable = ["beacon_chain/portable"] -test_logger = [] [dependencies] alloy-primitives = { workspace = true } diff --git a/beacon_node/network/src/sync/tests/mod.rs b/beacon_node/network/src/sync/tests/mod.rs index 6e948e4726..8ffe24dda5 100644 --- a/beacon_node/network/src/sync/tests/mod.rs +++ b/beacon_node/network/src/sync/tests/mod.rs @@ -148,13 +148,13 @@ pub fn init_tracing() { INIT_TRACING.call_once(|| { if std::env::var(CI_LOGGER_DIR_ENV_VAR).is_ok() { // Enable logging to log files for each test and each fork. - tracing_subscriber::registry() + let _ = tracing_subscriber::registry() .with( tracing_subscriber::fmt::layer() .with_ansi(false) .with_writer(CILogWriter), ) - .init(); + .try_init(); } }); } diff --git a/book/src/contributing_setup.md b/book/src/contributing_setup.md index e2bda0aa5d..62e590e28f 100644 --- a/book/src/contributing_setup.md +++ b/book/src/contributing_setup.md @@ -109,31 +109,30 @@ For VSCode users, this is already configured in the repository's `.vscode/settin } ``` -### test_logger +### Logging in tests -The test_logger, located in `/common/logging/` can be used to create a `Logger` that by -default returns a NullLogger. But if `--features 'logging/test_logger'` is passed while -testing the logs are displayed. This can be very helpful while debugging tests. - -Example: +By default, when running tests, the logs will not be printed if the tests passed. For example, to run the tests for the `beacon_chain` package: +```bash +cargo test --release -p beacon_chain ``` -$ cargo nextest run -p beacon_chain -E 'test(validator_pubkey_cache::test::basic_operation)' --features 'logging/test_logger' - Finished test [unoptimized + debuginfo] target(s) in 0.20s - Running unittests (target/debug/deps/beacon_chain-975363824f1143bc) -running 1 test -Sep 19 19:23:25.192 INFO Beacon chain initialized, head_slot: 0, head_block: 0x2353…dcf4, head_state: 0xef4b…4615, module: beacon_chain::builder:649 -Sep 19 19:23:25.192 INFO Saved beacon chain to disk, module: beacon_chain::beacon_chain:3608 -Sep 19 19:23:26.798 INFO Beacon chain initialized, head_slot: 0, head_block: 0x2353…dcf4, head_state: 0xef4b…4615, module: beacon_chain::builder:649 -Sep 19 19:23:26.798 INFO Saved beacon chain to disk, module: beacon_chain::beacon_chain:3608 -Sep 19 19:23:28.407 INFO Beacon chain initialized, head_slot: 0, head_block: 0xdcdd…501f, head_state: 0x3055…032c, module: beacon_chain::builder:649 -Sep 19 19:23:28.408 INFO Saved beacon chain to disk, module: beacon_chain::beacon_chain:3608 -Sep 19 19:23:30.069 INFO Beacon chain initialized, head_slot: 0, head_block: 0xa739…1b22, head_state: 0xac1c…eab6, module: beacon_chain::builder:649 -Sep 19 19:23:30.069 INFO Saved beacon chain to disk, module: beacon_chain::beacon_chain:3608 -test validator_pubkey_cache::test::basic_operation ... ok +To always show the logs, run the tests with `-- --nocapture`. -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 51 filtered out; finished in 6.46s +```bash +cargo test --release -p beacon_chain -- --nocapture +``` + +By default, the log shown is `DEBUG` level. This can be overridden using the environment variable `RUST_LOG`. For example, to only show logs with `INFO` level and above: + +```bash +RUST_LOG=info cargo test --release -p beacon_chain -- --nocapture +``` + +To only show logs from the `beacon_chain` crate and with `INFO` level and above: + +```bash +RUST_LOG=beacon_chain=info cargo test --release -p beacon_chain -- --nocapture ``` ### Consensus Spec Tests diff --git a/common/logging/Cargo.toml b/common/logging/Cargo.toml index 1606b8ceb4..6277985b2e 100644 --- a/common/logging/Cargo.toml +++ b/common/logging/Cargo.toml @@ -4,12 +4,11 @@ version = "0.2.0" authors = ["blacktemplar "] edition = { workspace = true } -[features] -# Print log output to stderr when running tests instead of dropping it. -test_logger = [] - [dependencies] -chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } +chrono = { version = "0.4", default-features = false, features = [ + "clock", + "std", +] } logroller = { workspace = true } metrics = { workspace = true } serde = { workspace = true } diff --git a/common/logging/src/lib.rs b/common/logging/src/lib.rs index 8ef3436b06..eb2f096e13 100644 --- a/common/logging/src/lib.rs +++ b/common/logging/src/lib.rs @@ -42,16 +42,15 @@ impl TimeLatch { /// Return a tracing subscriber suitable for test usage. /// -/// By default no logs will be printed, but they can be enabled via -/// the `test_logger` feature. This feature can be enabled for any -/// dependent crate by passing `--features logging/test_logger`, e.g. +/// By default no logs will be printed, logs will be printed by using --nocapture. Example: /// ```bash -/// cargo test -p beacon_chain --features logging/test_logger +/// cargo test --release -p beacon_chain -- --nocapture /// ``` pub fn create_test_tracing_subscriber() { - if cfg!(feature = "test_logger") { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::try_new("debug").unwrap()) - .try_init(); - } + let _ = tracing_subscriber::fmt() + .with_test_writer() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("debug")), + ) + .try_init(); } From 728356ad03e346bbe8a1a6c2bea91fb474f751ea Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 30 Apr 2026 08:43:14 +0200 Subject: [PATCH 3/3] Submit ptc votes that we produce to the ptc op pool (#9231) We are not submitting ptc votes that we produce to our lcoal ptc op pool. So when we are the block producer we don't include our own ptc votes! Co-Authored-By: Eitan Seri-Levi --- beacon_node/http_api/src/beacon/pool.rs | 7 +++++++ beacon_node/http_api/tests/tests.rs | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/beacon_node/http_api/src/beacon/pool.rs b/beacon_node/http_api/src/beacon/pool.rs index c6b8a69643..3525567eb4 100644 --- a/beacon_node/http_api/src/beacon/pool.rs +++ b/beacon_node/http_api/src/beacon/pool.rs @@ -629,6 +629,13 @@ fn publish_payload_attestation_messages( "Payload attestation invalid for fork choice" ); } + + if let Err(e) = chain.add_payload_attestation_to_pool(&verified) { + warn!( + reason = ?e, + "Failed to add payload attestation to pool" + ); + } } Err(PayloadAttestationError::PriorPayloadAttestationMessageKnown { .. }) => { num_already_known += 1; diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 01a77ad4d7..6f8f9c10a5 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -2846,6 +2846,8 @@ impl ApiTester { let message = self.make_valid_payload_attestation_message(0); let fork_name = self.chain.spec.fork_name_at_slot::(message.data.slot); + let pool_count_before = self.chain.op_pool.num_payload_attestation_messages(); + self.client .post_beacon_pool_payload_attestations(&[message], fork_name) .await @@ -2856,6 +2858,12 @@ impl ApiTester { "valid payload attestation should be sent to network" ); + assert_eq!( + self.chain.op_pool.num_payload_attestation_messages(), + pool_count_before + 1, + "payload attestation should be added to op pool" + ); + self } @@ -2863,6 +2871,8 @@ impl ApiTester { let message = self.make_valid_payload_attestation_message(1); let fork_name = self.chain.spec.fork_name_at_slot::(message.data.slot); + let pool_count_before = self.chain.op_pool.num_payload_attestation_messages(); + self.client .post_beacon_pool_payload_attestations_ssz(&[message], fork_name) .await @@ -2873,6 +2883,12 @@ impl ApiTester { "valid payload attestation (SSZ) should be sent to network" ); + assert_eq!( + self.chain.op_pool.num_payload_attestation_messages(), + pool_count_before + 1, + "payload attestation should be added to op pool" + ); + self }