From 7885a2e2899fb6c705145200cd56577339ed436e Mon Sep 17 00:00:00 2001 From: owen Date: Fri, 5 Jun 2026 16:07:09 +0100 Subject: [PATCH 1/3] wires in engine api --- .gitignore | 3 +- Cargo.lock | 3 + Cargo.toml | 1 + crates/beacon_state/tile/src/error.rs | 7 + crates/beacon_state/tile/src/fork_choice.rs | 110 ++++++- .../beacon_state/tile/src/state_transition.rs | 3 +- crates/beacon_state/tile/src/tile.rs | 120 +++++++- crates/beacon_state/tile/tests/common.rs | 21 +- crates/bin/Cargo.toml | 1 + crates/bin/src/main.rs | 20 ++ crates/common/src/spine/messages.rs | 24 +- crates/config/src/engine_config.rs | 24 ++ crates/config/src/lib.rs | 9 + crates/e2e/src/utils.rs | 4 + crates/e2e/tests/checkpoint_load.rs | 6 + crates/engine/Cargo.toml | 2 + crates/engine/examples/engine_tile_relay.rs | 86 ------ crates/engine/src/client.rs | 22 +- crates/engine/src/jwt.rs | 2 +- crates/engine/src/req_handlers.rs | 97 ++++--- crates/engine/src/resp_handlers.rs | 35 +-- crates/engine/src/tile.rs | 28 +- crates/engine/src/types.rs | 271 ++++++++++++------ crates/storage/src/tile.rs | 7 + kurtosis/README.md | 52 +++- kurtosis/run-reth.sh | 36 +++ kurtosis/setup.sh | 57 +++- 27 files changed, 742 insertions(+), 309 deletions(-) create mode 100644 crates/config/src/engine_config.rs delete mode 100644 crates/engine/examples/engine_tile_relay.rs create mode 100755 kurtosis/run-reth.sh diff --git a/.gitignore b/.gitignore index 15676165..497efd76 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,5 @@ CLAUDE.md # kurtosis generated files kurtosis/silver-devnet.toml # ethereum consensus-spec-tests -crates/beacon_state/consensus-spec-tests/ \ No newline at end of file +crates/beacon_state/consensus-spec-tests/ +kurtosis/el/ diff --git a/Cargo.lock b/Cargo.lock index 4a327da7..46ddb965 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4329,6 +4329,7 @@ dependencies = [ "silver_config", "silver_control", "silver_discovery", + "silver_engine", "silver_gossip", "silver_network", "silver_peer", @@ -4524,7 +4525,9 @@ dependencies = [ "serde", "sha2", "silver_common", + "silver_config", "simd-json", + "snap 1.1.1", "thiserror 1.0.69", "tracing", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index 780b5704..9d234ef2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,7 @@ silver_network = {path = "crates/network" } silver_peer = {path = "crates/peer" } silver_common_macros = { path = "crates/common_macros" } silver_storage = { path = "crates/storage" } +silver_engine = { path = "crates/engine"} flux = { git = "https://github.com/gattaca-com/flux", rev = "f902769fe25915d8e58b50469f4a8b76f6045870"} flux-utils = { git = "https://github.com/gattaca-com/flux", rev = "f902769fe25915d8e58b50469f4a8b76f6045870", features = ["bytes"]} diff --git a/crates/beacon_state/tile/src/error.rs b/crates/beacon_state/tile/src/error.rs index e95aa40d..39675500 100644 --- a/crates/beacon_state/tile/src/error.rs +++ b/crates/beacon_state/tile/src/error.rs @@ -13,6 +13,12 @@ pub enum PrecheckError { b256_hex(parent_root) )] ParentMissing { parent_root: B256, last_applied_slot: Slot, block_slot: Slot }, + #[error( + "block parent invalid: parent_root=0x{} block_root=0x{}", + b256_hex(parent_root), + b256_hex(block_root) + )] + ParentInvalid { parent_root: B256, block_root: B256 }, #[error("past block: block_epoch={block_epoch} finalized_epoch={finalized_epoch}")] PreFinalized { block_epoch: Epoch, finalized_epoch: Epoch }, #[error("block past-slot precheck failed: block_slot={block_slot} parent_slot={parent_slot}")] @@ -53,6 +59,7 @@ impl PrecheckError { Self::PastSlot { .. } | Self::FutureSlot { .. } | Self::AlreadyKnown { .. } => Feedback::Ignore, + Self::ParentInvalid { block_root, .. } | Self::ProposerLookaheadMismatch { block_root, .. } | Self::ProposerIndexTooBig { block_root, .. } | Self::InvalidBls { block_root, .. } => Feedback::Reject(Some(block_root)), diff --git a/crates/beacon_state/tile/src/fork_choice.rs b/crates/beacon_state/tile/src/fork_choice.rs index fe1fa68a..3b475d9a 100644 --- a/crates/beacon_state/tile/src/fork_choice.rs +++ b/crates/beacon_state/tile/src/fork_choice.rs @@ -1,6 +1,7 @@ use flux::utils::ArrayVec; use silver_beacon_state_data::{B256, Checkpoint, Epoch, Slot, StateId}; use silver_common::metrics::timed; +use tracing::info; // TODO(stalls): ~8 epochs of unpruned mainnet activity hits 256. The May // 2023 incident lasted ~25 epochs. Either grow + paginate the node table or @@ -52,8 +53,8 @@ pub struct ForkChoiceNode { pub justified_checkpoint: Checkpoint, pub finalized_checkpoint: Checkpoint, - /// 0=irrelevant, 1=optimistic, 2=valid, 3=invalid. Wired by EL plumbing. - #[allow(dead_code)] + /// 0=irrelevant, 1=optimistic, 2=valid, 3=invalid. Updated from EL + /// payload verdicts; invalid nodes are excluded from head viability. pub execution_status: u8, /// Per-tier index bundle of this block's post-state. Every node carries a @@ -153,10 +154,6 @@ impl ForkChoice { weight: 0, justified_checkpoint: b.justified, finalized_checkpoint: b.finalized, - // TODO(EL): newly imported blocks are optimistic until - // engine_newPayloadV4 returns VALID/INVALID. Wire EL responses - // back to flip this to 2 (valid) or 3 (invalid) and drop invalid - // descendants from the head choice. execution_status: 1, // optimistic state_id: b.state_id, }); @@ -243,6 +240,104 @@ impl ForkChoice { } } + /// Returns `(head_root, head_exec, safe_exec, finalized_exec)` for + /// `engine_forkchoiceUpdatedV3`. `head_root` is the head's beacon root, + /// echoed by the engine tile so the FCU verdict can be applied here. + /// safe = justified block; zeros when the checkpoint block is absent from + /// the tree. + pub fn fcu_execution_hashes(&self) -> (B256, B256, B256, B256) { + let zero = [0u8; 32]; + let Some(ji) = self.find_node_idx(&self.justified_checkpoint.root) else { + return (zero, zero, zero, zero); + }; + let node = &self.nodes[ji]; + let head_idx = + if node.best_descendant != NULL && self.node_is_viable_for_head(node.best_descendant) { + node.best_descendant + } else { + ji + }; + let fin_exec = self + .find_node_idx(&self.finalized_checkpoint.root) + .map(|i| self.nodes[i].execution_block_hash) + .unwrap_or(zero); + let head = &self.nodes[head_idx]; + (head.block_root, head.execution_block_hash, node.execution_block_hash, fin_exec) + } + + /// EL VALID verdict (newPayload or FCU response): the EL fully validated + /// `block_root`'s payload, which transitively validates every ancestor. + /// Walk up until the first already-valid node. + pub fn on_payload_valid(&mut self, block_root: &B256) { + let Some(mut idx) = self.find_node_idx(block_root) else { + return; + }; + loop { + let n = &mut self.nodes[idx]; + if n.execution_status == 2 { + break; + } + n.execution_status = 2; + if n.parent == NULL { + break; + } + idx = n.parent; + } + } + + /// EL INVALID verdict (newPayload or FCU response): `block_root` and + /// everything above the last valid ancestor (`latest_valid_hash`) is + /// invalid. When the EL gave no usable hash, condemn only `block_root` — + /// optimistic ancestors keep their status. Caller must recompute head + /// afterwards: invalid nodes are filtered by `node_is_viable_for_head` + /// and the next score pass reroutes best_child/best_descendant around + /// them. + pub fn on_payload_invalid(&mut self, block_root: &B256, latest_valid_hash: &B256) { + let Some(head_idx) = self.find_node_idx(block_root) else { + return; + }; + let lvh_idx = if *latest_valid_hash == [0u8; 32] { + None + } else { + self.nodes.iter().position(|n| n.execution_block_hash == *latest_valid_hash) + }; + + info!(?block_root, ?latest_valid_hash, "payload invalid, marking branch"); + + // Ancestor segment: block down to (exclusive) the last valid ancestor. + let mut idx = head_idx; + loop { + if Some(idx) == lvh_idx { + self.nodes[idx].execution_status = 2; + break; + } + let n = &mut self.nodes[idx]; + if n.execution_status == 2 { + break; + } + n.execution_status = 3; + n.best_child = NULL; + n.best_descendant = NULL; + // Unknown ancestor: only `block_root` is provably bad. + if n.parent == NULL || lvh_idx.is_none() { + break; + } + idx = n.parent; + } + + // Descendants of an invalid node are invalid. Parents always precede + // children in `nodes`, so one forward pass suffices. + for i in 0..self.nodes.len() { + let p = self.nodes[i].parent; + if p != NULL && self.nodes[p].execution_status == 3 { + let n = &mut self.nodes[i]; + n.execution_status = 3; + n.best_child = NULL; + n.best_descendant = NULL; + } + } + } + // TODO(perf): O(n_nodes) scan. Hit per on_block (parent lookup), per // find_head (justified root), and per moved validator vote in // compute_deltas (worst case 2M × MAX_FORK_CHOICE_NODES on @@ -295,7 +390,8 @@ impl ForkChoice { // — silver isn't a proposer. Implication: blocks whose post-state // names checkpoints more advanced than ours get filtered, and we // accept only blocks <= our anchor's j/f. Revisit if/when proposing. - n.justified_checkpoint.epoch <= self.justified_checkpoint.epoch && + n.execution_status != 3 && + n.justified_checkpoint.epoch <= self.justified_checkpoint.epoch && n.finalized_checkpoint.epoch <= self.finalized_checkpoint.epoch } diff --git a/crates/beacon_state/tile/src/state_transition.rs b/crates/beacon_state/tile/src/state_transition.rs index 8a2c2087..0dd59aa1 100644 --- a/crates/beacon_state/tile/src/state_transition.rs +++ b/crates/beacon_state/tile/src/state_transition.rs @@ -11,9 +11,8 @@ use silver_beacon_state_data::{ }; use silver_common::{ metrics::timed, - ssz_view, ssz_view::{ - ATTESTATION_DATA_SIZE, AttestationDataView, AttestationView, BEACON_BLOCK_BODY_FIXED, + self, ATTESTATION_DATA_SIZE, AttestationDataView, AttestationView, BEACON_BLOCK_BODY_FIXED, BEACON_BLOCK_HEADER_SIZE, BLOCK_SYNC_AGGREGATE_SIZE, BeaconBlockBodyView, BeaconBlockHeaderView, CONSOLIDATION_REQUEST_SIZE, ConsolidationRequestView, DEPOSIT_CONTRACT_TREE_DEPTH, DEPOSIT_REQUEST_SIZE, DEPOSIT_SIZE, DepositDataView, diff --git a/crates/beacon_state/tile/src/tile.rs b/crates/beacon_state/tile/src/tile.rs index 748a55ec..b2cfe146 100644 --- a/crates/beacon_state/tile/src/tile.rs +++ b/crates/beacon_state/tile/src/tile.rs @@ -11,7 +11,8 @@ use silver_beacon_state_data::{ Version, decode_checkpoint_pubkeys, randao_mix_at_epoch, }; use silver_common::{ - BeaconStateEvent, BlockSource, DataColumnsAvailable, GossipTopic, NewGossipMsg, P2pStreamId, + BeaconStateEvent, BlockSource, DataColumnsAvailable, EngineFcuReq, EngineNewPayloadReq, + EngineReq, EngineResp, GossipTopic, NewGossipMsg, P2pStreamId, PayloadValidationStatus, PeerEvent, RpcInbound, RpcResponse, RpcResponseInbound, RpcSeverity, SilverSpine, SyncUpdate, TCacheRead, TRandomAccess, TRead, hex32, ssz_view::{ @@ -187,6 +188,7 @@ pub struct BeaconStateTile { gossip_consumer: TRandomAccess, rpc_consumer: TRandomAccess, + incoming_engine_resp_consumer: TRandomAccess, } type Producers = ::Producers; @@ -194,12 +196,14 @@ type Producers = ::Producers; impl BeaconStateTile { /// If `checkpoint_state` is non-empty, bootstraps immediately; otherwise /// starts inert in `Mode::Syncing` (call `bootstrap` before the loop). + #[allow(clippy::too_many_arguments)] pub fn new( ticker: SlotTicker, spec: SpecConfig, mut state: BeaconStateOwner, gossip_consumer: TRandomAccess, rpc_consumer: TRandomAccess, + incoming_engine_resp_consumer: TRandomAccess, checkpoint_state: &[u8], decompressed_pubkeys: &[u8], ) -> Self { @@ -244,6 +248,7 @@ impl BeaconStateTile { ), gossip_consumer, rpc_consumer, + incoming_engine_resp_consumer, }; if !checkpoint_state.is_empty() { @@ -288,7 +293,7 @@ impl BeaconStateTile { } pub fn try_apply_block(&mut self, data: &[u8]) -> Feedback { - self.apply_block_impl(data, false) + self.apply_block_impl(data, false, |_block_root| {}) } /// SSZ `hash_tree_root` of the most-recently-applied block's full @@ -579,7 +584,13 @@ impl BeaconStateTile { // Pre-finalization block - either backfill or irrelevant. return Feedback::Ignore; } - let f = self.apply_block_impl(data, true); + let f = self.apply_block_impl(data, true, |root| { + producers.produce(EngineReq::NewPayload(EngineNewPayloadReq { + data: *data_tcache, + block_root: root, + block_source: source, + })); + }); if let Feedback::Reject(Some(block_root)) = f { producers.produce(BeaconStateEvent::BlockRejected { block_root, source }); @@ -597,6 +608,14 @@ impl BeaconStateTile { producers.produce(BeaconStateEvent::PersistBlock { ssz: *data_tcache, source }); + let (head_root, head, safe, fin) = self.fork_choice.fcu_execution_hashes(); + producers.produce(EngineReq::Fcu(EngineFcuReq { + block_root: head_root, + head_block_hash: head, + safe_block_hash: safe, + finalized_block_hash: fin, + })); + f } @@ -920,6 +939,48 @@ impl BeaconStateTile { }); } + /// EL payload verdict, from either newPayload or an FCU response. + fn on_payload_verdict( + &mut self, + block_root: &B256, + latest_valid_hash: &B256, + status: PayloadValidationStatus, + ) { + match status { + PayloadValidationStatus::Valid => { + self.fork_choice.on_payload_valid(block_root); + } + PayloadValidationStatus::Invalid => { + self.fork_choice.on_payload_invalid(block_root, latest_valid_hash); + self.recompute_head(); + } + // Optimistic: EL still syncing; verdict arrives on a later FCU. + PayloadValidationStatus::Syncing | PayloadValidationStatus::Accepted => {} + } + } + + fn handle_engine_response(&mut self, eng_resp: EngineResp, _producers: &mut Producers) { + match eng_resp { + EngineResp::NewPayload(r) => { + self.on_payload_verdict(&r.block_root, &r.latest_valid_hash, r.status); + } + EngineResp::Fcu(r) => { + self.on_payload_verdict(&r.block_root, &r.latest_valid_hash, r.status); + } + // Proposal flow — silver doesn't propose yet, nothing requests + // payloads. + EngineResp::GetPayload(_) => {} + // EL-mempool blob fetch. Belongs to the storage tile (it owns + // column validation/availability), not here; see the TODO at its + // column-request path. + EngineResp::GetBlobs(_) => {} + // Payload-body reconstruction is unneeded: the store persists and + // serves full SignedBeaconBlocks, so there is nothing to rebuild + // from EL bodies. + EngineResp::GetPayloadBodies(_) => {} + } + } + fn handle_gossip( &mut self, m: NewGossipMsg, @@ -1106,7 +1167,12 @@ impl BeaconStateTile { self.sync_finalized_slot.max(self.fork_choice.finalized_checkpoint.epoch * SLOTS_PER_EPOCH) } - fn apply_block_impl(&mut self, data: &[u8], gate_da: bool) -> Feedback { + fn apply_block_impl( + &mut self, + data: &[u8], + gate_da: bool, + mut notify_el: F, + ) -> Feedback { let parsed = match self.precheck_block(data) { Ok(p) => p, Err(err) => { @@ -1124,6 +1190,12 @@ impl BeaconStateTile { return Feedback::AwaitData(parsed.block_root); } + // Fire as early as possible so the EL round-trip overlaps with the + // state transition — mirrors Lighthouse's async payload notification. + // After the DA gate, so a block parked on AwaitData doesn't re-send + // newPayload on every retry. + notify_el(parsed.block_root); + let block_epoch = parsed.block_slot / SLOTS_PER_EPOCH; // Per-block attester shuffling against the parent post-state (active @@ -1173,14 +1245,16 @@ impl BeaconStateTile { Ok((epoch_idx, longtail_idx)) => { let es = epoch.view_opt(epoch_idx).state(); let checkpoints = (es.current_justified_checkpoint, es.finalized_checkpoint); - Ok((view.commit(epoch_idx, longtail_idx), checkpoints)) + let execution_block_hash = + view.slot.state().latest_execution_payload_header.block_hash; + Ok((view.commit(epoch_idx, longtail_idx), checkpoints, execution_block_hash)) } Err(e) => Err(e), }; // `view`'s &mut self.state borrow ends here (`commit`/`drop` consumed // it); the fork-choice + publish work below needs &mut self. - let (new_id, (justified, finalized)) = match outcome { + let (new_id, (justified, finalized), execution_block_hash) = match outcome { Ok(committed) => committed, Err(e) => { tracing::error!( @@ -1193,7 +1267,7 @@ impl BeaconStateTile { } }; - self.publish_applied_block(&parsed, new_id, justified, finalized); + self.publish_applied_block(&parsed, new_id, justified, finalized, execution_block_hash); Feedback::Accept(Some(parsed.block_root)) } @@ -1203,6 +1277,7 @@ impl BeaconStateTile { new_id: StateId, justified: Checkpoint, finalized: Checkpoint, + execution_block_hash: [u8; 32], ) { // Fold block-included attestations into the LMD vote tracker. for i in 0..self.attestation_votes_scratch.len() { @@ -1210,14 +1285,12 @@ impl BeaconStateTile { self.on_attestation(v.validator as usize, v.block_root, v.target_epoch); } - // TODO(EL): pass the execution payload block hash to fork choice and - // drive engine_forkchoiceUpdatedV3 once the EL path is wired. self.fork_choice.on_block(BlockImport { slot: parsed.block_slot, block_root: parsed.block_root, parent_root: parsed.parent_root, state_root: parsed.state_root, - execution_block_hash: [0u8; 32], + execution_block_hash, justified, finalized, state_id: new_id, @@ -1276,6 +1349,11 @@ impl BeaconStateTile { return Err(PrecheckError::ParentMissing { parent_root, last_applied_slot, block_slot }); }; let parent_node = self.fork_choice.node(parent_idx); + // EL declared the parent invalid — descendants are invalid by + // definition. Reject before the COW/EL round-trip. + if parent_node.execution_status == 3 { + return Err(PrecheckError::ParentInvalid { parent_root, block_root }); + } let parent_state_id = parent_node.state_id; // Immutable read view of the parent post-state. @@ -1810,6 +1888,11 @@ impl Tile for BeaconStateTile { adapter.consume(|m: DataColumnsAvailable, producers| { self.handle_data_columns_available(m, producers); }); + + adapter.consume(|eng_resp: EngineResp, producers| { + self.handle_engine_response(eng_resp, producers); + }); + self.incoming_engine_resp_consumer.free(); } } @@ -1942,10 +2025,21 @@ mod tests { let ticker = SlotTicker::new(genesis, Duration::from_secs(12), Duration::from_secs(4)); let gossip_p = TCache::producer("test_gossip", 1 << 20); let event_p = TCache::producer("test_event", 1 << 20); + let engine_p = TCache::producer("test_engine", 1 << 20); let gossip_c = gossip_p.cache_ref().random_access("test_gossip", true).unwrap(); let rpc_c = event_p.cache_ref().random_access("test_event", true).unwrap(); + let engine_c = engine_p.cache_ref().random_access("test_engine", true).unwrap(); let state = BeaconStateOwner::pre_bootstrap(); - BeaconStateTile::new(ticker, SpecConfig::mainnet(), state, gossip_c, rpc_c, &[], &[]) + BeaconStateTile::new( + ticker, + SpecConfig::mainnet(), + state, + gossip_c, + rpc_c, + engine_c, + &[], + &[], + ) } fn placeholder_pubkey(i: usize) -> BLSPubkey { @@ -2161,7 +2255,7 @@ mod tests { let head_before = tile.last_applied; let nodes_before = tile.fork_choice.nodes.len(); - tile.apply_block_impl(&buf, true); + tile.apply_block_impl(&buf, true, |_block_root| {}); assert_eq!(tile.last_applied, head_before, "head must be unchanged"); assert_eq!(tile.fork_choice.nodes.len(), nodes_before, "no node added"); @@ -2380,7 +2474,7 @@ mod tests { buf[108..116].copy_from_slice(&0u64.to_le_bytes()); // proposer_index buf[116..148].copy_from_slice(&parent_root); // parent_root - tile.apply_block_impl(&buf, true); + tile.apply_block_impl(&buf, true, |_block_root| {}); assert_eq!(tile.fork_choice.nodes.len(), 1); } diff --git a/crates/beacon_state/tile/tests/common.rs b/crates/beacon_state/tile/tests/common.rs index aaf4fd9c..10df593f 100644 --- a/crates/beacon_state/tile/tests/common.rs +++ b/crates/beacon_state/tile/tests/common.rs @@ -124,15 +124,24 @@ impl OutboundKind { impl Harness { pub fn new(wall_slot: u64, checkpoint_ssz: &[u8]) -> Self { - Self::build(wall_slot, |ticker, gc, rc| { + Self::build(wall_slot, |ticker, gc, rc, ec| { let state = BeaconStateOwner::pre_bootstrap(); - BeaconStateTile::new(ticker, SpecConfig::mainnet(), state, gc, rc, checkpoint_ssz, &[]) + BeaconStateTile::new( + ticker, + SpecConfig::mainnet(), + state, + gc, + rc, + ec, + checkpoint_ssz, + &[], + ) }) } fn build(wall_slot: u64, build_tile: F) -> Self where - F: FnOnce(SlotTicker, TRandomAccess, TRandomAccess) -> BeaconStateTile, + F: FnOnce(SlotTicker, TRandomAccess, TRandomAccess, TRandomAccess) -> BeaconStateTile, { static SEQ: AtomicU64 = AtomicU64::new(0); let seq = SEQ.fetch_add(1, Ordering::Relaxed); @@ -154,11 +163,13 @@ impl Harness { let gossip_in_producer = TCache::producer("gossip_in", 1 << 24); let rpc_in_producer = TCache::producer("rpc_in", 1 << 24); + let engine_resp_producer = TCache::producer("engine_resp", 1 << 24); let gossip_consumer = gossip_in_producer.cache_ref().random_access("test", true).expect("gossip ra"); let rpc_consumer = rpc_in_producer.cache_ref().random_access("test", true).expect("rpc ra"); - - let tile = build_tile(ticker, gossip_consumer, rpc_consumer); + let engine_resp_consumer = + engine_resp_producer.cache_ref().random_access("test", true).expect("engine resp ra"); + let tile = build_tile(ticker, gossip_consumer, rpc_consumer, engine_resp_consumer); // Order matters: attach tile first so its tile_id stays 0 for the // real consumer of `inbound`; Injector gets tile_id 1. diff --git a/crates/bin/Cargo.toml b/crates/bin/Cargo.toml index 65e93075..7a8f9450 100644 --- a/crates/bin/Cargo.toml +++ b/crates/bin/Cargo.toml @@ -16,6 +16,7 @@ silver_gossip.workspace = true silver_network.workspace = true silver_peer.workspace = true silver_storage.workspace = true +silver_engine.workspace = true clap.workspace = true flux.workspace = true diff --git a/crates/bin/src/main.rs b/crates/bin/src/main.rs index de3a80af..b90a8fbe 100644 --- a/crates/bin/src/main.rs +++ b/crates/bin/src/main.rs @@ -12,6 +12,7 @@ use silver_common::{Enr, ProtoIdentify, SilverSpine, TCache, TCacheProducer}; use silver_config::Config; use silver_control::Controller; use silver_discovery::{DiscV5, Discovery}; +use silver_engine::EngineTile; use silver_gossip::GossipHandler; use silver_network::{Context, NetworkTile, P2p}; use silver_peer::PeerManager; @@ -80,6 +81,8 @@ fn main() -> Result<(), Box> { ssz_gossip_producer.cache_ref().random_access("ds_ssz_gossip", true)?; let ssz_persist_gossip_consumer_ds = ssz_gossip_producer.cache_ref().random_access("ds_persist_ssz_gossip", true)?; + let ssz_gossip_consumer_eng = + ssz_gossip_producer.cache_ref().random_access("eng_ssz_gossip", true)?; let outgoing_gossip_producer = TCache::producer("outgoing_gossip", config.outgoing_gossip_tcache_size()); let incoming_rpc_producer = TCache::producer("incoming_rpc", config.incoming_rpc_tcache_size()); @@ -89,6 +92,14 @@ fn main() -> Result<(), Box> { incoming_rpc_producer.cache_ref().random_access("ds_incoming_rpc", true)?; let persist_rpc_consumer_ds = incoming_rpc_producer.cache_ref().random_access("ds_persist_incoming_rpc", true)?; + let incoming_rpc_consumer_eng = + incoming_rpc_producer.cache_ref().random_access("eng_incoming_rpc", true)?; + let incoming_engine_resp_producer = TCache::producer( + "incoming_engine_resp", + config.engine_config().incoming_engine_resp_tcache_size, + ); + let incoming_engine_resp_consumer = + incoming_engine_resp_producer.cache_ref().random_access("engine_incoming_resp", true)?; // rpc producer let outgoing_rpc_producer = @@ -203,6 +214,7 @@ fn main() -> Result<(), Box> { state, ssz_gossip_consumer, incoming_rpc_consumer, + incoming_engine_resp_consumer, &checkpoint, &checkpoint_pubkeys, ); @@ -224,6 +236,13 @@ fn main() -> Result<(), Box> { config.data_storage_dir().into(), ); + let engine_tile = EngineTile::new( + config.engine_config(), + ssz_gossip_consumer_eng, + incoming_rpc_consumer_eng, + incoming_engine_resp_producer, + ); + // Spine let spine = SilverSpine::new(None); // TODO panic handler @@ -234,6 +253,7 @@ fn main() -> Result<(), Box> { attach_tile(network_tile, scoped_spine, TileConfig::new(3, ThreadPriority::OSDefault)); attach_tile(beacon_state_tile, scoped_spine, TileConfig::new(4, ThreadPriority::OSDefault)); attach_tile(storage_tile, scoped_spine, TileConfig::new(5, ThreadPriority::OSDefault)); + attach_tile(engine_tile, scoped_spine, TileConfig::new(6, ThreadPriority::OSDefault)); }); Ok(()) diff --git a/crates/common/src/spine/messages.rs b/crates/common/src/spine/messages.rs index 5f77f7ca..4eb5c507 100644 --- a/crates/common/src/spine/messages.rs +++ b/crates/common/src/spine/messages.rs @@ -651,13 +651,13 @@ pub struct WithdrawalInline { /// `engine_forkchoiceUpdatedV3` request. Fully inline — no TCache needed. /// -/// When `has_attrs` is false the `attrs_*` fields are ignored. -/// `attrs_withdrawal_count` gives the number of valid entries in -/// `attrs_withdrawals`; the remainder are zero-filled. +/// `block_root` is the beacon root of the `head_block_hash` block. The EL +/// response carries no beacon identity, so the engine tile echoes it in +/// `EngineFcuResp` to let fork choice apply the verdict. #[derive(Clone, Copy, Debug)] #[repr(C)] pub struct EngineFcuReq { - pub id: u64, + pub block_root: [u8; 32], pub head_block_hash: [u8; 32], pub safe_block_hash: [u8; 32], pub finalized_block_hash: [u8; 32], @@ -675,21 +675,21 @@ pub struct EngineFcuReq { #[derive(Clone, Copy, Debug)] #[repr(C)] pub struct EngineNewPayloadReq { - pub id: u64, pub data: TCacheRead, - pub parent_beacon_block_root: [u8; 32], - pub versioned_hash_count: u8, - pub versioned_hashes: [[u8; 32]; MAX_BLOBS_PER_BLOCK], + pub block_root: [u8; 32], + pub block_source: BlockSource, } /// Response to `engine_forkchoiceUpdatedV3`. Fully inline. /// -/// `latest_valid_hash` is all-zeros when the EL did not return one. -/// `payload_id` is meaningful only when `has_payload_id` is true. +/// `block_root` echoes the request's head beacon root (zeros for the +/// prepare-payload path). `latest_valid_hash` is all-zeros when the EL did +/// not return one. `payload_id` is meaningful only when `has_payload_id` is +/// true. #[derive(Clone, Copy, Debug)] #[repr(C)] pub struct EngineFcuResp { - pub id: u64, + pub block_root: [u8; 32], pub status: PayloadValidationStatus, pub latest_valid_hash: [u8; 32], pub has_payload_id: bool, @@ -702,7 +702,7 @@ pub struct EngineFcuResp { #[derive(Clone, Copy, Debug)] #[repr(C)] pub struct EngineNewPayloadResp { - pub id: u64, + pub block_root: [u8; 32], pub status: PayloadValidationStatus, pub latest_valid_hash: [u8; 32], } diff --git a/crates/config/src/engine_config.rs b/crates/config/src/engine_config.rs new file mode 100644 index 00000000..ab45a0b3 --- /dev/null +++ b/crates/config/src/engine_config.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; + +fn default_tcache_size() -> usize { + 2 << 24 +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct EngineConfig { + pub execution_endpoint: String, + /// Path to the EL's hex-encoded JWT secret file. + pub jwt_secret: String, + #[serde(default = "default_tcache_size")] + pub incoming_engine_resp_tcache_size: usize, +} + +impl Default for EngineConfig { + fn default() -> Self { + Self { + execution_endpoint: "http://localhost:8551".into(), + jwt_secret: "0".into(), + incoming_engine_resp_tcache_size: 2 << 24, + } + } +} diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 16af1c5c..2c772c31 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1,6 +1,7 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; pub use discovery_config::DiscoveryConfig; +pub use engine_config::EngineConfig; pub use peer_score_params::ScoreParams; use secp256k1::PublicKey; use serde::{Deserialize, Serialize}; @@ -11,6 +12,7 @@ use crate::chain_config::ChainConfig; mod chain_config; mod discovery_config; +mod engine_config; mod peer_score_params; mod syncing_config; @@ -106,6 +108,8 @@ pub struct Config { outgoing_rpc_tcache_size: usize, #[serde(default = "default_data_dir")] data_storage_dir: String, + #[serde(default)] + engine_config: EngineConfig, } impl Config { @@ -137,6 +141,7 @@ impl Config { incoming_rpc_tcache_size: 2 << 27, // ssz outgoing_rpc_tcache_size: 2 << 24, // ssz data_storage_dir: default_data_dir(), + engine_config: Default::default(), } } @@ -300,6 +305,10 @@ impl Config { pub fn data_storage_dir(&self) -> &str { &self.data_storage_dir } + + pub fn engine_config(&self) -> EngineConfig { + self.engine_config.clone() + } } #[cfg(test)] diff --git a/crates/e2e/src/utils.rs b/crates/e2e/src/utils.rs index 3a85555e..42437f28 100644 --- a/crates/e2e/src/utils.rs +++ b/crates/e2e/src/utils.rs @@ -131,8 +131,11 @@ impl PmBsHarness { let gossip_p = TCache::producer("gossip_in", 1 << 20); let rpc_cap = (n_blocks * 300 * 1024).next_power_of_two().max(1 << 22); let rpc_p = TCache::producer("rpc_in", rpc_cap); + let engine_resp_p = TCache::producer("engine_resp", 1 << 24); let gossip_c = gossip_p.cache_ref().random_access("test", true).expect("gossip ra"); let rpc_c = rpc_p.cache_ref().random_access("test", true).expect("rpc ra"); + let engine_resp_c = + engine_resp_p.cache_ref().random_access("test", true).expect("engine resp ra"); let state = BeaconStateOwner::pre_bootstrap(); let mut bs = BeaconStateTile::new( @@ -141,6 +144,7 @@ impl PmBsHarness { state, gossip_c, rpc_c, + engine_resp_c, checkpoint, &[], ); diff --git a/crates/e2e/tests/checkpoint_load.rs b/crates/e2e/tests/checkpoint_load.rs index ccc01b09..92d2f83f 100644 --- a/crates/e2e/tests/checkpoint_load.rs +++ b/crates/e2e/tests/checkpoint_load.rs @@ -153,8 +153,10 @@ fn finalized_state_loads() { let ticker = SlotTicker::new(genesis_time, Duration::from_secs(12), Duration::from_secs(4)); let gossip_p = TCache::producer("gossip_in", 1 << 20); let rpc_p = TCache::producer("rpc_in", 1 << 20); + let engine_resp_p = TCache::producer("engine_resp", 1 << 24); let gossip_c = gossip_p.cache_ref().random_access("test", false).unwrap(); let rpc_c = rpc_p.cache_ref().random_access("test", false).unwrap(); + let engine_resp_c = engine_resp_p.cache_ref().random_access("test", false).unwrap(); let state = BeaconStateOwner::pre_bootstrap(); let mut tile = BeaconStateTile::new( @@ -163,6 +165,7 @@ fn finalized_state_loads() { state, gossip_c, rpc_c, + engine_resp_c, &ssz, &[], ); @@ -327,8 +330,10 @@ fn tile_apply_block_ef_fixture() { ); let gossip_p = TCache::producer("gossip_ef", 1 << 20); let rpc_p = TCache::producer("rpc_ef", 1 << 20); + let engine_resp_p = TCache::producer("engine_resp", 1 << 24); let gossip_c = gossip_p.cache_ref().random_access("test", false).unwrap(); let rpc_c = rpc_p.cache_ref().random_access("test", false).unwrap(); + let engine_resp_c = engine_resp_p.cache_ref().random_access("test", false).unwrap(); let state = BeaconStateOwner::pre_bootstrap(); let mut tile = BeaconStateTile::new( @@ -337,6 +342,7 @@ fn tile_apply_block_ef_fixture() { state, gossip_c, rpc_c, + engine_resp_c, &pre_ssz, &[], ); diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 22a1d378..18100270 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -6,6 +6,7 @@ rust-version.workspace = true version.workspace = true [dependencies] +silver_config.workspace = true base64.workspace = true flux.workspace = true hex.workspace = true @@ -19,6 +20,7 @@ sha2.workspace = true silver_common.workspace = true thiserror.workspace = true tracing.workspace = true +snap = "1.1.1" [dev-dependencies] tracing-subscriber.workspace = true diff --git a/crates/engine/examples/engine_tile_relay.rs b/crates/engine/examples/engine_tile_relay.rs deleted file mode 100644 index 83f7a3f1..00000000 --- a/crates/engine/examples/engine_tile_relay.rs +++ /dev/null @@ -1,86 +0,0 @@ -// Run the engine tile standalone against a local EL node. -// -// A separate feeder process can write EngineReq messages over the shared -// memory spine and large SSZ payloads into the shmem TCache segments. -// -// Usage: -// cargo run -p silver_engine --example engine_tile_relay -- \ -// --endpoint http://127.0.0.1:8551 \ -// --jwt-secret /path/to/jwtsecret -// -// Press Ctrl-C to stop. - -use std::sync::atomic::Ordering; - -use flux::tile::{TileConfig, attach_tile}; -use silver_common::{SilverSpine, TCache}; -use silver_engine::{EngineClient, EngineTile, JwtSecret}; -use tracing_subscriber::EnvFilter; - -const ENGINE_TCACHE_SIZE: usize = 1 << 24; // 16 MiB - -const REQ_SHMEM_NAME: &str = "/silver-engine-req"; -const RESP_SHMEM_NAME: &str = "/silver-engine-resp"; - -fn main() { - tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::INFO.into())) - .init(); - - let (endpoint, jwt) = parse_args(); - tracing::info!(%endpoint, "starting engine tile"); - - // Shmem-backed TCache pair. The req segment is written by the feeder - // process; the resp segment is written here and read by the feeder. - let req_consumer = - TCache::shm_consumer(REQ_SHMEM_NAME, ENGINE_TCACHE_SIZE).expect("req shmem consumer"); - let resp_producer = TCache::shm_producer(RESP_SHMEM_NAME, ENGINE_TCACHE_SIZE); - - let tile = EngineTile::new(EngineClient::new(endpoint, jwt), req_consumer, resp_producer); - - let spine = SilverSpine::new(None); - spine.start(None, None, |scoped| { - let stop = std::sync::Arc::clone(&scoped.stop_flag); - attach_tile(tile, scoped, TileConfig::background(None, None)); - - while stop.load(Ordering::Relaxed) == 0 { - std::thread::sleep(std::time::Duration::from_millis(100)); - } - }); -} - -fn parse_args() -> (String, JwtSecret) { - let args: Vec = std::env::args().collect(); - let mut endpoint = "http://127.0.0.1:8551".to_string(); - let mut jwt_arg: Option = None; - - let mut i = 1; - while i < args.len() { - match args[i].as_str() { - "--endpoint" => { - i += 1; - endpoint = args[i].clone(); - } - "--jwt-secret" => { - i += 1; - jwt_arg = Some(args[i].clone()); - } - _ => {} - } - i += 1; - } - - let jwt = match jwt_arg { - Some(s) if std::path::Path::new(&s).exists() => { - JwtSecret::from_file(std::path::Path::new(&s)) - .unwrap_or_else(|e| panic!("invalid jwt file {s}: {e}")) - } - Some(s) => JwtSecret::from_hex(&s).unwrap_or_else(|e| panic!("invalid jwt hex: {e}")), - _ => { - eprintln!("--jwt-secret required"); - std::process::exit(1); - } - }; - - (endpoint, jwt) -} diff --git a/crates/engine/src/client.rs b/crates/engine/src/client.rs index f0818c47..dcffe128 100644 --- a/crates/engine/src/client.rs +++ b/crates/engine/src/client.rs @@ -2,12 +2,13 @@ use std::time::Duration; use mio::{Events, Poll}; use rustc_hash::FxHashMap; +use silver_common::ssz_hash::B256; use crate::{ EngineError, JwtSecret, http::{HttpPool, http_pool_enqueue, poll_http_pool}, ipc::{IpcPool, ipc_pool_enqueue, poll_ipc_pool}, - types::{B256, ForkchoiceState, PayloadAttributesV3, write_new_payload_params}, + types::{ForkchoiceState, PayloadAttributesV3, write_new_payload_params}, }; const EVENTS_CAPACITY: usize = 16; @@ -32,8 +33,8 @@ pub enum ReqKind { Capabilities, ClientVersion, Syncing, - Fcu(u64), - NewPayload(u64), + Fcu(B256), // head beacon block root, zeros for prepare-payload + NewPayload(B256), // block root GetPayloadFetch(u64), GetBlobs(u64), GetPayloadBodiesByHash(u64), @@ -56,7 +57,8 @@ pub struct EngineClient { } impl EngineClient { - pub fn new(endpoint: impl Into, jwt: JwtSecret) -> Self { + pub fn new(endpoint: impl Into, jwt: &str) -> Self { + let jwt = JwtSecret::from_file(jwt).expect("invalid JWT secret"); Self { transport: Transport::Http(HttpPool::new(endpoint.into(), jwt)), poll: Poll::new().expect("mio Poll::new failed"), @@ -116,28 +118,26 @@ fn enqueue(c: &mut EngineClient, rpc_id: u64, body: &simd_json::OwnedValue) { pub fn send_fcu( c: &mut EngineClient, + block_root: B256, state: ForkchoiceState, attrs: Option, - req_id: u64, ) { let (id, body) = make_rpc_body(&mut c.id, "engine_forkchoiceUpdatedV3", simd_json::json!([state, attrs])); enqueue(c, id, &body); - c.pending_requests.insert(id, ReqKind::Fcu(req_id)); + c.pending_requests.insert(id, ReqKind::Fcu(block_root)); } pub fn send_new_payload( c: &mut EngineClient, data: &[u8], - versioned_hashes: &[B256], - parent_beacon_block_root: &B256, - req_id: u64, + block_root: [u8; 32], ) -> Result<(), EngineError> { let rpc_id = next_id(&mut c.id); c.scratch.clear(); c.scratch .extend_from_slice(b"{\"jsonrpc\":\"2.0\",\"method\":\"engine_newPayloadV4\",\"params\":"); - write_new_payload_params(data, versioned_hashes, parent_beacon_block_root, &mut c.scratch)?; + write_new_payload_params(data, &mut c.scratch)?; c.scratch.extend_from_slice(b",\"id\":"); append_decimal_u64(rpc_id, &mut c.scratch); c.scratch.push(b'}'); @@ -145,7 +145,7 @@ pub fn send_new_payload( Transport::Http(p) => http_pool_enqueue(p, rpc_id, &c.scratch, &mut c.poll), Transport::Ipc(p) => ipc_pool_enqueue(p, rpc_id, &c.scratch, &mut c.poll), } - c.pending_requests.insert(rpc_id, ReqKind::NewPayload(req_id)); + c.pending_requests.insert(rpc_id, ReqKind::NewPayload(block_root)); Ok(()) } diff --git a/crates/engine/src/jwt.rs b/crates/engine/src/jwt.rs index f2a57858..7c830435 100644 --- a/crates/engine/src/jwt.rs +++ b/crates/engine/src/jwt.rs @@ -26,7 +26,7 @@ impl JwtSecret { Ok(Self { secret: arr, cached_iat: 0, cached_token: String::new() }) } - pub fn from_file(path: &std::path::Path) -> Result { + pub fn from_file(path: &str) -> Result { let s = std::fs::read_to_string(path).map_err(|e| EngineError::Jwt(e.to_string()))?; Self::from_hex(s.trim()) } diff --git a/crates/engine/src/req_handlers.rs b/crates/engine/src/req_handlers.rs index 1512a9df..af77aeaa 100644 --- a/crates/engine/src/req_handlers.rs +++ b/crates/engine/src/req_handlers.rs @@ -1,6 +1,6 @@ use flux::spine::FluxSpine; use silver_common::{ - EngineFcuReq, EngineGetBlobsReq, EngineGetPayloadBodiesByHashReq, + BlockSource, EngineFcuReq, EngineGetBlobsReq, EngineGetPayloadBodiesByHashReq, EngineGetPayloadBodiesByRangeReq, EngineGetPayloadReq, EngineNewPayloadReq, EngineNewPayloadResp, EnginePreparePayloadReq, EngineReq, EngineResp, PayloadValidationStatus, SilverSpine, TRandomAccess, @@ -18,13 +18,16 @@ use crate::{ #[inline] pub(crate) fn handle_request( client: &mut EngineClient, - req_consumer: &mut TRandomAccess, + gossip_consumer: &mut TRandomAccess, + rpc_consumer: &mut TRandomAccess, req: &EngineReq, producers: &mut ::Producers, ) { match req { EngineReq::Fcu(r) => handle_fcu(client, r), - EngineReq::NewPayload(r) => handle_new_payload(client, req_consumer, r, producers), + EngineReq::NewPayload(r) => { + handle_new_payload(client, gossip_consumer, rpc_consumer, r, producers) + } EngineReq::PreparePayload(r) => handle_prepare_payload(client, *r), EngineReq::GetPayload(r) => handle_get_payload(client, *r), EngineReq::GetBlobs(r) => handle_get_blobs(client, r), @@ -35,46 +38,67 @@ pub(crate) fn handle_request( #[inline] fn handle_fcu(client: &mut EngineClient, r: &EngineFcuReq) { - tracing::info!(head = %hex::encode(&r.head_block_hash[..4]), id = r.id, "FCU ← spine"); + tracing::info!(head = %hex::encode(&r.head_block_hash[..4]), "FCU ← spine"); let state = ForkchoiceState { head_block_hash: r.head_block_hash, safe_block_hash: r.safe_block_hash, finalized_block_hash: r.finalized_block_hash, }; - send_fcu(client, state, None, r.id); + send_fcu(client, r.block_root, state, None); } #[inline] fn handle_new_payload( client: &mut EngineClient, - req_consumer: &mut TRandomAccess, + gossip_consumer: &mut TRandomAccess, + rpc_consumer: &mut TRandomAccess, r: &EngineNewPayloadReq, producers: &mut ::Producers, ) { - let acquired = req_consumer.acquire(r.data); - let bytes = match acquired.buffer() { - Ok((b, _)) => b, - Err(e) => { - tracing::warn!("failed to read payload data: {e}"); - producers - .engine_resps - .produce(&EngineResp::NewPayload(invalid_new_payload_resp(r.id)).into()); - return; - } - }; + match r.block_source { + BlockSource::Gossip => { + let acquired = gossip_consumer.acquire(r.data); + let bytes = match acquired.buffer() { + Ok((b, _)) => b, + Err(e) => { + tracing::warn!("failed to read payload data: {e}"); + producers.engine_resps.produce( + &EngineResp::NewPayload(invalid_new_payload_resp(r.block_root)).into(), + ); + return; + } + }; - let n = r.versioned_hash_count as usize; - if let Err(e) = - send_new_payload(client, bytes, &r.versioned_hashes[..n], &r.parent_beacon_block_root, r.id) - { - tracing::warn!("failed to encode payload: {e}"); - producers - .engine_resps - .produce(&EngineResp::NewPayload(invalid_new_payload_resp(r.id)).into()); - } + if let Err(e) = send_new_payload(client, bytes, r.block_root) { + tracing::warn!("failed to encode payload: {e}"); + producers.engine_resps.produce( + &EngineResp::NewPayload(invalid_new_payload_resp(r.block_root)).into(), + ); + } + gossip_consumer.free(); + } + BlockSource::Rpc => { + let acquired = rpc_consumer.acquire(r.data); + let bytes = match acquired.buffer() { + Ok((b, _)) => b, + Err(e) => { + tracing::warn!("failed to read payload data: {e}"); + producers.engine_resps.produce( + &EngineResp::NewPayload(invalid_new_payload_resp(r.block_root)).into(), + ); + return; + } + }; - drop(acquired); - req_consumer.free(); + if let Err(e) = send_new_payload(client, bytes, r.block_root) { + tracing::warn!("failed to encode payload: {e}"); + producers.engine_resps.produce( + &EngineResp::NewPayload(invalid_new_payload_resp(r.block_root)).into(), + ); + } + rpc_consumer.free(); + } + }; } #[inline] @@ -131,7 +155,8 @@ fn handle_prepare_payload(client: &mut EngineClient, r: EnginePreparePayloadReq) withdrawals, parent_beacon_block_root: r.attrs_parent_beacon_block_root, }); - send_fcu(client, state, attrs, r.id); + // Proposal path correlates on payload_id, not the head verdict. + send_fcu(client, [0u8; 32], state, attrs); } #[inline] @@ -141,11 +166,11 @@ fn handle_get_payload(client: &mut EngineClient, r: EngineGetPayloadReq) { } #[inline] -fn invalid_new_payload_resp(id: u64) -> EngineNewPayloadResp { +fn invalid_new_payload_resp(block_root: [u8; 32]) -> EngineNewPayloadResp { // Internal error (TCache read/decode failure) — use SYNCING, not INVALID. // INVALID tells the CL the block is definitively bad; we don't know that here. EngineNewPayloadResp { - id, + block_root, status: PayloadValidationStatus::Syncing, latest_valid_hash: [0u8; 32], } @@ -157,16 +182,8 @@ mod tests { #[test] fn invalid_new_payload_resp_fields() { - let resp = invalid_new_payload_resp(99); - assert_eq!(resp.id, 99); + let resp = invalid_new_payload_resp([0u8; 32]); assert_eq!(resp.status, PayloadValidationStatus::Syncing); assert_eq!(resp.latest_valid_hash, [0u8; 32]); } - - #[test] - fn invalid_new_payload_resp_preserves_id() { - for id in [0, 1, u64::MAX] { - assert_eq!(invalid_new_payload_resp(id).id, id); - } - } } diff --git a/crates/engine/src/resp_handlers.rs b/crates/engine/src/resp_handlers.rs index c46210a3..9639725c 100644 --- a/crates/engine/src/resp_handlers.rs +++ b/crates/engine/src/resp_handlers.rs @@ -158,7 +158,7 @@ pub(crate) fn handle_sync_response( #[inline] pub(crate) fn handle_fcu_response( - spine_id: u64, + block_root: [u8; 32], response: Result<&mut [u8], EngineError>, adapter: &mut SpineAdapter, ) { @@ -166,7 +166,7 @@ pub(crate) fn handle_fcu_response( let raw = match response { Err(e) => { tracing::warn!("forkchoiceUpdated error: {e}"); - break 'parse fcu_error(spine_id); + break 'parse fcu_error(block_root); } Ok(b) => b, }; @@ -183,24 +183,17 @@ pub(crate) fn handle_fcu_response( latest_valid_hash = %r.payload_status.latest_valid_hash .map(|h| hex::encode(&h[..4])) .unwrap_or_else(|| "null".into()), - id = spine_id, "FCU → Reth" ); - EngineFcuResp { - id: spine_id, - status, - latest_valid_hash, - has_payload_id, - payload_id, - } + EngineFcuResp { block_root, status, latest_valid_hash, has_payload_id, payload_id } } Ok(RpcResult { error: Some(e), .. }) => { tracing::warn!("forkchoiceUpdated rpc error: {}", e.message); - break 'parse fcu_error(spine_id); + break 'parse fcu_error(block_root); } Ok(_) | Err(_) => { tracing::warn!("forkchoiceUpdated: missing result"); - break 'parse fcu_error(spine_id); + break 'parse fcu_error(block_root); } } }; @@ -209,7 +202,7 @@ pub(crate) fn handle_fcu_response( #[inline] pub(crate) fn handle_new_payload_response( - spine_id: u64, + block_root: [u8; 32], response: Result<&mut [u8], EngineError>, adapter: &mut SpineAdapter, ) { @@ -217,7 +210,7 @@ pub(crate) fn handle_new_payload_response( let raw = match response { Err(e) => { tracing::warn!("newPayload error: {e}"); - break 'parse new_payload_error(spine_id); + break 'parse new_payload_error(block_root); } Ok(b) => b, }; @@ -226,15 +219,15 @@ pub(crate) fn handle_new_payload_response( let status = status_from_str(&ps.status); let latest_valid_hash = ps.latest_valid_hash.unwrap_or([0u8; 32]); tracing::info!("newPayload → {:?}", status); - EngineNewPayloadResp { id: spine_id, status, latest_valid_hash } + EngineNewPayloadResp { block_root, status, latest_valid_hash } } Ok(RpcResult { error: Some(e), .. }) => { tracing::warn!("newPayload rpc error: {}", e.message); - break 'parse new_payload_error(spine_id); + break 'parse new_payload_error(block_root); } Ok(_) | Err(_) => { tracing::warn!("newPayload: missing result"); - break 'parse new_payload_error(spine_id); + break 'parse new_payload_error(block_root); } } }; @@ -398,9 +391,9 @@ fn get_payload_bodies_error(id: u64) -> EngineGetPayloadBodiesResp { } #[inline] -fn fcu_error(id: u64) -> EngineFcuResp { +fn fcu_error(block_root: [u8; 32]) -> EngineFcuResp { EngineFcuResp { - id, + block_root, status: PayloadValidationStatus::Syncing, latest_valid_hash: [0u8; 32], has_payload_id: false, @@ -409,9 +402,9 @@ fn fcu_error(id: u64) -> EngineFcuResp { } #[inline] -fn new_payload_error(id: u64) -> EngineNewPayloadResp { +fn new_payload_error(block_root: [u8; 32]) -> EngineNewPayloadResp { EngineNewPayloadResp { - id, + block_root, status: PayloadValidationStatus::Syncing, latest_valid_hash: [0u8; 32], } diff --git a/crates/engine/src/tile.rs b/crates/engine/src/tile.rs index 4f407a03..66211ff1 100644 --- a/crates/engine/src/tile.rs +++ b/crates/engine/src/tile.rs @@ -2,6 +2,7 @@ use std::time::{Duration, Instant}; use flux::{spine::SpineAdapter, tile::Tile}; use silver_common::{ELSyncStatus, EngineReq, SilverSpine, TProducer, TRandomAccess}; +use silver_config::EngineConfig; use crate::{ EngineClient, @@ -14,7 +15,8 @@ const HEALTHCHECK_INTERVAL: Duration = Duration::from_secs(10); pub struct EngineTile { pub client: EngineClient, - pub req_consumer: TRandomAccess, + pub gossip_consumer: TRandomAccess, + pub rpc_consumer: TRandomAccess, pub resp_producer: TProducer, first_run: bool, @@ -29,7 +31,13 @@ pub struct EngineTile { impl Tile for EngineTile { fn loop_body(&mut self, adapter: &mut SpineAdapter) { adapter.consume(|req: EngineReq, producers| { - handle_request(&mut self.client, &mut self.req_consumer, &req, producers); + handle_request( + &mut self.client, + &mut self.gossip_consumer, + &mut self.rpc_consumer, + &req, + producers, + ); }); self.spin(adapter); } @@ -37,13 +45,15 @@ impl Tile for EngineTile { impl EngineTile { pub fn new( - client: EngineClient, - req_consumer: TRandomAccess, + config: EngineConfig, + gossip_consumer: TRandomAccess, + rpc_consumer: TRandomAccess, resp_producer: TProducer, ) -> Self { Self { - client, - req_consumer, + client: EngineClient::new(&config.execution_endpoint, &config.jwt_secret), + gossip_consumer, + rpc_consumer, resp_producer, first_run: true, @@ -81,9 +91,9 @@ impl EngineTile { ReqKind::Syncing => { handle_sync_response(response, adapter, sync_status, healthcheck_pending) } - ReqKind::Fcu(spine_id) => handle_fcu_response(spine_id, response, adapter), - ReqKind::NewPayload(spine_id) => { - handle_new_payload_response(spine_id, response, adapter) + ReqKind::Fcu(block_root) => handle_fcu_response(block_root, response, adapter), + ReqKind::NewPayload(block_root) => { + handle_new_payload_response(block_root, response, adapter) } ReqKind::GetPayloadFetch(spine_id) => { handle_get_payload_fetch(spine_id, response, adapter, resp_producer, scratch) diff --git a/crates/engine/src/types.rs b/crates/engine/src/types.rs index 1c722e66..19c677a4 100644 --- a/crates/engine/src/types.rs +++ b/crates/engine/src/types.rs @@ -1,4 +1,8 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use silver_common::ssz_view::{ + BEACON_BLOCK_BODY_FIXED, BeaconBlockBodyView, ExecutionPayloadView, SIGNED_BEACON_BLOCK_MIN, + SignedBeaconBlockView, +}; pub type B256 = [u8; 32]; pub type ExecutionAddress = [u8; 20]; @@ -248,56 +252,54 @@ fn write_withdrawals_json(data: &[u8], out: &mut Vec) -> Result<(), crate::E pub(crate) fn write_new_payload_params( data: &[u8], - versioned_hashes: &[[u8; 32]], - parent_beacon_block_root: &[u8; 32], out: &mut Vec, ) -> Result<(), crate::EngineError> { - if data.len() < 5 { - return Err(crate::EngineError::Ssz("new-payload data too short".into())); - } - let payload_len = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize; - if data.len() < 4 + payload_len + 1 { - return Err(crate::EngineError::Ssz("new-payload data truncated".into())); - } - let ssz = &data[4..4 + payload_len]; - - // Collect exec-request slices without allocating. - let mut pos = 4 + payload_len; - let req_count = data[pos] as usize; - pos += 1; - // u8 count, so max 255; stack array of (start, end) indices into `data`. - let mut req_spans = [(0usize, 0usize); 256]; - for span in req_spans.iter_mut().take(req_count) { - if data.len() < pos + 4 { - return Err(crate::EngineError::Ssz("exec-request length truncated".into())); - } - let req_len = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize; - pos += 4; - if data.len() < pos + req_len { - return Err(crate::EngineError::Ssz("exec-request body truncated".into())); - } - *span = (pos, pos + req_len); - pos += req_len; + if data.len() < SIGNED_BEACON_BLOCK_MIN { + return Err(crate::EngineError::Ssz(format!( + "block too short: {} < {SIGNED_BEACON_BLOCK_MIN}", + data.len() + ))); } - - if ssz.len() < PAYLOAD_FIXED_LEN { + let body = SignedBeaconBlockView::body(data); + if body.len() < BEACON_BLOCK_BODY_FIXED { return Err(crate::EngineError::Ssz(format!( - "payload too short: {} < {PAYLOAD_FIXED_LEN}", - ssz.len() + "body too short: {} < {BEACON_BLOCK_BODY_FIXED}", + body.len() ))); } - let extra_off = u32::from_le_bytes(ssz[436..440].try_into().unwrap()) as usize; - let txs_off = u32::from_le_bytes(ssz[504..508].try_into().unwrap()) as usize; - let wd_off = u32::from_le_bytes(ssz[508..512].try_into().unwrap()) as usize; + let execution_payload_offset: usize = + BeaconBlockBodyView::execution_payload_offset(body) as usize; + let bls_to_execution_payload: usize = + BeaconBlockBodyView::bls_to_execution_changes_offset(body) as usize; + let blob_kzg_off: usize = BeaconBlockBodyView::blob_kzg_commitments_offset(body) as usize; + let execution_requests_offset: usize = + BeaconBlockBodyView::execution_requests_offset(body) as usize; + if execution_payload_offset < BEACON_BLOCK_BODY_FIXED || + bls_to_execution_payload < execution_payload_offset || + blob_kzg_off < bls_to_execution_payload || + execution_requests_offset < blob_kzg_off || + body.len() < execution_requests_offset + { + return Err(crate::EngineError::Ssz("invalid body variable offsets".into())); + } + let execution_payload = &body[execution_payload_offset..bls_to_execution_payload]; + if execution_payload.len() < PAYLOAD_FIXED_LEN { + return Err(crate::EngineError::Ssz(format!( + "execution_payload too short: {} < {PAYLOAD_FIXED_LEN}", + execution_payload.len() + ))); + } + let extra_off: usize = ExecutionPayloadView::extra_data_offset(execution_payload) as usize; + let txs_off: usize = ExecutionPayloadView::transactions_offset(execution_payload) as usize; + let wd_off: usize = ExecutionPayloadView::withdrawals_offset(execution_payload) as usize; if extra_off < PAYLOAD_FIXED_LEN || txs_off < extra_off || wd_off < txs_off || - ssz.len() < wd_off + execution_payload.len() < wd_off { - return Err(crate::EngineError::Ssz("invalid variable offsets".into())); + return Err(crate::EngineError::Ssz("invalid execution_payload variable offsets".into())); } - let wd_data = &ssz[wd_off..]; - if !wd_data.len().is_multiple_of(44) { + if !execution_payload[wd_off..].len().is_multiple_of(44) { return Err(crate::EngineError::Ssz("withdrawals length not multiple of 44".into())); } @@ -306,71 +308,102 @@ pub(crate) fn write_new_payload_params( // ExecutionPayload object — field order matches serde declaration order. out.extend_from_slice(b"{\"parentHash\":\"0x"); - append_hex(&ssz[0..32], out); + append_hex(ExecutionPayloadView::parent_hash(execution_payload), out); out.extend_from_slice(b"\",\"feeRecipient\":\"0x"); - append_hex(&ssz[32..52], out); + append_hex(ExecutionPayloadView::fee_recipient(execution_payload), out); out.extend_from_slice(b"\",\"stateRoot\":\"0x"); - append_hex(&ssz[52..84], out); + append_hex(ExecutionPayloadView::state_root(execution_payload), out); out.extend_from_slice(b"\",\"receiptsRoot\":\"0x"); - append_hex(&ssz[84..116], out); + append_hex(ExecutionPayloadView::receipts_root(execution_payload), out); out.extend_from_slice(b"\",\"logsBloom\":\"0x"); - append_hex(&ssz[116..372], out); + append_hex(ExecutionPayloadView::logs_bloom(execution_payload), out); out.extend_from_slice(b"\",\"prevRandao\":\"0x"); - append_hex(&ssz[372..404], out); + append_hex(ExecutionPayloadView::prev_randao(execution_payload), out); out.extend_from_slice(b"\",\"blockNumber\":"); - append_quantity_u64(u64::from_le_bytes(ssz[404..412].try_into().unwrap()), out); + append_quantity_u64(ExecutionPayloadView::block_number(execution_payload), out); out.extend_from_slice(b",\"gasLimit\":"); - append_quantity_u64(u64::from_le_bytes(ssz[412..420].try_into().unwrap()), out); + append_quantity_u64(ExecutionPayloadView::gas_limit(execution_payload), out); out.extend_from_slice(b",\"gasUsed\":"); - append_quantity_u64(u64::from_le_bytes(ssz[420..428].try_into().unwrap()), out); + append_quantity_u64(ExecutionPayloadView::gas_used(execution_payload), out); out.extend_from_slice(b",\"timestamp\":"); - append_quantity_u64(u64::from_le_bytes(ssz[428..436].try_into().unwrap()), out); + append_quantity_u64(ExecutionPayloadView::timestamp(execution_payload), out); out.extend_from_slice(b",\"extraData\":\"0x"); - append_hex(&ssz[extra_off..txs_off], out); + append_hex(&execution_payload[extra_off..txs_off], out); out.extend_from_slice(b"\",\"baseFeePerGas\":"); - append_u256_le_quantity(ssz[440..472].try_into().unwrap(), out); + append_u256_le_quantity(ExecutionPayloadView::base_fee_per_gas(execution_payload), out); out.extend_from_slice(b",\"blockHash\":\"0x"); - append_hex(&ssz[472..504], out); + append_hex(ExecutionPayloadView::block_hash(execution_payload), out); out.extend_from_slice(b"\",\"transactions\":["); - write_txs_json(&ssz[txs_off..wd_off], out)?; + write_txs_json(&execution_payload[txs_off..wd_off], out)?; out.extend_from_slice(b"],\"withdrawals\":["); - write_withdrawals_json(wd_data, out)?; + write_withdrawals_json(&execution_payload[wd_off..], out)?; out.extend_from_slice(b"],\"blobGasUsed\":"); - append_quantity_u64(u64::from_le_bytes(ssz[512..520].try_into().unwrap()), out); + append_quantity_u64(ExecutionPayloadView::blob_gas_used(execution_payload), out); out.extend_from_slice(b",\"excessBlobGas\":"); - append_quantity_u64(u64::from_le_bytes(ssz[520..528].try_into().unwrap()), out); + append_quantity_u64(ExecutionPayloadView::excess_blob_gas(execution_payload), out); out.push(b'}'); - // versionedHashes + // versionedHashes — derived from blob_kzg_commitments + let blob_kzg_off: usize = BeaconBlockBodyView::blob_kzg_commitments_offset(body) as usize; + let blob_kzg_data = &body[blob_kzg_off..execution_requests_offset]; + // blob_kzg_data is a flat list of 48-byte KZG commitments (no SSZ list offsets, + // because each element is fixed-size, so SSZ encodes it as a plain + // concatenation). out.extend_from_slice(b",["); - for (i, h) in versioned_hashes.iter().enumerate() { + for (i, commitment) in blob_kzg_data.chunks(48).enumerate() { + use sha2::{Digest, Sha256}; + let mut h = Sha256::digest(commitment); + h[0] = 0x01; if i > 0 { out.push(b','); } out.extend_from_slice(b"\"0x"); - append_hex(h, out); + append_hex(&h, out); out.push(b'"'); } out.push(b']'); - // parentBeaconBlockRoot out.extend_from_slice(b",\"0x"); - append_hex(parent_beacon_block_root, out); + append_hex(SignedBeaconBlockView::parent_root(data), out); out.push(b'"'); - // executionRequests + // executionRequests — 3-field SSZ container at + // body[execution_requests_offset..] + let er = &body[execution_requests_offset..]; + if er.len() < 12 { + return Err(crate::EngineError::Ssz("execution_requests too short".into())); + } + let dep_off = u32::from_le_bytes(er[0..4].try_into().unwrap()) as usize; + let wd_off = u32::from_le_bytes(er[4..8].try_into().unwrap()) as usize; + let cons_off = u32::from_le_bytes(er[8..12].try_into().unwrap()) as usize; + if dep_off > wd_off || wd_off > cons_off || cons_off > er.len() { + return Err(crate::EngineError::Ssz("invalid execution_requests offsets".into())); + } + + // Per EIP-7685: only non-empty request types are included; type byte is + // prepended. Type 0x00 = deposit requests, 0x01 = withdrawal requests, 0x02 + // = consolidation requests. + let slices: [(u8, &[u8]); 3] = + [(0x00, &er[dep_off..wd_off]), (0x01, &er[wd_off..cons_off]), (0x02, &er[cons_off..])]; out.extend_from_slice(b",["); - for (i, &(start, end)) in req_spans[..req_count].iter().enumerate() { - if i > 0 { + let mut first = true; + for (type_byte, data) in &slices { + if data.is_empty() { + continue; + } + if !first { out.push(b','); } + first = false; out.extend_from_slice(b"\"0x"); - append_hex(&data[start..end], out); + append_hex(&[*type_byte], out); + append_hex(data, out); out.push(b'"'); } out.push(b']'); out.push(b']'); + Ok(()) } @@ -742,6 +775,9 @@ mod tests { use super::*; const SAMPLE_PAYLOAD_SSZ: &[u8] = include_bytes!("../testdata/sample_payload.ssz"); + const SAMPLE_PAYLOAD_SSZ_SNAPPY: &[u8] = include_bytes!( + "/home/owen/code/rust/silver/crates/beacon_state/tile/consensus-spec-tests/tests/mainnet/fulu/ssz_static/SignedBeaconBlock/ssz_random/case_4/serialized.ssz_snappy" + ); const EMPTY_VAR_PAYLOAD_SSZ: &[u8] = include_bytes!("../testdata/empty_var_payload.ssz"); const MANY_TX_PAYLOAD_SSZ: &[u8] = include_bytes!("../testdata/many_tx_payload.ssz"); const TX_SINGLE: &[u8] = include_bytes!("../testdata/tx_single.bin"); @@ -749,16 +785,52 @@ mod tests { const WITHDRAWALS: &[u8] = include_bytes!("../testdata/withdrawals.bin"); const GET_PAYLOAD_TCACHE: &[u8] = include_bytes!("../testdata/get_payload_tcache.bin"); - fn make_new_payload_frame(ssz: &[u8], exec_reqs: &[&[u8]]) -> Vec { - let mut frame = Vec::new(); - frame.extend_from_slice(&(ssz.len() as u32).to_le_bytes()); - frame.extend_from_slice(ssz); - frame.push(exec_reqs.len() as u8); - for req in exec_reqs { - frame.extend_from_slice(&(req.len() as u32).to_le_bytes()); - frame.extend_from_slice(req); + // Wraps a bare ExecutionPayload SSZ in a minimal SignedBeaconBlock SSZ: + // zeroed fixed fields, the five early body lists empty, empty + // bls_to_execution_changes. `requests` are the raw bytes of the three + // ExecutionRequests lists (deposits, withdrawals, consolidations). + fn make_signed_block( + payload_ssz: &[u8], + kzg_commitments: &[[u8; 48]], + requests: [&[u8]; 3], + parent_root: [u8; 32], + ) -> Vec { + let mut out = vec![0u8; 184 + BEACON_BLOCK_BODY_FIXED]; + out[0..4].copy_from_slice(&100u32.to_le_bytes()); // message offset + out[116..148].copy_from_slice(&parent_root); + out[180..184].copy_from_slice(&84u32.to_le_bytes()); // body offset + + let fixed = BEACON_BLOCK_BODY_FIXED as u32; + let ep_off = fixed; + let bls_off = ep_off + payload_ssz.len() as u32; + let kzg_off = bls_off; + let er_off = kzg_off + 48 * kzg_commitments.len() as u32; + for (pos, val) in [ + (200, fixed), + (204, fixed), + (208, fixed), + (212, fixed), + (216, fixed), + (380, ep_off), + (384, bls_off), + (388, kzg_off), + (392, er_off), + ] { + out[184 + pos..184 + pos + 4].copy_from_slice(&val.to_le_bytes()); + } + out.extend_from_slice(payload_ssz); + for c in kzg_commitments { + out.extend_from_slice(c); } - frame + let mut off = 12u32; + for r in &requests { + out.extend_from_slice(&off.to_le_bytes()); + off += r.len() as u32; + } + for r in &requests { + out.extend_from_slice(r); + } + out } // Constructs the getPayload JSON whose TCache encoding is @@ -1095,11 +1167,14 @@ mod tests { #[test] fn write_new_payload_params_sample_fields() { - let frame = make_new_payload_frame(SAMPLE_PAYLOAD_SSZ, &[]); - let vhashes = [[0xaau8; 32], [0xbbu8; 32]]; - let pbbr = [0xccu8; 32]; + let block = make_signed_block( + SAMPLE_PAYLOAD_SSZ, + &[[0xaau8; 48], [0xbbu8; 48]], + [&[], &[], &[]], + [0xccu8; 32], + ); let mut out = Vec::new(); - write_new_payload_params(&frame, &vhashes, &pbbr, &mut out).unwrap(); + write_new_payload_params(&block, &mut out).unwrap(); let val = simd_json::to_borrowed_value(&mut out).unwrap(); let params = val.as_array().unwrap(); @@ -1134,15 +1209,16 @@ mod tests { assert_eq!(wds[1].get("index").and_then(|v| v.as_str()), Some("0x2")); assert_eq!(wds[1].get("amount").and_then(|v| v.as_str()), Some("0x7d0")); + // sha256 of 48×0xaa / 48×0xbb with first byte forced to 0x01 let vh = params[1].as_array().unwrap(); assert_eq!(vh.len(), 2); assert_eq!( vh[0].as_str(), - Some("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + Some("0x01659f8a49133759d495ee5d15262cdc0050f9027e20c7bed3e0599e27adec4b") ); assert_eq!( vh[1].as_str(), - Some("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + Some("0x018fc9d98c32189fe8232b46db86446b16895b4e2a911803d4b9c1d229838914") ); assert_eq!( @@ -1156,22 +1232,33 @@ mod tests { #[test] fn write_new_payload_params_exec_requests() { - let frame = make_new_payload_frame(SAMPLE_PAYLOAD_SSZ, &[&[0x01, 0x02], &[0x03]]); + let block = make_signed_block(SAMPLE_PAYLOAD_SSZ, &[], [&[], &[0x02], &[0x03]], [0u8; 32]); let mut out = Vec::new(); - write_new_payload_params(&frame, &[], &[0u8; 32], &mut out).unwrap(); + write_new_payload_params(&block, &mut out).unwrap(); let val = simd_json::to_borrowed_value(&mut out).unwrap(); let reqs = val.as_array().unwrap()[3].as_array().unwrap(); + // empty deposit list skipped; type byte prepended to the others assert_eq!(reqs.len(), 2); assert_eq!(reqs[0].as_str(), Some("0x0102")); - assert_eq!(reqs[1].as_str(), Some("0x03")); + assert_eq!(reqs[1].as_str(), Some("0x0203")); + } + + #[test] + fn write_new_payload_params_snappy() { + let ssz = snap::raw::Decoder::new() + .decompress_vec(SAMPLE_PAYLOAD_SSZ_SNAPPY) + .expect("snappy decode"); + let mut json = Vec::new(); + write_new_payload_params(&ssz, &mut json).unwrap(); + println!("{}", std::str::from_utf8(&json).unwrap()); } #[test] fn write_new_payload_params_empty_variable_fields() { - let frame = make_new_payload_frame(EMPTY_VAR_PAYLOAD_SSZ, &[]); + let block = make_signed_block(EMPTY_VAR_PAYLOAD_SSZ, &[], [&[], &[], &[]], [0u8; 32]); let mut out = Vec::new(); - write_new_payload_params(&frame, &[], &[0u8; 32], &mut out).unwrap(); + write_new_payload_params(&block, &mut out).unwrap(); let val = simd_json::to_borrowed_value(&mut out).unwrap(); let ep = &val.as_array().unwrap()[0]; @@ -1182,9 +1269,9 @@ mod tests { #[test] fn write_new_payload_params_many_txs() { - let frame = make_new_payload_frame(MANY_TX_PAYLOAD_SSZ, &[]); + let block = make_signed_block(MANY_TX_PAYLOAD_SSZ, &[], [&[], &[], &[]], [0u8; 32]); let mut out = Vec::new(); - write_new_payload_params(&frame, &[], &[0u8; 32], &mut out).unwrap(); + write_new_payload_params(&block, &mut out).unwrap(); let val = simd_json::to_borrowed_value(&mut out).unwrap(); let txs = @@ -1196,15 +1283,15 @@ mod tests { #[test] fn write_new_payload_params_too_short() { - assert!(write_new_payload_params(&[0u8; 4], &[], &[0u8; 32], &mut Vec::new()).is_err()); + assert!(write_new_payload_params(&[0u8; 4], &mut Vec::new()).is_err()); } #[test] fn write_new_payload_params_truncated_payload() { - // length claims 100 bytes but only 10 are available - let mut frame = vec![100u8, 0, 0, 0]; - frame.extend_from_slice(&[0u8; 10]); - assert!(write_new_payload_params(&frame, &[], &[0u8; 32], &mut Vec::new()).is_err()); + // body offsets point past the truncated end + let block = make_signed_block(SAMPLE_PAYLOAD_SSZ, &[], [&[], &[], &[]], [0u8; 32]); + let truncated = &block[..block.len() - 200]; + assert!(write_new_payload_params(truncated, &mut Vec::new()).is_err()); } // --------------------------------------------------------------------------- diff --git a/crates/storage/src/tile.rs b/crates/storage/src/tile.rs index 222e8424..2ccecc59 100644 --- a/crates/storage/src/tile.rs +++ b/crates/storage/src/tile.rs @@ -151,6 +151,13 @@ impl StorageTile { "data columns by root request: {to_request:b}" ); + // TODO(engine_getBlobsV2): try the EL mempool before/alongside the + // peer ByRoot request — produce `EngineReq::GetBlobs` with the + // versioned hashes from the block's kzg_commitments, then build the + // custody columns locally from the returned blobs. Avoids the p2p + // round trip for mempool blobs. Needs compute_cells_and_kzg_proofs + // (EIP-7594) and the `EngineResp::GetBlobs` queue consumed here + // rather than in the beacon state tile. if self.store.is_synced() { emit(self.column_request(block_root, to_request)); } diff --git a/kurtosis/README.md b/kurtosis/README.md index 5deff408..2eb49c47 100644 --- a/kurtosis/README.md +++ b/kurtosis/README.md @@ -1,4 +1,4 @@ -# Local kurtosis devnet for peer-connection debugging +# Local kurtosis devnet for peer-connection and engine-API debugging A controlled, multi-client ethereum devnet for testing silver's peer behaviour against real CL clients **whose logs you can read**. The point is two-sided @@ -6,6 +6,10 @@ visibility: every silver `connection lost` / `received goodbye` / eviction can be matched against the peer's own account of why it dropped silver, same wall clock. +silver also drives an EL over the engine API (`newPayload` / +`forkchoiceUpdated`); it gets its own dedicated **reth**, run as a second +host process via `run-reth.sh` (see "The execution layer" below). + Containers, not VMs — kurtosis runs the clients as Docker containers; silver runs as a **bare host process** and joins the enclave over the Docker bridge. @@ -13,7 +17,8 @@ runs as a **bare host process** and joins the enclave over the Docker bridge. - **Good for:** protocol correctness — handshake, RPC framing, gossip scoring, Status/Goodbye semantics, "why did silver drop a healthy peer", multi-client - behaviour differences. + behaviour differences. Engine-API wiring: payload verdicts (VALID / INVALID / + SYNCING), fork-choice updates against a real EL, JWT auth. - **Not for:** the zombie-dial / timeout / peer-supply class. Everything here is same-host: near-zero RTT, no loss, no NAT, always enough peers. Those bugs only reproduce on the real internet from a clean source IP. Use a public @@ -24,7 +29,8 @@ detection are Docker-bridge specific. ## Prerequisites -`kurtosis`, `docker`, `curl`, `jq`, and a Fulu-capable silver build. +`kurtosis`, `docker`, `curl`, `jq`, `reth` (silver's EL), and a Fulu-capable +silver build. ## Quick start @@ -32,11 +38,14 @@ detection are Docker-bridge specific. # 1. bring up the devnet + harvest config values into silver-devnet.toml kurtosis/setup.sh # enclave name defaults to silver-dev -# 2. run silver against it (zero source edits) +# 2. start silver's dedicated EL (separate terminal, keeps running) +kurtosis/run-reth.sh + +# 3. run silver against it (zero source edits) RUST_LOG=info,silver_network=info,silver_peer=info \ cargo run --release --bin silver -- --config kurtosis/silver-devnet.toml -# 3. (optional) watch live counters +# 4. (optional) watch live counters cargo run -p silver_surfer -- ~/.local/share silver # counters-network / counters-peer ``` @@ -48,6 +57,8 @@ cargo run -p silver_surfer -- ~/.local/share silver # counters-n | `setup.sh` | Spins up the enclave, harvests the network values, and writes the config TOML. | | `silver-devnet.toml` | silver `Config` (TOML), generated by `setup.sh`. Static fields (`secret_key`, ports, `next_fork_version`) come from env-overridable vars in the script. | | `genesis.ssz` | Genesis state, fetched by `setup.sh` as silver's sync anchor. Generated (gitignored via `*.ssz`); referenced by `chain_config.checkpoint_file`. | +| `run-reth.sh` | Runs silver's dedicated local reth from the harvested `el/` files. | +| `el/` | EL genesis, devnet enodes, JWT, and reth datadir for the local reth, harvested by `setup.sh`. Generated (gitignored). | ## How `setup.sh` wires silver to the enclave @@ -64,6 +75,8 @@ vars in the script; `next_fork_epoch` is omitted and defaults to FAR_FUTURE): | `external_ip_v4` | `kt-` Docker bridge gateway (so peers can dial silver back; non-fatal). | | `chain_config.spec` | `/eth/v1/config/spec` — devnet fork versions + blob schedule. silver derives the fork digest **and** BLS signing domains from these; mainnet defaults mismatch (wrong digest → no peers; wrong domain → blocks fail sig verification). | | `chain_config.checkpoint_file` | `genesis.ssz` fetched from `/eth/v2/debug/beacon/states/genesis` — silver's sync anchor (bootstraps fork choice so block 1's parent, the genesis block, resolves). | +| `engine_config.execution_endpoint` | `http://127.0.0.1:$ENGINE_PORT` — the local reth started by `run-reth.sh` (port from an env-overridable var, default 8551). | +| `engine_config.jwt_secret` | `el/jwt.hex`, written by `setup.sh` (fixed env-overridable value); `run-reth.sh` hands the same file to reth's `--authrpc.jwtsecret`. | On Linux the enclave's `172.x` container IPs are routable from the host, so silver dials them directly (no port-publishing). `next_fork_version` / @@ -74,6 +87,28 @@ peer-id/ENR across restarts. Re-run `setup.sh` after an enclave restart to re-sync (only the derived fields change). +## The execution layer + +silver needs an EL to validate payloads. It gets a **dedicated local reth** +(`run-reth.sh`, a bare host process like silver itself) rather than borrowing +one inside the enclave: every enclave EL is already driven by its paired CL, +and two CLs steering one EL fight over its fork choice — while silver syncs, +its lagging `forkchoiceUpdated`s would yank the shared EL's head backwards. + +`setup.sh` harvests what the local reth needs into `el/`: + +- the devnet **EL genesis** (`el_cl_genesis_data` artifact); +- the enclave ELs' **enodes** (via `admin_nodeInfo`), passed as both + bootnodes and trusted peers so reth syncs the EL chain and sees the tx + pool. The container IPs inside the enodes are host-routable on Linux, same + as the CL dialing; +- a **JWT** shared between silver and reth only (`el/jwt.hex`). + +reth's datadir lives in `el/datadir` and survives restarts; `setup.sh` wipes +it automatically when the enclave (and hence the EL genesis) changes. reth's +log line for each `newPayload` / `forkchoiceUpdated` is the EL-side view to +correlate against silver's engine logs. + ## Viewing node logs ```bash @@ -134,9 +169,10 @@ accumulates blob blocks (hence columns) to sync/validate against. - **`preset: mainnet`** in `net.yaml` matches silver's default `SpecConfig::mainnet()`. Minimal preset would desync slot/epoch math. - **Version-sensitive (upstream, not silver):** the `*_fork_epoch` keys in - `net.yaml` and the `http` beacon-API port-id used by `setup.sh` track - `ethereum-package` churn. If `setup.sh` fails resolving a URL or the chain - won't start, check these against the package version you pulled. + `net.yaml`, the `http` / `rpc` port-ids, and the `el_cl_genesis_data` + artifact name used by `setup.sh` track `ethereum-package` churn. If + `setup.sh` fails resolving a URL or the chain won't start, check these + against the package version you pulled. ## Teardown diff --git a/kurtosis/run-reth.sh b/kurtosis/run-reth.sh new file mode 100755 index 00000000..6da45f86 --- /dev/null +++ b/kurtosis/run-reth.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Run silver's dedicated local reth against the kurtosis devnet. +# +# kurtosis/run-reth.sh (setup.sh must have run first) +# +# Consumes the files setup.sh harvested into el/: the devnet EL genesis, +# the enclave EL enodes (bootnodes + trusted peers, so it syncs and shares +# the tx pool), and the silver<->reth JWT. The datadir persists across +# restarts; setup.sh drops it automatically when the enclave genesis changes. +# +# Requires `reth` on PATH. ENGINE_PORT must match the value setup.sh wrote +# into silver-devnet.toml (default 8551). +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EL_DIR="$HERE/el" +ENGINE_PORT="${ENGINE_PORT:-8551}" + +command -v reth >/dev/null || { echo "missing dependency: reth" >&2; exit 1; } + +GENESIS_JSON="$(find "$EL_DIR/genesis" -name genesis.json 2>/dev/null | head -1)" +[ -n "$GENESIS_JSON" ] && [ -s "$EL_DIR/bootnodes.txt" ] && [ -s "$EL_DIR/jwt.hex" ] || { + echo "el/ incomplete — run kurtosis/setup.sh first" >&2; exit 1; +} + +ENODES="$(paste -sd, "$EL_DIR/bootnodes.txt")" + +exec reth node \ + --chain "$GENESIS_JSON" \ + --datadir "$EL_DIR/datadir" \ + --http \ + --authrpc.addr 127.0.0.1 \ + --authrpc.port "$ENGINE_PORT" \ + --authrpc.jwtsecret "$EL_DIR/jwt.hex" \ + --bootnodes "$ENODES" \ + --trusted-peers "$ENODES" diff --git a/kurtosis/setup.sh b/kurtosis/setup.sh index 4834ed65..6cef09b8 100755 --- a/kurtosis/setup.sh +++ b/kurtosis/setup.sh @@ -11,7 +11,8 @@ # Generates silver-devnet.toml from scratch each run (derived fields harvested # from the CL; static fields — secret_key, ports, next_fork_version — taken # from the env-overridable vars below). Also writes genesis.ssz (the sync -# anchor) alongside it. +# anchor) and el/ (genesis + bootnodes + JWT for silver's dedicated local +# reth — start it with run-reth.sh). # # Requires: kurtosis, docker, curl, jq. Linux only — silver runs as a host # process joining the Docker bridge, and external_ip_v4 detection is @@ -43,6 +44,10 @@ NEXT_FORK_VERSION="${NEXT_FORK_VERSION:-06000000}" # Custody groups silver subscribes to / advertises (ENR cgc). 4 = the spec # minimum for a non-supernode; bump to sample more columns. CUSTODY_GROUP_COUNT="${CUSTODY_GROUP_COUNT:-4}" +# silver <-> local reth engine API (see run-reth.sh). Any 32-byte hex JWT +# works — both sides read the same generated file. +ENGINE_PORT="${ENGINE_PORT:-8551}" +JWT_SECRET="${JWT_SECRET:-2222222222222222222222222222222222222222222222222222222222222222}" # Pin a known-good release — the bare locator pulls the default branch (HEAD), # which periodically breaks (e.g. the zkboost `GpuConfig` regression). Bump the @@ -153,6 +158,49 @@ mbe="$(jq -r '.MAX_BLOBS_PER_BLOCK_ELECTRA | tonumber' <<<"$SPEC")" sps="$(jq -r '.SECONDS_PER_SLOT | tonumber' <<<"$SPEC")" echo "spec: fulu_fork_version=$ffv" +# 7d. Local EL — silver drives its own dedicated reth over the engine API +# (newPayload / forkchoiceUpdated). A dedicated EL, not a shared enclave +# one: two CLs driving one EL fight over its fork choice. Harvest what a +# host reth needs — the EL genesis, the devnet EL enodes, and a JWT — +# into el/; run-reth.sh consumes them. +EL_DIR="$HERE/el" +mkdir -p "$EL_DIR" + +echo "fetching EL genesis -> $EL_DIR/genesis" +rm -rf "$EL_DIR/genesis" +kurtosis files download "$ENCLAVE" el_cl_genesis_data "$EL_DIR/genesis" >/dev/null +GENESIS_JSON="$(find "$EL_DIR/genesis" -name genesis.json | head -1)" +[ -n "$GENESIS_JSON" ] && [ -s "$GENESIS_JSON" ] || { + echo "no genesis.json in el_cl_genesis_data artifact" >&2; exit 1; +} + +# A new enclave means a new EL chain: drop the reth datadir when the +# downloaded genesis changes, else reth refuses the mismatched chain. +GENESIS_SUM="$(sha256sum "$GENESIS_JSON" | awk '{print $1}')" +if [ "$(cat "$EL_DIR/genesis.sha256" 2>/dev/null)" != "$GENESIS_SUM" ]; then + rm -rf "$EL_DIR/datadir" + echo "$GENESIS_SUM" > "$EL_DIR/genesis.sha256" +fi + +# enodes of the devnet ELs -> reth bootnodes/trusted peers. Container IPs in +# the enodes are host-routable on Linux (same model as the CL dialing). +# admin_nodeInfo failures are skipped (client without the admin namespace). +ENODES=() +for svc in $(kurtosis enclave inspect "$ENCLAVE" \ + | grep -oE 'el-[0-9]+-[a-z]+-[a-z]+' | sort -u); do + url="$(kurtosis port print "$ENCLAVE" "$svc" rpc)" || continue + [[ "$url" == http* ]] || url="http://$url" + enode="$(curl -fsS -X POST -H 'Content-Type: application/json' \ + --data '{"jsonrpc":"2.0","method":"admin_nodeInfo","params":[],"id":1}' \ + "$url" | jq -r '.result.enode // empty')" || continue + [ -n "$enode" ] && ENODES+=("$enode") +done +[ "${#ENODES[@]}" -gt 0 ] || { echo "no EL enodes harvested" >&2; exit 1; } +printf '%s\n' "${ENODES[@]}" > "$EL_DIR/bootnodes.txt" +echo "EL bootnodes: ${#ENODES[@]} harvested" + +echo "$JWT_SECRET" > "$EL_DIR/jwt.hex" + # 8. Write the config TOML. Static fields from the vars above; derived fields # harvested. external_ip_v4 omitted if unresolved (outbound-only). { @@ -165,6 +213,10 @@ echo "spec: fulu_fork_version=$ffv" echo "quic_port = $QUIC_PORT" echo "data_column_custody_group_count = $CUSTODY_GROUP_COUNT" echo + echo "[engine_config]" + echo "execution_endpoint = \"http://127.0.0.1:$ENGINE_PORT\"" + echo "jwt_secret = \"$EL_DIR/jwt.hex\"" + echo echo "[chain_config]" echo "genesis_unix_secs = $GENESIS" echo "checkpoint_file = \"$GENESIS_SSZ\"" @@ -187,6 +239,9 @@ echo "spec: fulu_fork_version=$ffv" echo "wrote -> $CONFIG" echo +echo "start silver's local reth (separate terminal):" +echo " kurtosis/run-reth.sh" +echo echo "run silver:" echo " RUST_LOG=info,silver_network=info,silver_peer=info \\" echo " cargo run --release --bin silver -- --config $CONFIG" From 43ea72ee05e68c0bd7b3d3fcc42b08bb182c4df7 Mon Sep 17 00:00:00 2001 From: owen Date: Fri, 12 Jun 2026 20:06:39 +0100 Subject: [PATCH 2/3] fix --- Cargo.lock | 1 - crates/beacon_state/tile/src/fork_choice.rs | 30 +++---- crates/beacon_state/tile/src/tile.rs | 24 ++++-- crates/engine/Cargo.toml | 1 - crates/engine/src/req_handlers.rs | 63 +++++---------- crates/engine/src/types.rs | 33 +++++--- crates/engine/testdata/signed_block.ssz | Bin 0 -> 23999 bytes .../engine/testdata/signed_block_params.json | 74 ++++++++++++++++++ 8 files changed, 149 insertions(+), 77 deletions(-) create mode 100644 crates/engine/testdata/signed_block.ssz create mode 100644 crates/engine/testdata/signed_block_params.json diff --git a/Cargo.lock b/Cargo.lock index 46ddb965..42489c34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4527,7 +4527,6 @@ dependencies = [ "silver_common", "silver_config", "simd-json", - "snap 1.1.1", "thiserror 1.0.69", "tracing", "tracing-subscriber", diff --git a/crates/beacon_state/tile/src/fork_choice.rs b/crates/beacon_state/tile/src/fork_choice.rs index 3b475d9a..33e6938b 100644 --- a/crates/beacon_state/tile/src/fork_choice.rs +++ b/crates/beacon_state/tile/src/fork_choice.rs @@ -101,6 +101,7 @@ impl ForkChoice { finalized_slot: Slot, finalized_block_root: B256, finalized_state_root: B256, + finalized_execution_block_hash: B256, state_id: StateId, ) -> Self { let mut nodes = Vec::with_capacity(MAX_FORK_CHOICE_NODES); @@ -111,7 +112,7 @@ impl ForkChoice { block_root: finalized_block_root, state_root: finalized_state_root, parent_root: [0u8; 32], - execution_block_hash: [0u8; 32], + execution_block_hash: finalized_execution_block_hash, parent: NULL, best_child: NULL, best_descendant: 0, // self @@ -564,7 +565,7 @@ mod tests { fn single_chain_head() { let fin = cp(0, 1); let jus = cp(0, 1); - let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), test_state_id()); + let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), [0u8; 32], test_state_id()); fc.on_block(block(1, root(2), root(1), jus, fin)); fc.on_block(block(2, root(3), root(2), jus, fin)); @@ -576,7 +577,7 @@ mod tests { fn fork_heavier_wins() { let fin = cp(0, 1); let jus = cp(0, 1); - let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), test_state_id()); + let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), [0u8; 32], test_state_id()); fc.on_block(block(1, root(2), root(1), jus, fin)); fc.on_block(block(1, root(3), root(1), jus, fin)); @@ -595,7 +596,7 @@ mod tests { // sibling loses weight. let fin = cp(0, 1); let jus = cp(0, 1); - let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), test_state_id()); + let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), [0u8; 32], test_state_id()); // root(1) → root(2) [idx 1] and root(3) [idx 2] fc.on_block(block(1, root(2), root(1), jus, fin)); @@ -619,7 +620,7 @@ mod tests { fn prune_below_finalized() { let fin = cp(0, 1); let jus = cp(0, 1); - let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), test_state_id()); + let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), [0u8; 32], test_state_id()); fc.on_block(block(1, root(2), root(1), jus, fin)); fc.on_block(block(2, root(3), root(2), jus, fin)); @@ -638,7 +639,7 @@ mod tests { fn deltas_moving_votes() { let fin = cp(0, 1); let jus = cp(0, 1); - let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), test_state_id()); + let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), [0u8; 32], test_state_id()); fc.on_block(block(1, root(2), root(1), jus, fin)); let mut votes = vec![Vote::default(); 16]; @@ -667,7 +668,8 @@ mod tests { // Each validator votes for a different block. let fin = cp(0, 100); let jus = cp(0, 100); - let mut fc = ForkChoice::init(fin, jus, 0, root(100), root(100), test_state_id()); + let mut fc = + ForkChoice::init(fin, jus, 0, root(100), root(100), [0u8; 32], test_state_id()); for i in 1..=16u8 { fc.on_block(block(i as u64, root(i), root(100), jus, fin)); @@ -695,7 +697,7 @@ mod tests { fn deltas_move_out_of_tree() { let fin = cp(0, 1); let jus = cp(0, 1); - let fc = ForkChoice::init(fin, jus, 0, root(1), root(1), test_state_id()); + let fc = ForkChoice::init(fin, jus, 0, root(1), root(1), [0u8; 32], test_state_id()); let mut votes = vec![Vote::default(); 16]; let mut balances = vec![0u64; 16]; @@ -719,7 +721,7 @@ mod tests { fn deltas_changing_balances() { let fin = cp(0, 1); let jus = cp(0, 1); - let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), test_state_id()); + let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), [0u8; 32], test_state_id()); fc.on_block(block(1, root(2), root(1), jus, fin)); let mut votes = vec![Vote::default(); 16]; @@ -746,7 +748,7 @@ mod tests { // Balances change but votes don't — still need deltas. let fin = cp(0, 1); let jus = cp(0, 1); - let fc = ForkChoice::init(fin, jus, 0, root(1), root(1), test_state_id()); + let fc = ForkChoice::init(fin, jus, 0, root(1), root(1), [0u8; 32], test_state_id()); let mut votes = vec![Vote::default(); 16]; let mut old_bal = vec![0u64; 16]; @@ -769,7 +771,7 @@ mod tests { fn split_tie_breaker_no_attestations() { let fin = cp(0, 1); let jus = cp(0, 1); - let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), test_state_id()); + let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), [0u8; 32], test_state_id()); // Two blocks at slot 1 forking from genesis. root(2) < root(3). fc.on_block(block(1, root(2), root(1), jus, fin)); @@ -784,7 +786,7 @@ mod tests { fn shorter_chain_but_heavier_weight() { let fin = cp(0, 1); let jus = cp(0, 1); - let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), test_state_id()); + let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), [0u8; 32], test_state_id()); // Long chain: root(1) → root(2) → root(3) → root(4). fc.on_block(block(1, root(2), root(1), jus, fin)); @@ -808,7 +810,7 @@ mod tests { fn on_block_duplicate() { let fin = cp(0, 1); let jus = cp(0, 1); - let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), test_state_id()); + let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), [0u8; 32], test_state_id()); fc.on_block(block(1, root(2), root(1), jus, fin)); assert_eq!(fc.nodes.len(), 2); @@ -822,7 +824,7 @@ mod tests { fn on_block_unknown_parent() { let fin = cp(0, 1); let jus = cp(0, 1); - let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), test_state_id()); + let mut fc = ForkChoice::init(fin, jus, 0, root(1), root(1), [0u8; 32], test_state_id()); // root(99) is not known. fc.on_block(block(1, root(2), root(99), jus, fin)); diff --git a/crates/beacon_state/tile/src/tile.rs b/crates/beacon_state/tile/src/tile.rs index b2cfe146..a9eccdd8 100644 --- a/crates/beacon_state/tile/src/tile.rs +++ b/crates/beacon_state/tile/src/tile.rs @@ -351,21 +351,32 @@ impl BeaconStateTile { // `latest_block_header.state_root` stays `[0;32]` — the first // post-bootstrap `process_slot` hashes that canonical state and a // patched value would shift the result. - let (block_root, anchor_state_root) = { + let (block_root, anchor_state_root, execution_block_hash) = { let rv = self.state.read_view(anchor); let state_root = ssz_hash::hash_tree_root_state(&rv, &mut self.stf_scratch.state_hash); let mut header = rv.slot.state().latest_block_header; if header.state_root == [0u8; 32] { header.state_root = state_root; } - (ssz_hash::hash_tree_root_block_header(&header), state_root) + ( + ssz_hash::hash_tree_root_block_header(&header), + state_root, + rv.slot.state().latest_execution_payload_header.block_hash, + ) }; let trusted = Checkpoint { epoch: slot.div_ceil(SLOTS_PER_EPOCH), root: block_root }; self.last_applied = anchor; self.last_applied_block_root = block_root; - self.fork_choice = - ForkChoice::init(trusted, trusted, slot, block_root, anchor_state_root, anchor); + self.fork_choice = ForkChoice::init( + trusted, + trusted, + slot, + block_root, + anchor_state_root, + execution_block_hash, + anchor, + ); self.state.publish_state_id(anchor); } @@ -2145,7 +2156,8 @@ mod tests { tile.mode = Mode::Following; let cp = Checkpoint { epoch: 0, root: ANCHOR_ROOT }; - tile.fork_choice = ForkChoice::init(cp, cp, start_slot, ANCHOR_ROOT, ANCHOR_ROOT, anchor); + tile.fork_choice = + ForkChoice::init(cp, cp, start_slot, ANCHOR_ROOT, ANCHOR_ROOT, [0u8; 32], anchor); tile.ensure_shuffling_window(start_slot / SLOTS_PER_EPOCH, anchor); } @@ -2464,7 +2476,7 @@ mod tests { let cp = Checkpoint { epoch: 0, root: parent_root }; tile.fork_choice = - ForkChoice::init(cp, cp, 10, parent_root, parent_root, tile.last_applied); + ForkChoice::init(cp, cp, 10, parent_root, parent_root, [0u8; 32], tile.last_applied); tile.last_applied_block_root = parent_root; // Valid structure, zeroed BLS signature → precheck reaches and fails diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 18100270..c733a4e1 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -20,7 +20,6 @@ sha2.workspace = true silver_common.workspace = true thiserror.workspace = true tracing.workspace = true -snap = "1.1.1" [dev-dependencies] tracing-subscriber.workspace = true diff --git a/crates/engine/src/req_handlers.rs b/crates/engine/src/req_handlers.rs index af77aeaa..66d55fc4 100644 --- a/crates/engine/src/req_handlers.rs +++ b/crates/engine/src/req_handlers.rs @@ -55,50 +55,29 @@ fn handle_new_payload( r: &EngineNewPayloadReq, producers: &mut ::Producers, ) { - match r.block_source { - BlockSource::Gossip => { - let acquired = gossip_consumer.acquire(r.data); - let bytes = match acquired.buffer() { - Ok((b, _)) => b, - Err(e) => { - tracing::warn!("failed to read payload data: {e}"); - producers.engine_resps.produce( - &EngineResp::NewPayload(invalid_new_payload_resp(r.block_root)).into(), - ); - return; - } - }; - - if let Err(e) = send_new_payload(client, bytes, r.block_root) { - tracing::warn!("failed to encode payload: {e}"); - producers.engine_resps.produce( - &EngineResp::NewPayload(invalid_new_payload_resp(r.block_root)).into(), - ); - } - gossip_consumer.free(); - } - BlockSource::Rpc => { - let acquired = rpc_consumer.acquire(r.data); - let bytes = match acquired.buffer() { - Ok((b, _)) => b, - Err(e) => { - tracing::warn!("failed to read payload data: {e}"); - producers.engine_resps.produce( - &EngineResp::NewPayload(invalid_new_payload_resp(r.block_root)).into(), - ); - return; - } - }; - - if let Err(e) = send_new_payload(client, bytes, r.block_root) { - tracing::warn!("failed to encode payload: {e}"); - producers.engine_resps.produce( - &EngineResp::NewPayload(invalid_new_payload_resp(r.block_root)).into(), - ); - } - rpc_consumer.free(); + let consumer = match r.block_source { + BlockSource::Gossip => gossip_consumer, + BlockSource::Rpc => rpc_consumer, + }; + let acquired = consumer.acquire(r.data); + let bytes = match acquired.buffer() { + Ok((b, _)) => b, + Err(e) => { + tracing::warn!("failed to read payload data: {e}"); + producers + .engine_resps + .produce(&EngineResp::NewPayload(invalid_new_payload_resp(r.block_root)).into()); + return; } }; + + if let Err(e) = send_new_payload(client, bytes, r.block_root) { + tracing::warn!("failed to encode payload: {e}"); + producers + .engine_resps + .produce(&EngineResp::NewPayload(invalid_new_payload_resp(r.block_root)).into()); + } + consumer.free(); } #[inline] diff --git a/crates/engine/src/types.rs b/crates/engine/src/types.rs index 19c677a4..fca30693 100644 --- a/crates/engine/src/types.rs +++ b/crates/engine/src/types.rs @@ -269,20 +269,20 @@ pub(crate) fn write_new_payload_params( } let execution_payload_offset: usize = BeaconBlockBodyView::execution_payload_offset(body) as usize; - let bls_to_execution_payload: usize = + let bls_to_execution_changes: usize = BeaconBlockBodyView::bls_to_execution_changes_offset(body) as usize; let blob_kzg_off: usize = BeaconBlockBodyView::blob_kzg_commitments_offset(body) as usize; let execution_requests_offset: usize = BeaconBlockBodyView::execution_requests_offset(body) as usize; if execution_payload_offset < BEACON_BLOCK_BODY_FIXED || - bls_to_execution_payload < execution_payload_offset || - blob_kzg_off < bls_to_execution_payload || + bls_to_execution_changes < execution_payload_offset || + blob_kzg_off < bls_to_execution_changes || execution_requests_offset < blob_kzg_off || body.len() < execution_requests_offset { return Err(crate::EngineError::Ssz("invalid body variable offsets".into())); } - let execution_payload = &body[execution_payload_offset..bls_to_execution_payload]; + let execution_payload = &body[execution_payload_offset..bls_to_execution_changes]; if execution_payload.len() < PAYLOAD_FIXED_LEN { return Err(crate::EngineError::Ssz(format!( "execution_payload too short: {} < {PAYLOAD_FIXED_LEN}", @@ -349,6 +349,11 @@ pub(crate) fn write_new_payload_params( // blob_kzg_data is a flat list of 48-byte KZG commitments (no SSZ list offsets, // because each element is fixed-size, so SSZ encodes it as a plain // concatenation). + if !blob_kzg_data.len().is_multiple_of(48) { + return Err(crate::EngineError::Ssz( + "blob_kzg_commitments length not multiple of 48".into(), + )); + } out.extend_from_slice(b",["); for (i, commitment) in blob_kzg_data.chunks(48).enumerate() { use sha2::{Digest, Sha256}; @@ -775,9 +780,12 @@ mod tests { use super::*; const SAMPLE_PAYLOAD_SSZ: &[u8] = include_bytes!("../testdata/sample_payload.ssz"); - const SAMPLE_PAYLOAD_SSZ_SNAPPY: &[u8] = include_bytes!( - "/home/owen/code/rust/silver/crates/beacon_state/tile/consensus-spec-tests/tests/mainnet/fulu/ssz_static/SignedBeaconBlock/ssz_random/case_4/serialized.ssz_snappy" - ); + // EF spec-test fixture (decompressed): + // mainnet/fulu/ssz_static/SignedBeaconBlock/ssz_random/case_4. + const SIGNED_BLOCK_SSZ: &[u8] = include_bytes!("../testdata/signed_block.ssz"); + // Expected newPayload params for the block above, derived independently + // from the case's value.yaml. + const SIGNED_BLOCK_PARAMS: &[u8] = include_bytes!("../testdata/signed_block_params.json"); const EMPTY_VAR_PAYLOAD_SSZ: &[u8] = include_bytes!("../testdata/empty_var_payload.ssz"); const MANY_TX_PAYLOAD_SSZ: &[u8] = include_bytes!("../testdata/many_tx_payload.ssz"); const TX_SINGLE: &[u8] = include_bytes!("../testdata/tx_single.bin"); @@ -1245,13 +1253,12 @@ mod tests { } #[test] - fn write_new_payload_params_snappy() { - let ssz = snap::raw::Decoder::new() - .decompress_vec(SAMPLE_PAYLOAD_SSZ_SNAPPY) - .expect("snappy decode"); + fn write_new_payload_params_spec_block() { let mut json = Vec::new(); - write_new_payload_params(&ssz, &mut json).unwrap(); - println!("{}", std::str::from_utf8(&json).unwrap()); + write_new_payload_params(SIGNED_BLOCK_SSZ, &mut json).unwrap(); + let actual = simd_json::to_owned_value(&mut json).unwrap(); + let expected = simd_json::to_owned_value(&mut SIGNED_BLOCK_PARAMS.to_vec()).unwrap(); + assert_eq!(actual, expected); } #[test] diff --git a/crates/engine/testdata/signed_block.ssz b/crates/engine/testdata/signed_block.ssz new file mode 100644 index 0000000000000000000000000000000000000000..00a1dbf4ddbd206f9c4138416479d3a78b8f0d6e GIT binary patch literal 23999 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 literal 0 HcmV?d00001 diff --git a/crates/engine/testdata/signed_block_params.json b/crates/engine/testdata/signed_block_params.json new file mode 100644 index 00000000..a617400f --- /dev/null +++ b/crates/engine/testdata/signed_block_params.json @@ -0,0 +1,74 @@ +[ + { + "parentHash": "0x494fada7f641f7428588502f1d99607a80017f4f27f68ee49e665c33a1e8709a", + "feeRecipient": "0x9e1662d87a878b8469aad0570cec4cc289c92022", + "stateRoot": "0x6da230ddd9f7e0f86da15c47876aa1d235dce53b280a0886f218361425bcbfe2", + "receiptsRoot": "0x40a219bc00387a7e6248bfdd3751e9806c7f2bd9fe47a29468442f89c8d4df59", + "logsBloom": "0x8842aadd0fe32a15dd4b2aa7dc982c3c063dfede1f7c5da9b144d3acc14a8327b86c8e27705a929811dc0d1f9b3f3abd0f5f2da01ad2931ae1d60b7a00dc5578bdadd2b618b1a994eafbdf146057d3f884adfb4e6fbb010f3bbd935210a7b38fe652c72220e4b8926d1ff9729b2dee945f9168746817710be5dd567f9dcde9d15b41036af951b00d7a8f56172500e10dea46bd50b669d1ac926190ca25caef7cb2696adb788d072d6996ba4ba116f75d881e80eb3c6aced99ca60bca98e9293d572857becb76b03cde10b20775b680280fc3b3d9e6dcb8ee6c1c5d754174901f1968a29053f5593decd8331dca6b7edec5027c3b816af6524f86b1b8bdc0eada", + "prevRandao": "0xa75486c3eb72e337fbf10c65d713511d92c6e92275cf63b3a42c8f475245ce32", + "blockNumber": "0x840e2a11ca4b7ad8", + "gasLimit": "0x6953b984c87609db", + "gasUsed": "0xabf0bdbab058283c", + "timestamp": "0x8e84b724f382d979", + "extraData": "0x307159c06234a81901f2f589ead239eba15e4f382f19b9c5c4", + "baseFeePerGas": "0xaee532b164d43f01236c36aa417c51028c6199eec54a3793725b132552a328b4", + "blockHash": "0x78020b26b836284232acd3c81aa1fb1e3ae3d6fa72010a699e22a186967515fc", + "transactions": [ + "0x6748d3daf9c8596bfead097e7b5f8ea8cd242ef7e98f36d63fd0d5344b61d0c9ac89d1202d7b15b47280f14ff64ef88c012a0b64bc873b86a46ec003e0cb9fffbde5a18822521ca960aa0f3808598f1e0b757bf8323ad5cc8c6ac3c022fb48cde97bb54b7662215cb97d431b2ad9961a8b04f37bbba5517686072bd69e5eda067bf1e32bf0451fca58e1668393b65bbac7e1282373afeaf373ffe139bfad3e1bece97662dfb40b9cb01eb1d1af827ba3e897349e5c358deac4e12b3378a9a75a81d63b982a6acd8c0bd425cf432deb9199744adf1f37689534e60a1118a29ab37dac570d02b1476b04d53069f00f15007bd07c18824542c39c0363b871c96ce7252bfeb78cb7fb155139e15de682a7c3c79ba0d55b8d929240b5a78a40bf83f350c84763acd90d2b05fccb3aa9af3125eee90dfb2c06a8957c4c544f3f609c5302c9d0900d65825d3a5be581d557ee51827b1d2a4415795d64ec4fae63d5617430eff47b8b29721d88325cce737b9a9a92578116efaab236ca28757225a4de8b9c8176525064703f0a093381db569866a656cb9c5b54c3fc77404effbd4c4cb187274d83a3eab67271c6d8492194cfa6285439fe0aa04a192caf16f5bba7b46b9b1f870404920959e4d4f2c2b3025e92b750a9e838df5b71b5d82024d1ada4cc47e5a855cd11cc16176bc65d3e18ad550bb58a9a791af51358ff444d06b89175f54dcd858146ac83c54a8e1a5d8d8c0df1d22165e5db47fab39b80cb9e8426ef36cdf7e26f3fb7722df05ca05d8d286c7938315a01c93b4c177cf219bbcc698eaba5fcef71664403d2a3f9dd77b84fa07c4789bfd1fe0a5f5200b388fa6efbea827072dd35b1098fbf33768a30f1c5809183682f0b4166e75be6e12303bc02dcff9c439e5e3f98517070da7ca38d438163afdcd271f18233ef455c5aeb28fbf3700182fbeb7389035818d0280a1439e5afc829cce5e2bc1242ede8a604012cb4d02dd75d8edf2e7000ff3f445768c45b08a65ada06d32e0f9c14936298e9f9978c142b2fe83bc3a955d400e5d8a775e2be9820c8ac91a9686807c4f4035fed6ee5c133317433897b5ed384f11360d26b7ed34ddeca4667799a07192de0c0ebc522476785220edb59cefb1a39c5a055862983240c7b799febc909d6e9b8d761a0685fbfc1f5527db3bf5867794d247543a8ecbe36efc4eb39bdabb813b0dadd13033b8cfceaf3aa", + "0x0ed7ed8f3e356d3079b00be579092e6ed3f5d747a8aeb920f239a4882ae4e24b7955d9acb03cbaa920abfc9df9f11c9baf5e884e004de7a69635f2242af31098384974da765c83700f40b581ab88faff192809a3a6f8703cc76d107118c15e442d3695f0f684b248e857c693908ea7dc82d3ade8c81287831726a84027600198b0175aae19cbe584fcbfdcf4b2016982e9213c618a1a19b9c55095b59e3f9ccf8b536834b77476a2cfa2b1b31458d69d0c4ba8e950855d7403ca6bcc4fef2aac57c089b0ae7fa41350ff4e32ea580de0977ed9711e8d34fc5c7ffb1bb17a7327d74ca88b644bfcdeaed5c60b18e1043ef49a2a7159da51a76e4bbb1c0dbdc9183d869f26ebc27ff82afad78fd09a0b27f4e5bde5be6754ade7c9c8be685137f20ad6f97ae5c2a95b0dec6b57f77ab2eff36422563399e46e892bc211115f0b26f08e1e37c717352d4fab82721b91ef829d511cba5e329f54f7873d00657b2350200ba1f7511b72c8d83208a742938fcb524d549a11ea7d0ca0c7060d39706d149c5b537e182fbc713c188d8d758d6bf4fbb2d60a9cd146e222b73d20b4d59212530e221b13ca77ce9a3d91d3d9233962dbefe883f3d6f89129eb07f1ab43cc5c3a7cde32944d04c81203a3723c8029f46127e2306f2e06cca0faba43b66c962253460e8c5bb8f0b3396e91e7905ca623b079055bdf0ac7908bac84a7d03856fe1055dc3e2716ffa83fe7b95d761c46cc834c10218cbfc1e38f8973bf8483bdc54da794c990c784f4f88a163f3da3535f09e9d3b5813115e67a9002269d475fd2bf7a64ea2a77ecbbbb14941b368c9fb8c877c19175085be0f661d1133417024d3b4b3caa601a7bd788e6c4dd16d9714f9f5715be807a04dd7e45dc9349", + "0x9196ba70d92e722940956c3d76233deacd74b77761c1277ce302ea9abe7e5e0da982f7d73968d9e3300de4522a0e848e5afbc059dd1713faf9b541743ded7d5acf437b72b289d2b651623bacd378f3a7f939e34021e7ef15a61fe038893656d459120e4b4798e4d5c7bf4694a32373a47ea68977c2870b7a91eccb73a2abe7891ae1d264ba7d4777e6745dc688c20b5da296987b4875b7b1f377091b5d342ede52860a1a6472b996adb4a4f8addc1c6626c93bd9e9b321608956f8ef47e32fbc5ddbfd80ba1c6e270534853586900fd2f387b907f92622aa831480be5af1b136870b7a91144bb99ecc32ffbd7008b0f94ab7488d7a86e6b318636664a7faaafd82429e363cd95efb5afeb4a2a337bca2dd2533c6be4a6534dce691c7e8d3e13e5117fa85118444f59237dae6fae00884db45ee10f3f8171da30d5397a0ca884ee8244d383f3b92522a4cf866b012c40ee4613637cebecef98431cd3c01945c1e703ffc409dbd85aa72b7fc689e010cc260f40fbe0e82d17441c630a203ca5d52ee2e18341e49cc15393af00a4c0b470fb60d2e8df3f3caca0e68e8d0d17d6523990be6283595d653c87bb6b57fddd432054caa0754b8c1f399d74b3f0402f8bf69b4b0c89c26734c0c499ae8b0716f42b9af10d17b126fb4706bd700009e9c546676d459420493398a085435569cbfad7179a6eac5823e450c24c6e18f14301ee9c296b0e04f3b4ae8eeff2c4a0509e5de5047d3dc2df53ab6d61a94114193d0c1868fce96344c5735c606b2f6f7a8c40b06c858e21e042f4d123f113f9ba733efd4823402de86a023218f1bf3fed7f387a52b468daf000bd5af91226b641ef5214e313359040044fed1bfd652a63c76b98abb8965858f8a35cfb21e912a558a6619266f7784633fee114d1eff55" + ], + "withdrawals": [ + { + "index": "0x33f9cbca45daef20", + "validatorIndex": "0x836c2d757d1a637a", + "address": "0x5a633c93d44b5bb0b705113ea19dcabc3c3b2cef", + "amount": "0x5f0463fe70d00acc" + }, + { + "index": "0x38c7c87f6c883b0c", + "validatorIndex": "0xe52112b24ba060e8", + "address": "0x801b6ac4f5f1a6245b21f739ea22614282968cba", + "amount": "0x77ea3d94a8eb0986" + }, + { + "index": "0x10a67cab3f2ebb38", + "validatorIndex": "0xaf5b5963940b1e37", + "address": "0x874229c46b4fdac9e65ccc5bcc5f4255257b03a1", + "amount": "0xaf347e8dc096a7f5" + }, + { + "index": "0xec2d931b7d00ab93", + "validatorIndex": "0x16db4d01b179db7c", + "address": "0x1d9946b0c65c84fc0b4862aa908f2275038aa8cc", + "amount": "0xba68d87966558676" + }, + { + "index": "0xad621fd779e03701", + "validatorIndex": "0x635ff56710d92b61", + "address": "0xe76b9d6eeace7942b8e09ca122e4351b8c28de80", + "amount": "0xa429fa439bac70a3" + }, + { + "index": "0x20c3ea6e3f44f79c", + "validatorIndex": "0xc3bfb5b26b8d0e7e", + "address": "0x965d32ef5237290b12f091ab761d3a0f951fbb15", + "amount": "0x9c36dd2bd0f2d70" + }, + { + "index": "0xb43dd29d24643e88", + "validatorIndex": "0xab16027fd003c322", + "address": "0x43f15105d297af2553b37140511fd6ba62111e7a", + "amount": "0x8da9dfb7ac264360" + } + ], + "blobGasUsed": "0xf598a1537e5cec98", + "excessBlobGas": "0x16749d4efaffe019" + }, + [], + "0xf90cf3c3c95a4a9fffe5c797f670c4e23f71048b4c5b53a959398a543a34de27", + [ + "0x006e233887dd94356e5d921ef257d34e498558795754aaa89ef537d258e7e800d099a447e781e4afa2c1bc183406300cd2af1d16462c7ace0bbaf05ac5b52cb0a049887259b8ff8131c80f5655590e004eebbda3add2ff9286eb1a211bba7e57137568b955efa952d4a394ccd45f372ad10fd8a84028a5850b549859d56def563981c52423161f5445ed6a48bdae102e366ed47f32663b49938af2fb09fac811eb274e230671602ef5dbd5aef5c010cda917d7c8ecbbe696955f90d8c5ae2c42ab86a07cdf0bbf3b7dc203fa078c39885c6c0e655f724479e3cd8ccc52a17e88cc8cf5b689936d0e188cdc9d3ed686f161d8ff65f21eb126168814aa6da83056f48a69f99e3e684eec414c70b16fe4ad88e729579e23e4e6353a578423a1f8a4d68223d4dbf893d5c4a163d96da01e2e444157849a24aacbb96d0e048f7397c02c306255ac3d58f080ac1c51e2cabd22770e2d063c5200281cfb4eca49dfd52fd8fcd6eb9c8e9dd050b1b6d89f9078c7a81ecb657f56e0d2677da10be9d8e5a626bc17c9472155d598f5321e76d6a2a4a507379e172c9ab09c07845ec5a2f5a61255f9d0002e9da49d40331c16b83b7b6ac6c78c975f61621ab253878163b645d9e5d3260d962bc7c4cbd9f9d23194ce6ac64c6570f963ec06b0303281fa558565034c18957b074a4a4e46ad69b2f64ef89ec6578fbccb0dbee93b170eca7d40386e0dc831391726fc6730ff57e58f58e059807b0bd0586369d6129132c3e767cc5aa1317f7b19f673c8ff6ea46ba741d4455ff47adf7094f641e524aa0a4b866be7cbf45cf4ce4d910d47ceabdddaede11c0f0b5a54f7839d6176a476e20b4ebe96e7f02c34cab602824a9cfeca2d960aecccc8930b366ae65d6f768447e6127ff834e352cccd827a7db1c36c2383bae1f34632e983ebce27f01aa0f1aefd7875548c6523a06b6876b4a8d7f92592631ff7f119c7c0bf4e6d2ca36c088e47aa0bdf4780a4e5e52f580002c7b7198bab58e3f025654810477d32422099e5259303d30d6593949b9b3a351305bebeea0829c48f5058e8e8ccf41e815a09424c3e856b1d17bd5bf70f1c178a5cc6ea18c4a3465e4b638be9240d594850c7e3f91b220a8d11ee793a45ceb513162fe4c9efac1497bb84caec48455f65ea50526b1d1f9adacb7fc9f488ae0531d5956616c1a8a8744a186b86f78d07de11867210a4c0cad633aef0e8d0fadbb7986566d55974e05a1d91c1fb13533393826f386ec8cae68da8eb5d36ca3f5f088735f9d3ac6944177faba121109d562b34fd11c703e5e8ca882c49c83485ce231e03c7fdb35c669eeeafd80af9aa9e095d99a769a290", + "0x0109994e82f77ed6173b6e518154a0bbe8787e434a3f3a43e6ac651c37da6990362f9b8c7c594ca2e537cec63aadce96ba90a78cf43cfc053ac94bbd3f8e2d962cf839294c9800490496dd5de093832776b0011da7e35ba9e00151855ccad1fd1e1c17808e511b0f9b8c28c4f0b75d7797f4ff7f4eba4ac9bf2df35b898708d9156fe7dcd7ecf22896174aded994500b7dd922cf9879faa8eaa8c07dcdfac93721453e2d0c730f751e221750631dcf7754b20a9d2cf6bda72eb02e9f2e1f6c951be29a5e2f0b4c06427d966b6feba2f2b698e6c659b32c6dfce6162b6c3f15c02a8b415a9f9e03c5468b50e3e13fa325fbc1a02c08e4aa8dcc056f226ac4d8b5e43dc441d838714a913e8cb43468e1e99f043d88137ff49115951276c5ee9d6341720fe3129289c5918b98687d7f9483a8ce9f1f0c15f22a202a2ab9576528100710015dfa556c97a9a419e40378820339bb0694770a23d8b805c86fa9279ce137416598ff91b05151b4f28ccaec89a4a0ea08fe030996bddcb19161d7f9ae02ab06865cac2711a66f288158239b01ff05298557d4602db54f96bc7a52f097556b0fceea9b53ee4b6b305a86e8a76cac5e1188e51a8a9bb76dc7a5c4a9650c6454aa0ce491c96293d862ee1425cc21bccf1d4f1fdca95e655dbe17b367db72b0615c5d198229584b593a6b756fbbc1d24020bfda813c23da560fdffca8ec704c9b1f4d5f8c0b374eb4380f14b505cdf82af59e94affb16fc7e2f752ac79c5d3bffbe18b225dae8dd5b4a12a91913a853e6058b08042f49e44f9b4d2cd6e995d63dd496ac42e613051dc12a5b9889274f99c63b9a1d0f264df85f8e869da4ea83a089af4d0ad7a2f3c7a55f5ea532a776da644dd744d99f7e96207a13a0fd225269baa8844c62c9c4fe39281e9b5f592db327931aa78dce33a28328b6fdf58a4eeca0c2b10a71e0127a66546147" + ] +] From 4de698cb3fcb2b009b29d5c759a1dad85f55d5c0 Mon Sep 17 00:00:00 2001 From: owen Date: Fri, 12 Jun 2026 20:35:52 +0100 Subject: [PATCH 3/3] fix imports --- .../beacon_state/tile/src/state_transition.rs | 83 +++++++++---------- 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/crates/beacon_state/tile/src/state_transition.rs b/crates/beacon_state/tile/src/state_transition.rs index 0dd59aa1..3ef8070e 100644 --- a/crates/beacon_state/tile/src/state_transition.rs +++ b/crates/beacon_state/tile/src/state_transition.rs @@ -2,17 +2,19 @@ use core::cmp::{max, min}; use blst::min_pk::PublicKey; use silver_beacon_state_data::{ - self as common, B256, BalancesWriteView, Current, EPOCHS_PER_SLASHINGS_VECTOR, Epoch, - EpochGroup, EpochId, EpochView, Eth1WriteView, Immutable, LongtailGroup, LongtailId, - LongtailView, PENDING_CONSOLIDATIONS_LIMIT, PENDING_PARTIAL_WITHDRAWALS_LIMIT, - ParticipationWriteView, PendingView, PendingWriteView, Previous, SLOTS_PER_EPOCH, - SYNC_COMMITTEE_SIZE, Slot, SlotStateView, SlotStateWriteView, SpecConfig, StateId, - StateReadView, StateWriterView, ValidatorsView, ValidatorsWriteView, append_validator, + B256, BalancesWriteView, BeaconBlockHeader, Current, EPOCHS_PER_SLASHINGS_VECTOR, Epoch, + EpochGroup, EpochId, EpochView, Eth1Data, Eth1WriteView, ExecutionPayloadHeader, Immutable, + LongtailGroup, LongtailId, LongtailView, PENDING_CONSOLIDATIONS_LIMIT, + PENDING_PARTIAL_WITHDRAWALS_LIMIT, ParticipationWriteView, PendingConsolidation, + PendingDeposit, PendingPartialWithdrawal, PendingView, PendingWriteView, Previous, + SLOTS_PER_EPOCH, SYNC_COMMITTEE_SIZE, Slot, SlotStateView, SlotStateWriteView, SpecConfig, + StateId, StateReadView, StateWriterView, ValidatorsView, ValidatorsWriteView, Withdrawals, + append_validator, }; use silver_common::{ metrics::timed, ssz_view::{ - self, ATTESTATION_DATA_SIZE, AttestationDataView, AttestationView, BEACON_BLOCK_BODY_FIXED, + ATTESTATION_DATA_SIZE, AttestationDataView, AttestationView, BEACON_BLOCK_BODY_FIXED, BEACON_BLOCK_HEADER_SIZE, BLOCK_SYNC_AGGREGATE_SIZE, BeaconBlockBodyView, BeaconBlockHeaderView, CONSOLIDATION_REQUEST_SIZE, ConsolidationRequestView, DEPOSIT_CONTRACT_TREE_DEPTH, DEPOSIT_REQUEST_SIZE, DEPOSIT_SIZE, DepositDataView, @@ -23,6 +25,7 @@ use silver_common::{ WITHDRAWAL_REQUEST_SIZE, WITHDRAWAL_SIZE, WithdrawalRequestView, WithdrawalView, }, }; +use silver_ssz::ssz_view::{IndexedAttestationView, MAX_ATTESTING_INDICES}; use crate::{ bls::{self, SigBatch}, @@ -69,7 +72,7 @@ pub struct StfScratch { /// Active set / committee participants / participating indices — reused /// across the epoch-transition and block-body passes. pub active: Vec, - pub postponed: Vec, + pub postponed: Vec, /// Sparse-edit rebuild buffers + effective-balance column for the /// epoch-transition passes. pub replace_u64: Vec<(u32, u64)>, @@ -82,7 +85,7 @@ pub struct StfScratch { impl StfScratch { pub fn new(validator_cap: usize) -> Self { Self { - active: Vec::with_capacity(validator_cap.max(ssz_view::MAX_ATTESTING_INDICES)), + active: Vec::with_capacity(validator_cap.max(MAX_ATTESTING_INDICES)), postponed: Vec::with_capacity(epoch_transition::MAX_PENDING_DEPOSITS_PER_EPOCH), replace_u64: Vec::with_capacity(validator_cap), replace_u8: Vec::with_capacity(validator_cap), @@ -508,7 +511,7 @@ pub fn process_block_header( return Err(BlockError::ParentRootMismatch { expected: expected_parent, got: parent_root }); } - view.slot.state_mut().latest_block_header = common::BeaconBlockHeader { + view.slot.state_mut().latest_block_header = BeaconBlockHeader { slot: block_slot, proposer_index, parent_root, @@ -847,7 +850,7 @@ fn process_eth1_data(slot: &mut SlotStateWriteView, eth1: &mut Eth1WriteView, bo let deposit_count = Eth1DataView::deposit_count(data); let block_hash: B256 = *Eth1DataView::block_hash(data); - let vote = common::Eth1Data { deposit_root, deposit_count, block_hash }; + let vote = Eth1Data { deposit_root, deposit_count, block_hash }; // One vote per slot, reset each voting period (`process_eth1_data_reset`); // `push` enforces the spec cap. eth1.push(vote); @@ -1297,7 +1300,7 @@ pub fn process_execution_payload( .copy_from_slice(&payload_bytes[extra_data_off..extra_data_off + extra_data_len]); } - slot.state_mut().latest_execution_payload_header = common::ExecutionPayloadHeader { + slot.state_mut().latest_execution_payload_header = ExecutionPayloadHeader { parent_hash: *ExecutionPayloadView::parent_hash(payload_bytes), fee_recipient: *ExecutionPayloadView::fee_recipient(payload_bytes), state_root: *ExecutionPayloadView::state_root(payload_bytes), @@ -1859,7 +1862,7 @@ pub fn process_deposit_requests(view: &mut StateWriterView, data: &[u8]) { let d: &[u8; DEPOSIT_REQUEST_SIZE] = data[i * DEPOSIT_REQUEST_SIZE..(i + 1) * DEPOSIT_REQUEST_SIZE].try_into().unwrap(); let pubkey = *DepositRequestView::pubkey(d); - let credentials = common::Withdrawals(*DepositRequestView::withdrawal_credentials(d)); + let credentials = Withdrawals(*DepositRequestView::withdrawal_credentials(d)); let amount = DepositRequestView::amount(d); let signature = *DepositRequestView::signature(d); let index = DepositRequestView::index(d); @@ -1868,7 +1871,7 @@ pub fn process_deposit_requests(view: &mut StateWriterView, data: &[u8]) { slot.state_mut().deposit_requests_start_index = index; } - pending.push_pending_deposit(common::PendingDeposit { + pending.push_pending_deposit(PendingDeposit { pubkey, withdrawal_credentials: credentials, amount, @@ -1948,7 +1951,7 @@ pub fn process_withdrawal_requests(view: &mut StateWriterView, cfg: &SpecConfig, current_epoch, ); let withdrawable_epoch = exit_queue_epoch + cfg.min_validator_withdrawability_delay; - pending.push_pending_partial_withdrawal(common::PendingPartialWithdrawal { + pending.push_pending_partial_withdrawal(PendingPartialWithdrawal { index: vi as u64, amount: to_withdraw, withdrawable_epoch, @@ -2048,7 +2051,7 @@ pub fn process_consolidation_requests(view: &mut StateWriterView, cfg: &SpecConf source_idx, exit_epoch + cfg.min_validator_withdrawability_delay, ); - pending.push_pending_consolidation(common::PendingConsolidation { + pending.push_pending_consolidation(PendingConsolidation { source_index: source_idx as u64, target_index: target_idx as u64, }); @@ -2135,7 +2138,7 @@ pub(crate) fn signing_root_for_block_header( ) -> B256 { let hb: &[u8; BEACON_BLOCK_HEADER_SIZE] = header[..BEACON_BLOCK_HEADER_SIZE].try_into().unwrap(); - let h = common::BeaconBlockHeader { + let h = BeaconBlockHeader { slot: BeaconBlockHeaderView::slot(hb), proposer_index: BeaconBlockHeaderView::proposer_index(hb), parent_root: *BeaconBlockHeaderView::parent_root(hb), @@ -2173,7 +2176,7 @@ pub fn process_deposits(view: &mut StateWriterView, data: &[u8]) -> Result<()> { } let pubkey = DepositDataView::pubkey(dd); - let credentials = common::Withdrawals(*DepositDataView::withdrawal_credentials(dd)); + let credentials = Withdrawals(*DepositDataView::withdrawal_credentials(dd)); let amount = DepositDataView::amount(dd); let signature = *DepositDataView::signature(dd); @@ -2261,7 +2264,7 @@ pub fn process_bls_to_execution_changes( validator_index, from_bls_pubkey, )?; - let creds = common::Withdrawals::eth1(to_execution_address); + let creds = Withdrawals::eth1(to_execution_address); validators.set_credentials(validator_index, creds); } Ok(()) @@ -2286,7 +2289,7 @@ pub fn collect_sigs_attester_slashings( for (ia_off, ia_end, indices) in [(off1, off2, i1), (off2, slashing.len(), i2)] { let ia = &slashing[ia_off..ia_end]; - let target_epoch = ssz_view::IndexedAttestationView::target_epoch(ia); + let target_epoch = IndexedAttestationView::target_epoch(ia); let fv = bls::fork_version_at_epoch(fork_epoch, prev_ver, cur_ver, target_epoch); active_scratch.clear(); let n_idx = indices.len() / 8; @@ -2301,8 +2304,8 @@ pub fn collect_sigs_attester_slashings( } active_scratch.push(vi as u32); } - let data_chunk: &[u8; 128] = ssz_view::IndexedAttestationView::data(ia); - let sig: &[u8; 96] = ssz_view::IndexedAttestationView::signature(ia); + let data_chunk: &[u8; 128] = IndexedAttestationView::data(ia); + let sig: &[u8; 96] = IndexedAttestationView::signature(ia); let object_root = ssz_hash::hash_attestation_data(data_chunk); let domain = bls::compute_domain(bls::DOMAIN_BEACON_ATTESTER, fv, &gvr); let signing_root = bls::compute_signing_root(&object_root, &domain); @@ -2430,9 +2433,7 @@ pub fn validate_attester_slashing_for_gossip( } let i1 = attesting_indices_bytes(slashing, off1, off2); let i2 = attesting_indices_bytes(slashing, off2, slashing.len()); - if i1.len() / 8 > ssz_view::MAX_ATTESTING_INDICES || - i2.len() / 8 > ssz_view::MAX_ATTESTING_INDICES - { + if i1.len() / 8 > MAX_ATTESTING_INDICES || i2.len() / 8 > MAX_ATTESTING_INDICES { return false; } if !indices_sorted_unique(i1) || !indices_sorted_unique(i2) { @@ -2459,7 +2460,7 @@ pub fn validate_attester_slashing_for_gossip( sig_batch.clear(); for (ia_off, ia_end, indices) in [(off1, off2, i1), (off2, slashing.len(), i2)] { let ia = &slashing[ia_off..ia_end]; - let target_epoch = ssz_view::IndexedAttestationView::target_epoch(ia); + let target_epoch = IndexedAttestationView::target_epoch(ia); let fv = bls::fork_version_at_epoch(fork_epoch, prev_ver, cur_ver, target_epoch); let n_idx = indices.len() / 8; for k in 0..n_idx { @@ -2468,8 +2469,8 @@ pub fn validate_attester_slashing_for_gossip( return false; } } - let data_chunk: &[u8; 128] = ssz_view::IndexedAttestationView::data(ia); - let sig: &[u8; 96] = ssz_view::IndexedAttestationView::signature(ia); + let data_chunk: &[u8; 128] = IndexedAttestationView::data(ia); + let sig: &[u8; 96] = IndexedAttestationView::signature(ia); let object_root = ssz_hash::hash_attestation_data(data_chunk); let domain = bls::compute_domain(bls::DOMAIN_BEACON_ATTESTER, fv, &gvr); let signing_root = bls::compute_signing_root(&object_root, &domain); @@ -2740,7 +2741,7 @@ fn switch_to_compounding_validator( ) { let mut bytes = validators.credentials(vi as usize).0; bytes[0] = COMPOUNDING_WITHDRAWAL_PREFIX; - let creds = common::Withdrawals(bytes); + let creds = Withdrawals(bytes); validators.set_credentials(vi, creds); let balance = balances.get(vi as usize); @@ -2748,7 +2749,7 @@ fn switch_to_compounding_validator( let excess = balance - MIN_ACTIVATION_BALANCE; balances.set(vi, MIN_ACTIVATION_BALANCE); let pubkey = *validators.pubkey(vi as usize); - pending.push_pending_deposit(common::PendingDeposit { + pending.push_pending_deposit(PendingDeposit { pubkey, withdrawal_credentials: creds, amount: excess, @@ -2767,7 +2768,7 @@ fn switch_to_compounding_validator( fn apply_deposit( view: &mut StateWriterView, pubkey: &[u8; 48], - credentials: &common::Withdrawals, + credentials: &Withdrawals, amount: u64, signature: &[u8; 96], ) -> Result<()> { @@ -2780,7 +2781,7 @@ fn apply_deposit( append_validator(view, *pubkey, pubkey_decompressed, *credentials); } - view.pending.push_pending_deposit(common::PendingDeposit { + view.pending.push_pending_deposit(PendingDeposit { pubkey: *pubkey, withdrawal_credentials: *credentials, amount, @@ -2823,11 +2824,7 @@ mod tests { assert_ne!(view.slot.state().eth1_data.deposit_root, deposit_root); for _ in 0..1024 { - view.eth1.push(common::Eth1Data { - deposit_root, - deposit_count: 42, - block_hash: [0xBB; 32], - }); + view.eth1.push(Eth1Data { deposit_root, deposit_count: 42, block_hash: [0xBB; 32] }); } process_eth1_data(&mut view.slot, &mut view.eth1, &body); assert_eq!(view.slot.state().eth1_data.deposit_root, deposit_root); @@ -2880,8 +2877,7 @@ mod tests { let mut st = fresh_state(); let (mut view, _, _) = st.view(); - view.slot.state_mut().eth1_data = - common::Eth1Data { deposit_root: root, ..Default::default() }; + view.slot.state_mut().eth1_data = Eth1Data { deposit_root: root, ..Default::default() }; // eth1_deposit_index defaults to 0. deposit_into(&mut view, &deposit).expect("valid proof must accept"); @@ -2897,8 +2893,7 @@ mod tests { let mut st = fresh_state(); let (mut view, _, _) = st.view(); - view.slot.state_mut().eth1_data = - common::Eth1Data { deposit_root: root, ..Default::default() }; + view.slot.state_mut().eth1_data = Eth1Data { deposit_root: root, ..Default::default() }; let err = deposit_into(&mut view, &deposit).unwrap_err(); assert!(err.is_fatal()); @@ -2914,8 +2909,7 @@ mod tests { let mut st = fresh_state(); let (mut view, _, _) = st.view(); - view.slot.state_mut().eth1_data = - common::Eth1Data { deposit_root: root, ..Default::default() }; + view.slot.state_mut().eth1_data = Eth1Data { deposit_root: root, ..Default::default() }; let err = deposit_into(&mut view, &deposit).unwrap_err(); assert!(matches!(err, Error::InvalidDepositProof { .. })); @@ -2928,8 +2922,7 @@ mod tests { let mut st = fresh_state(); let (mut view, _, _) = st.view(); - view.slot.state_mut().eth1_data = - common::Eth1Data { deposit_root: root, ..Default::default() }; + view.slot.state_mut().eth1_data = Eth1Data { deposit_root: root, ..Default::default() }; // Proof was built for index 0; claim index 1 instead → must fail. view.slot.state_mut().eth1_deposit_index += 1;