From a55e442595a874bf7f83ee8b1c7a3850beaf708d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 5 Oct 2026 20:15:58 -0300 Subject: [PATCH 1/3] feat(beacon): log a Glamsterdam banner after the first gloas block Marks the fork in the node's own log at the moment it goes live, as ethrex does for amsterdam (lambdaclass/ethrex#7341). The banner follows the import of the chain's first gloas block: the first block gloas applies to while it did not apply to its parent, so missed slots at the start of the fork epoch still leave one. The decision settles on the first gloas block this process imports, banner or not. Blocks import parent-first, so a node that sees the chain's first gloas block sees it before any other, and a node started after the fork would otherwise pay a whole-block parent decode on every import for nothing. The art is a placeholder: a polar bear in sunglasses over the wordmark, plain ASCII so it needs no locale fallback. --- .../blockchain/assets/glamsterdam_banner.txt | 14 ++ crates/blockchain/src/glamsterdam.rs | 176 ++++++++++++++++++ crates/blockchain/src/lib.rs | 16 ++ 3 files changed, 206 insertions(+) create mode 100644 crates/blockchain/assets/glamsterdam_banner.txt create mode 100644 crates/blockchain/src/glamsterdam.rs diff --git a/crates/blockchain/assets/glamsterdam_banner.txt b/crates/blockchain/assets/glamsterdam_banner.txt new file mode 100644 index 00000000..89f6786f --- /dev/null +++ b/crates/blockchain/assets/glamsterdam_banner.txt @@ -0,0 +1,14 @@ + .-. .-. + ( `-._________.-' ) + `. .' + | ____ ____ | + | (____)===(____) | + | | + \ .-. / + `. `-' .' + `-._____.-' + + ___ _ _ __ __ ___ _____ ___ ___ ___ _ __ __ + / __|| | /_\ | \/ |/ __||_ _|| __|| _ \| \ /_\ | \/ | +| (_ || |__ / _ \ | |\/| |\__ \ | | | _| | /| |) | / _ \ | |\/| | + \___||____|/_/ \_\|_| |_||___/ |_| |___||_|_\|___/ /_/ \_\|_| |_| diff --git a/crates/blockchain/src/glamsterdam.rs b/crates/blockchain/src/glamsterdam.rs new file mode 100644 index 00000000..8a482566 --- /dev/null +++ b/crates/blockchain/src/glamsterdam.rs @@ -0,0 +1,176 @@ +//! The Glamsterdam banner, logged once this node imports the chain's first +//! gloas block. +//! +//! The fork goes live with a block, not at a slot: gloas applies from +//! `GLOAS_FORK_EPOCH` on, but the epoch can open with missed slots, so the +//! first gloas block is the first one gloas applies to while it did not apply +//! to its parent. + +use ethlambda_storage::{Chain, Store}; +use ethlambda_types::{ + ShortRoot, + beacon::{config::Config, signing::compute_epoch_at_slot}, + primitives::H256, +}; +use tracing::info; + +const BANNER: &str = include_str!("../assets/glamsterdam_banner.txt"); + +/// Whether this process may still log the banner. +/// +/// Settled by the first gloas block this process imports, whether or not that +/// block is the chain's first. Blocks import parent-first, so a node that +/// imports the chain's first gloas block imports it before any other. One +/// that does not (it started after the fork, or was checkpoint-synced past +/// it) never will, and asking every later import for its parent's slot would +/// cost a whole-block decode per block for the rest of the process's life. +#[derive(Debug)] +pub(crate) struct GlamsterdamBanner { + pending: bool, +} + +impl Default for GlamsterdamBanner { + fn default() -> Self { + Self { pending: true } + } +} + +impl GlamsterdamBanner { + /// Logs the banner if the beacon block just imported at `slot` is the + /// chain's first gloas block. + pub(crate) fn on_block_imported( + &mut self, + store: &Store, + slot: u64, + block_root: H256, + parent_root: H256, + ) { + if !self.pending || store.chain() != Chain::Beacon { + return; + } + let parent_slot = || store.block_entry(&parent_root).map(|(slot, _)| slot); + if self.take_first_gloas_block(&store.config(), slot, parent_slot) { + log(slot, block_root); + } + } + + /// Whether the block at `slot` is the chain's first gloas block, settling + /// the banner on the first gloas block either way. + /// + /// `parent_slot` is called only for that block, since on a beacon store it + /// decodes the whole parent block. `None` (no parent block on record) + /// settles without a banner. + fn take_first_gloas_block( + &mut self, + config: &Config, + slot: u64, + parent_slot: impl FnOnce() -> Option, + ) -> bool { + if !self.pending || !is_gloas(config, slot) { + return false; + } + self.pending = false; + parent_slot().is_some_and(|parent_slot| !is_gloas(config, parent_slot)) + } +} + +/// Whether gloas, or a fork after it, applies at `slot`. +fn is_gloas(config: &Config, slot: u64) -> bool { + config + .fork_at_epoch(compute_epoch_at_slot(slot)) + .has_payload_envelopes() +} + +/// Logs the banner one line at a time, so every line carries the log +/// formatter's prefix instead of only the first. +fn log(slot: u64, block_root: H256) { + info!(""); + for line in BANNER.lines() { + info!("{line}"); + } + info!(""); + let epoch = compute_epoch_at_slot(slot); + info!( + %slot, + epoch, + block_root = %ShortRoot(&block_root.0), + "Glamsterdam is live: imported the first gloas block" + ); +} + +#[cfg(test)] +mod tests { + use super::*; + use ethlambda_types::beacon::{fork::ForkName, preset::SLOTS_PER_EPOCH}; + + const GLOAS_EPOCH: u64 = 5; + const FORK_SLOT: u64 = GLOAS_EPOCH * SLOTS_PER_EPOCH; + + fn gloas_at(epoch: u64) -> Config { + Config::mainnet() + .with_fork_epoch(ForkName::Fulu, 0) + .with_fork_epoch(ForkName::Gloas, epoch) + } + + fn no_parent_read() -> Option { + panic!("the parent's slot was read for a block gloas does not apply to") + } + + #[test] + fn the_block_crossing_the_fork_is_the_first() { + let mut banner = GlamsterdamBanner::default(); + let first = banner + .take_first_gloas_block(&gloas_at(GLOAS_EPOCH), FORK_SLOT, || Some(FORK_SLOT - 1)); + assert!(first); + } + + #[test] + fn missed_slots_at_the_fork_still_leave_a_first_block() { + let mut banner = GlamsterdamBanner::default(); + let slot = FORK_SLOT + SLOTS_PER_EPOCH + 3; + let first = + banner.take_first_gloas_block(&gloas_at(GLOAS_EPOCH), slot, || Some(FORK_SLOT - 2)); + assert!(first); + } + + #[test] + fn blocks_before_the_fork_neither_log_nor_settle() { + let config = gloas_at(GLOAS_EPOCH); + let mut banner = GlamsterdamBanner::default(); + assert!(!banner.take_first_gloas_block(&config, FORK_SLOT - 1, no_parent_read)); + assert!(banner.take_first_gloas_block(&config, FORK_SLOT, || Some(FORK_SLOT - 1))); + } + + #[test] + fn the_banner_is_logged_once() { + let config = gloas_at(GLOAS_EPOCH); + let mut banner = GlamsterdamBanner::default(); + assert!(banner.take_first_gloas_block(&config, FORK_SLOT, || Some(FORK_SLOT - 1))); + // A competing first gloas block, on a fork from the same parent. + assert!(!banner.take_first_gloas_block(&config, FORK_SLOT + 1, no_parent_read)); + } + + #[test] + fn a_node_started_after_the_fork_settles_without_a_banner() { + let config = gloas_at(GLOAS_EPOCH); + let mut banner = GlamsterdamBanner::default(); + let slot = FORK_SLOT + 10; + assert!(!banner.take_first_gloas_block(&config, slot, || Some(slot - 1))); + assert!(!banner.pending); + } + + #[test] + fn a_parent_off_record_settles_without_a_banner() { + let mut banner = GlamsterdamBanner::default(); + assert!(!banner.take_first_gloas_block(&gloas_at(GLOAS_EPOCH), FORK_SLOT, || None)); + assert!(!banner.pending); + } + + #[test] + fn no_first_gloas_block_without_a_scheduled_fork() { + let config = Config::mainnet().with_fork_epoch(ForkName::Fulu, 0); + let mut banner = GlamsterdamBanner::default(); + assert!(!banner.take_first_gloas_block(&config, u64::MAX, no_parent_read)); + assert!(banner.pending); + } +} diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 969420ae..8c2237e9 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -64,6 +64,7 @@ pub mod block_builder; pub(crate) mod coverage; pub mod events; pub(crate) mod fork_choice_tree; +mod glamsterdam; pub mod import_timing; pub mod key_manager; pub mod metrics; @@ -416,6 +417,7 @@ impl BlockChain { column_parking: ColumnParking::default(), envelopes: EnvelopeQueues::default(), beacon_aggregates: Default::default(), + glamsterdam_banner: Default::default(), custody_columns, engine, safe_slots_to_import_optimistically, @@ -568,6 +570,10 @@ pub struct BlockChainServer { /// than in the p2p layer that first sees an aggregate. beacon_aggregates: crate::beacon_aggregates::AggregateGossip, + /// Whether the Glamsterdam banner may still be logged. Beacon-only; see + /// [`crate::glamsterdam`]. + glamsterdam_banner: crate::glamsterdam::GlamsterdamBanner, + /// The lean-only or beacon-only half of this actor's state. See /// [`ChainDuties`]. duties: ChainDuties, @@ -1988,6 +1994,7 @@ impl BlockChainServer { events: EventBus::default(), duties: ChainDuties::Beacon, beacon_aggregates: Default::default(), + glamsterdam_banner: Default::default(), } } @@ -2842,6 +2849,14 @@ impl BlockChainServer { parent_root = %ShortRoot(&parent_root.0), "Block imported successfully" ); + // After the block's own import log, so the banner follows the + // block it marks. + self.glamsterdam_banner.on_block_imported( + &self.store, + slot, + block_root, + parent_root, + ); // Recover per-attestation single-message aggregates from the // block's merged multi-message aggregate and fold them into @@ -4368,6 +4383,7 @@ mod tests { column_parking: ColumnParking::default(), envelopes: EnvelopeQueues::default(), beacon_aggregates: Default::default(), + glamsterdam_banner: Default::default(), custody_columns: Vec::new(), engine: None, safe_slots_to_import_optimistically: constants::SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY, From 6936db1fdab014724385cfc2a0de2914e1afcca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 5 Oct 2026 20:21:08 -0300 Subject: [PATCH 2/3] feat(beacon): draw the Glamsterdam banner as ethlambda's polar bear Replaces the placeholder art with a polar bear in the startup logo's style: block characters over a wordmark plate like the logo's own. The banner is no longer plain ASCII, so a terminal that cannot decode UTF-8 shows it garbled, as it would the logo without its ASCII fallback. --- .../blockchain/assets/glamsterdam_banner.txt | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/crates/blockchain/assets/glamsterdam_banner.txt b/crates/blockchain/assets/glamsterdam_banner.txt index 89f6786f..2ac98807 100644 --- a/crates/blockchain/assets/glamsterdam_banner.txt +++ b/crates/blockchain/assets/glamsterdam_banner.txt @@ -1,14 +1,20 @@ - .-. .-. - ( `-._________.-' ) - `. .' - | ____ ____ | - | (____)===(____) | - | | - \ .-. / - `. `-' .' - `-._____.-' - - ___ _ _ __ __ ___ _____ ___ ___ ___ _ __ __ - / __|| | /_\ | \/ |/ __||_ _|| __|| _ \| \ /_\ | \/ | -| (_ || |__ / _ \ | |\/| |\__ \ | | | _| | /| |) | / _ \ | |\/| | - \___||____|/_/ \_\|_| |_||___/ |_| |___||_|_\|___/ /_/ \_\|_| |_| + ▄ ░ + ▀ + ▀ ▄ + ░ ▄ + ▄▄▄▄▄▄▄▄▄▄ + ▄▄▄▄▄▄▄████████████████▄ + ▄▄ ▄████████████████████████████▀ + ▄▄███████████████████████████████████ + ▄███▄███████████████████████████████████ + ▀▀▀▀▀▀█████████████████████████████████▀ + ██████████▀▀▀▀▀▀▀▀▀▀████████▀ + ██████ ░░░░ ░░░░ ▀██████ + █████ ░░░░ ░░░░ ▀████ + █████ ░░░░ ░░░░ ████▄ + ▄█████▄ ░░░░░ ░░░░░ ▄█████▄ + ▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀ +▄███▀▀▀█▀█████▀▀██▀███▀██▀▀▀█▀▀▀█▀▀▀▀█▀▀▀██▀▀▀███▀▀██▀███▀██▄ +███ █▀▀█ ████ ▀▀ █ ▄▀▄ █▄▀▀███ ██ ▀▀██ ▀▀▄█ ██ █ ▀▀ █ ▄▀▄ ███ +███▄▀▀ █ ▀▀▀█ ██ █ ███ █▀▀▀▄██ ██ ▀▀▀█ █▄▀█ ▀▀▄█ ██ █ ███ ███ + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ From 6674b5d2abe8c5d22fa73ddb5b378e05fe95415d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 5 Oct 2026 20:25:22 -0300 Subject: [PATCH 3/3] feat(beacon): put the ethlambda wordmark on the Glamsterdam banner The polar bear now stands over both plates, ethlambda's above Glamsterdam's, so the banner says which client crossed the fork. --- crates/blockchain/assets/glamsterdam_banner.txt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/blockchain/assets/glamsterdam_banner.txt b/crates/blockchain/assets/glamsterdam_banner.txt index 2ac98807..ad035e5b 100644 --- a/crates/blockchain/assets/glamsterdam_banner.txt +++ b/crates/blockchain/assets/glamsterdam_banner.txt @@ -1,4 +1,4 @@ - ▄ ░ + ▄ ░ ▀ ▀ ▄ ░ ▄ @@ -14,7 +14,10 @@ █████ ░░░░ ░░░░ ████▄ ▄█████▄ ░░░░░ ░░░░░ ▄█████▄ ▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀ -▄███▀▀▀█▀█████▀▀██▀███▀██▀▀▀█▀▀▀█▀▀▀▀█▀▀▀██▀▀▀███▀▀██▀███▀██▄ +▄███████▀▀▀▀█▀▀▀█▀██▀█▀█████▀▀██▀███▀█▀▀▀██▀▀▀███▀▀█████████▄ +████████ ▀▀███ ██ ▀▀ █ ████ ▀▀ █ ▄▀▄ █ ▀▀▄█ ██ █ ▀▀ █████████ +████████ ▀▀▀██ ██ ██ █ ▀▀▀█ ██ █ ███ █ ▀▀▄█ ▀▀▄█ ██ █████████ +████▀▀▀█▀█████▀▀██▀███▀██▀▀▀█▀▀▀█▀▀▀▀█▀▀▀██▀▀▀███▀▀██▀███▀███ ███ █▀▀█ ████ ▀▀ █ ▄▀▄ █▄▀▀███ ██ ▀▀██ ▀▀▄█ ██ █ ▀▀ █ ▄▀▄ ███ ███▄▀▀ █ ▀▀▀█ ██ █ ███ █▀▀▀▄██ ██ ▀▀▀█ █▄▀█ ▀▀▄█ ██ █ ███ ███ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀