Fetch and sign payload envelope

This commit is contained in:
Eitan Seri- Levi
2026-02-03 19:37:09 -08:00
parent 2d321f60eb
commit 1ed80fa35d
8 changed files with 208 additions and 16 deletions

View File

@@ -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<T: SlotClock + 'static, E: EthSpec> 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<E>,
) -> Result<SignedExecutionPayloadEnvelope<E>, 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::<E, FullPayload<E>>(
SignableMessage::ExecutionPayloadEnvelope(&envelope),
signing_root,
&self.task_executor,
None,
)
.await
.map_err(Error::SpecificError)?;
Ok(SignedExecutionPayloadEnvelope {
message: envelope,
signature,
})
}
}

View File

@@ -49,6 +49,7 @@ pub enum SignableMessage<'a, E: EthSpec, Payload: AbstractExecPayload<E> = FullP
SignedContributionAndProof(&'a ContributionAndProof<E>),
ValidatorRegistration(&'a ValidatorRegistrationData),
VoluntaryExit(&'a VoluntaryExit),
ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope<E>),
}
impl<E: EthSpec, Payload: AbstractExecPayload<E>> SignableMessage<'_, E, Payload> {
@@ -70,6 +71,7 @@ impl<E: EthSpec, Payload: AbstractExecPayload<E>> 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.

View File

@@ -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<E>> {
SyncAggregatorSelectionData(&'a SyncAggregatorSelectionData),
ContributionAndProof(&'a ContributionAndProof<E>),
ValidatorRegistration(&'a ValidatorRegistrationData),
ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope<E>),
}
impl<'a, E: EthSpec, Payload: AbstractExecPayload<E>> Web3SignerObject<'a, E, Payload> {
@@ -140,6 +142,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload<E>> Web3SignerObject<'a, E, Pa
MessageType::SyncCommitteeContributionAndProof
}
Web3SignerObject::ValidatorRegistration(_) => MessageType::ValidatorRegistration,
Web3SignerObject::ExecutionPayloadEnvelope(_) => MessageType::ExecutionPayloadEnvelope,
}
}
}

View File

@@ -143,6 +143,7 @@ impl<S: ValidatorStore, T: SlotClock + 'static> BlockServiceBuilder<S, T> {
// Combines a set of non-block-proposing `beacon_nodes` and only-block-proposing
// `proposer_nodes`.
#[derive(Clone)]
pub struct ProposerFallback<T> {
beacon_nodes: Arc<BeaconNodeFallback<T>>,
proposer_nodes: Option<Arc<BeaconNodeFallback<T>>>,
@@ -610,7 +611,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> BlockService<S, T> {
self_ref
.sign_and_publish_block(
proposer_fallback,
proposer_fallback.clone(),
slot,
graffiti,
&validator_pubkey,
@@ -618,6 +619,74 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> BlockService<S, T> {
)
.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<T>,
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::<S::E>(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(())
}

View File

@@ -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<Self::E>,
) -> impl Future<Output = Result<SignedExecutionPayloadEnvelope<Self::E>, Error<Self::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`.