diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index e7efa9564f..1575ad3a5c 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -403,9 +403,9 @@ pub fn get_validator_execution_payload_envelope( .and(chain_filter) .then( |slot: Slot, - // TODO(gloas) we're only doing local building - // we'll need to implement builder index logic - // eventually. + // TODO(gloas) we're only doing local building + // we'll need to implement builder index logic + // eventually. _builder_index: u64, accept_header: Option, not_synced_filter: Result<(), Rejection>, diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 90712d5a6b..f8d345e87c 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -3978,8 +3978,26 @@ impl ApiTester { .unwrap(); let envelope = envelope_response.data; - assert_eq!(envelope.beacon_block_root, block_root); - assert_eq!(envelope.slot, slot); + + // Verify envelope fields match the produced block + assert_eq!( + envelope.beacon_block_root, block_root, + "Envelope beacon_block_root should match the produced block's root" + ); + assert_eq!( + envelope.slot, slot, + "Envelope slot should match the block's slot" + ); + assert_eq!( + envelope.builder_index, + u64::MAX, + "Builder index should be u64::MAX for local building" + ); + assert_ne!( + envelope.state_root, + Hash256::ZERO, + "State root should not be zero" + ); self.chain.slot_clock.set_slot(slot.as_u64() + 1); } @@ -4054,8 +4072,25 @@ impl ApiTester { .await .unwrap(); - assert_eq!(envelope.beacon_block_root, block_root); - assert_eq!(envelope.slot, slot); + // Verify envelope fields match the produced block + assert_eq!( + envelope.beacon_block_root, block_root, + "Envelope beacon_block_root should match the produced block's root" + ); + assert_eq!( + envelope.slot, slot, + "Envelope slot should match the block's slot" + ); + assert_eq!( + envelope.builder_index, + u64::MAX, + "Builder index should be u64::MAX for local building" + ); + assert_ne!( + envelope.state_root, + Hash256::ZERO, + "State root should not be zero" + ); self.chain.slot_clock.set_slot(slot.as_u64() + 1); } diff --git a/consensus/types/src/execution/execution_payload_envelope.rs b/consensus/types/src/execution/execution_payload_envelope.rs index 7f68dae037..978a35dfb9 100644 --- a/consensus/types/src/execution/execution_payload_envelope.rs +++ b/consensus/types/src/execution/execution_payload_envelope.rs @@ -28,6 +28,49 @@ impl SignedRoot for ExecutionPayloadEnvelope {} mod tests { use super::*; use crate::MainnetEthSpec; + use crate::test_utils::TestRandom; + use rand::SeedableRng; + use rand_xorshift::XorShiftRng; ssz_and_tree_hash_tests!(ExecutionPayloadEnvelope); + + #[test] + fn signing_root_is_deterministic() { + let mut rng = XorShiftRng::from_seed([0x42; 16]); + let envelope = ExecutionPayloadEnvelope::::random_for_test(&mut rng); + let domain = Hash256::random_for_test(&mut rng); + + let signing_root_1 = envelope.signing_root(domain); + let signing_root_2 = envelope.signing_root(domain); + + assert_eq!(signing_root_1, signing_root_2); + } + + #[test] + fn signing_root_changes_with_domain() { + let mut rng = XorShiftRng::from_seed([0x42; 16]); + let envelope = ExecutionPayloadEnvelope::::random_for_test(&mut rng); + let domain_1 = Hash256::random_for_test(&mut rng); + let domain_2 = Hash256::random_for_test(&mut rng); + + let signing_root_1 = envelope.signing_root(domain_1); + let signing_root_2 = envelope.signing_root(domain_2); + + assert_ne!(signing_root_1, signing_root_2); + } + + #[test] + fn signing_root_changes_with_envelope_data() { + let mut rng = XorShiftRng::from_seed([0x42; 16]); + let envelope_1 = ExecutionPayloadEnvelope::::random_for_test(&mut rng); + let mut envelope_2 = envelope_1.clone(); + envelope_2.beacon_block_root = Hash256::random_for_test(&mut rng); + + let domain = Hash256::random_for_test(&mut rng); + + let signing_root_1 = envelope_1.signing_root(domain); + let signing_root_2 = envelope_2.signing_root(domain); + + assert_ne!(signing_root_1, signing_root_2); + } } diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs index 7b6a582363..c3c61751d0 100644 --- a/validator_client/lighthouse_validator_store/src/lib.rs +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -20,12 +20,12 @@ use task_executor::TaskExecutor; use tracing::{error, info, instrument, warn}; use types::{ AbstractExecPayload, Address, AggregateAndProof, Attestation, BeaconBlock, BlindedPayload, - ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, Fork, Graffiti, Hash256, - SelectionProof, SignedAggregateAndProof, SignedBeaconBlock, SignedContributionAndProof, - SignedRoot, SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, - SyncAggregatorSelectionData, SyncCommitteeContribution, SyncCommitteeMessage, - SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, VoluntaryExit, - graffiti::GraffitiString, + ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, ExecutionPayloadEnvelope, Fork, + FullPayload, Graffiti, Hash256, SelectionProof, SignedAggregateAndProof, SignedBeaconBlock, + SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedRoot, + SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, SyncAggregatorSelectionData, + SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, + ValidatorRegistrationData, VoluntaryExit, graffiti::GraffitiString, }; use validator_store::{ DoppelgangerStatus, Error as ValidatorStoreError, ProposalData, SignedBlock, UnsignedBlock, @@ -1242,4 +1242,33 @@ impl ValidatorStore for LighthouseValidatorS .get_builder_proposals_defaulting(validator.get_builder_proposals()), }) } + + /// Sign an `ExecutionPayloadEnvelope` for Gloas (local building). + /// The proposer acts as the builder and signs with the BeaconBuilder domain. + async fn sign_execution_payload_envelope( + &self, + validator_pubkey: PublicKeyBytes, + envelope: ExecutionPayloadEnvelope, + ) -> Result, Error> { + let domain_hash = self.spec.get_builder_domain(); + let signing_root = envelope.signing_root(domain_hash); + + // Execution payload envelope signing is not slashable, bypass doppelganger protection. + let signing_method = self.doppelganger_bypassed_signing_method(validator_pubkey)?; + + let signature = signing_method + .get_signature_from_root::>( + SignableMessage::ExecutionPayloadEnvelope(&envelope), + signing_root, + &self.task_executor, + None, + ) + .await + .map_err(Error::SpecificError)?; + + Ok(SignedExecutionPayloadEnvelope { + message: envelope, + signature, + }) + } } diff --git a/validator_client/signing_method/src/lib.rs b/validator_client/signing_method/src/lib.rs index bf3cc6a17d..c132d86c17 100644 --- a/validator_client/signing_method/src/lib.rs +++ b/validator_client/signing_method/src/lib.rs @@ -49,6 +49,7 @@ pub enum SignableMessage<'a, E: EthSpec, Payload: AbstractExecPayload = FullP SignedContributionAndProof(&'a ContributionAndProof), ValidatorRegistration(&'a ValidatorRegistrationData), VoluntaryExit(&'a VoluntaryExit), + ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope), } impl> SignableMessage<'_, E, Payload> { @@ -70,6 +71,7 @@ impl> SignableMessage<'_, E, Payload SignableMessage::SignedContributionAndProof(c) => c.signing_root(domain), SignableMessage::ValidatorRegistration(v) => v.signing_root(domain), SignableMessage::VoluntaryExit(exit) => exit.signing_root(domain), + SignableMessage::ExecutionPayloadEnvelope(e) => e.signing_root(domain), } } } @@ -233,6 +235,9 @@ impl SigningMethod { Web3SignerObject::ValidatorRegistration(v) } SignableMessage::VoluntaryExit(e) => Web3SignerObject::VoluntaryExit(e), + SignableMessage::ExecutionPayloadEnvelope(e) => { + Web3SignerObject::ExecutionPayloadEnvelope(e) + } }; // Determine the Web3Signer message type. diff --git a/validator_client/signing_method/src/web3signer.rs b/validator_client/signing_method/src/web3signer.rs index 246d9e9e09..7bf953aaeb 100644 --- a/validator_client/signing_method/src/web3signer.rs +++ b/validator_client/signing_method/src/web3signer.rs @@ -19,6 +19,7 @@ pub enum MessageType { SyncCommitteeSelectionProof, SyncCommitteeContributionAndProof, ValidatorRegistration, + ExecutionPayloadEnvelope, } #[derive(Debug, PartialEq, Copy, Clone, Serialize)] @@ -75,6 +76,7 @@ pub enum Web3SignerObject<'a, E: EthSpec, Payload: AbstractExecPayload> { SyncAggregatorSelectionData(&'a SyncAggregatorSelectionData), ContributionAndProof(&'a ContributionAndProof), ValidatorRegistration(&'a ValidatorRegistrationData), + ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope), } impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Payload> { @@ -140,6 +142,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Pa MessageType::SyncCommitteeContributionAndProof } Web3SignerObject::ValidatorRegistration(_) => MessageType::ValidatorRegistration, + Web3SignerObject::ExecutionPayloadEnvelope(_) => MessageType::ExecutionPayloadEnvelope, } } } diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index dd1b938baf..243059702b 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -143,6 +143,7 @@ impl BlockServiceBuilder { // Combines a set of non-block-proposing `beacon_nodes` and only-block-proposing // `proposer_nodes`. +#[derive(Clone)] pub struct ProposerFallback { beacon_nodes: Arc>, proposer_nodes: Option>>, @@ -610,7 +611,7 @@ impl BlockService { self_ref .sign_and_publish_block( - proposer_fallback, + proposer_fallback.clone(), slot, graffiti, &validator_pubkey, @@ -618,6 +619,74 @@ impl BlockService { ) .await?; + // For Gloas, fetch the execution payload envelope, sign it, and publish it + if fork_name.gloas_enabled() { + self_ref + .fetch_sign_and_publish_payload_envelope(proposer_fallback, slot, &validator_pubkey) + .await?; + } + + Ok(()) + } + + /// Fetch, sign, and publish the execution payload envelope for Gloas. + /// This should be called after the block has been published. + #[instrument(skip_all, fields(%slot, ?validator_pubkey))] + async fn fetch_sign_and_publish_payload_envelope( + &self, + proposer_fallback: ProposerFallback, + slot: Slot, + validator_pubkey: &PublicKeyBytes, + ) -> Result<(), BlockError> { + info!(slot = slot.as_u64(), "Fetching execution payload envelope"); + + // Fetch the envelope from the beacon node (builder_index=0 for local building) + let envelope = proposer_fallback + .request_proposers_last(|beacon_node| async move { + beacon_node + .get_validator_execution_payload_envelope::(slot, 0) + .await + .map(|response| response.data) + .map_err(|e| { + BlockError::Recoverable(format!( + "Error fetching execution payload envelope: {:?}", + e + )) + }) + }) + .await?; + + info!( + slot = slot.as_u64(), + beacon_block_root = %envelope.beacon_block_root, + "Received execution payload envelope, signing" + ); + + // Sign the envelope + let signed_envelope = self + .validator_store + .sign_execution_payload_envelope(*validator_pubkey, envelope) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error signing execution payload envelope: {:?}", + e + )) + })?; + + info!( + slot = slot.as_u64(), + "Signed execution payload envelope, publishing" + ); + + // TODO(gloas): Publish the signed envelope + // For now, just log that we would publish it + debug!( + slot = slot.as_u64(), + beacon_block_root = %signed_envelope.message.beacon_block_root, + "Would publish signed execution payload envelope (not yet implemented)" + ); + Ok(()) } diff --git a/validator_client/validator_store/src/lib.rs b/validator_client/validator_store/src/lib.rs index 4fdbb8064c..87ab669e8d 100644 --- a/validator_client/validator_store/src/lib.rs +++ b/validator_client/validator_store/src/lib.rs @@ -5,8 +5,9 @@ use std::fmt::Debug; use std::future::Future; use std::sync::Arc; use types::{ - Address, Attestation, AttestationError, BlindedBeaconBlock, Epoch, EthSpec, Graffiti, Hash256, - SelectionProof, SignedAggregateAndProof, SignedBlindedBeaconBlock, SignedContributionAndProof, + Address, Attestation, AttestationError, BlindedBeaconBlock, Epoch, EthSpec, + ExecutionPayloadEnvelope, Graffiti, Hash256, SelectionProof, SignedAggregateAndProof, + SignedBlindedBeaconBlock, SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedValidatorRegistrationData, Slot, SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, }; @@ -178,6 +179,13 @@ pub trait ValidatorStore: Send + Sync { /// runs. fn prune_slashing_protection_db(&self, current_epoch: Epoch, first_run: bool); + /// Sign an `ExecutionPayloadEnvelope` for Gloas. + fn sign_execution_payload_envelope( + &self, + validator_pubkey: PublicKeyBytes, + envelope: ExecutionPayloadEnvelope, + ) -> impl Future, Error>> + Send; + /// Returns `ProposalData` for the provided `pubkey` if it exists in `InitializedValidators`. /// `ProposalData` fields include defaulting logic described in `get_fee_recipient_defaulting`, /// `get_gas_limit_defaulting`, and `get_builder_proposals_defaulting`.